provared 0.6.0 → 0.7.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // provared-check: check a book or a Show from the command line.
3
3
  //
4
- // provared-check <file> [--issuer <thumbprint>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
4
+ // provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
5
5
  // [--block <fingerprint>]... [--voucher <fingerprint>]... [--disclosures <file>] [--cancellation <file>]...
6
6
  // [--seal <file>]... [--witness <fingerprint>]... [--root <top fingerprint>] [--size <entries>] [--json]
7
7
  //
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { readFileSync } from 'node:fs';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { DENIAL_SOURCES, MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, limitWords, neverWords } from '../src/check.js';
23
+ import { DENIAL_SOURCES, MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, isPageAddress, limitWords, neverWords } from '../src/check.js';
24
24
 
25
25
  // Node.js marks its ML-DSA and SLH-DSA as experimental and says so on every
26
26
  // run. The checker does not repeat that warning; any other warning is shown.
@@ -29,16 +29,19 @@ process.on('warning', (w) => {
29
29
  if (w.name !== 'ExperimentalWarning') console.error(`${w.name}: ${w.message}`);
30
30
  });
31
31
 
32
- const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
33
- [--block <fingerprint>]... [--voucher <fingerprint>]... [--disclosures <file>] [--cancellation <file>]...
34
- [--seal <file>]... [--witness <fingerprint>]... [--root <top fingerprint>] [--size <entries>]
35
- [--headers <file>] [--json]
32
+ const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]...
33
+ [--stamp-service <fingerprint>]... [--block <fingerprint>]... [--voucher <fingerprint>]...
34
+ [--disclosures <file>] [--cancellation <file>]... [--seal <file>]... [--witness <fingerprint>]...
35
+ [--root <top fingerprint>] [--size <entries>] [--headers <file>] [--json]
36
36
  provared-check --sample [--json]
37
37
 
38
38
  <file> a book (one entry to a line) or a Show (one JSON object)
39
39
  --sample check the sample record that comes with the library, naming the passkey, the recorder and the
40
40
  time-stamp service it was made with as trusted (they are stand-ins, and the people are invented)
41
41
  --issuer the thumbprint of an issuer key you already trust; may be repeated
42
+ --origin the address of the page at which the person signs permissions, such as https://sign.example.org
43
+ (https://, a website name in lower case, a port only if it is needed, and nothing more); a
44
+ permission signed at any other address is refused; may be repeated
42
45
  --sealer the fingerprint of the key set of a recorder you expect to have sealed the book; may be repeated
43
46
  --stamp-service the fingerprint of the certificate of a time-stamp service you trust; may be repeated
44
47
  --block the fingerprint (64 hex characters) of a block of a public blockchain that you trust to be part of the chain;
@@ -68,14 +71,14 @@ const FINGERPRINT = /^[A-Za-z0-9_-]{43}$/;
68
71
  const BLOCK = /^[0-9a-f]{64}$/;
69
72
 
70
73
  // Options that may be repeated, and those that may be given once.
71
- const LISTS = { '--issuer': 'issuerKeys', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--witness': 'witnesses', '--cancellation': 'cancellations', '--seal': 'seals' };
74
+ const LISTS = { '--issuer': 'issuerKeys', '--origin': 'issuerOrigins', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--witness': 'witnesses', '--cancellation': 'cancellations', '--seal': 'seals' };
72
75
  const ONCE = { '--disclosures': 'disclosures', '--root': 'expectedRoot', '--size': 'expectedSize', '--headers': 'headers' };
73
76
 
74
77
  // The arguments, or the reason they cannot be used. A value that is not a
75
78
  // fingerprint is refused, never set aside: a typing error must not quietly
76
79
  // leave a time-stamp or a name uncounted.
77
80
  function parseArguments(argv) {
78
- const out = { file: null, sample: false, issuerKeys: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], witnesses: [], cancellations: [], seals: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
81
+ const out = { file: null, sample: false, issuerKeys: [], issuerOrigins: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], witnesses: [], cancellations: [], seals: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
79
82
  for (let i = 0; i < argv.length; i++) {
80
83
  const a = argv[i];
81
84
  if (a === '--help' || a === '-h') return { help: true };
@@ -93,6 +96,9 @@ function parseArguments(argv) {
93
96
  return { error: `The value of ${a} is not a fingerprint. A fingerprint is 43 letters, digits, hyphens or underscores, with no spaces.` };
94
97
  }
95
98
  if (a === '--block' && !BLOCK.test(value)) return { error: 'The value of --block is not the fingerprint of a block: 64 characters, the digits 0 to 9 and the letters a to f.' };
99
+ if (a === '--origin' && !isPageAddress(value)) {
100
+ return { error: 'The value of --origin is not a page address in its plain form: https://, a website name in lower case, a port only if it is needed, and nothing more, such as https://sign.example.org.' };
101
+ }
96
102
  if (a === '--size' && !/^\d{1,6}$/.test(value)) return { error: 'The value of --size is not a number of entries.' };
97
103
  if (Object.hasOwn(LISTS, a)) out[LISTS[a]].push(value);
98
104
  else out[ONCE[a]] = a === '--size' ? Number(value) : value;
@@ -101,7 +107,7 @@ function parseArguments(argv) {
101
107
  }
102
108
  if (out.sample) {
103
109
  if (out.file !== null) return { error: '--sample names the sample record itself: give no file with it.' };
104
- for (const name of ['issuerKeys', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'cancellations', 'seals']) {
110
+ for (const name of ['issuerKeys', 'issuerOrigins', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'cancellations', 'seals']) {
105
111
  if (out[name].length) return { error: '--sample names what to trust itself: give no other option with it but --json.' };
106
112
  }
107
113
  if (out.disclosures !== undefined || out.expectedRoot !== undefined || out.expectedSize !== undefined || out.headers !== undefined) {
@@ -503,6 +509,10 @@ function print(result, isShow) {
503
509
  console.log(` Time-stamped in a way you trust? ${stamped}`);
504
510
  if (s.counts.seals || args.witnesses.length) console.log(` Witnessed by a witness you trust? ${witnessedWords(s)}${isShow && s.witnessed ? ' (single pages: this says nothing about totals over the whole book)' : ''}`);
505
511
  console.log(` Checked against keys you named? ${trustWords(s)}`);
512
+ // Shown only where the person named an address: a permission signed
513
+ // elsewhere is a problem, reported above, so here every one was signed
514
+ // at an address they named.
515
+ if (args.issuerOrigins.length) console.log(` Signed at an address you named? yes: every permission was signed at ${[...new Set(args.issuerOrigins)].join(' or ')}`);
506
516
  if (args.seals.length) console.log(` Seals held elsewhere compared? ${heldWords(result)}`);
507
517
  }
508
518
  console.log(` Slips ${s.counts.slips}, stubs ${s.counts.stubs} (countersigned ${s.counts.countersigned}, one-sided ${s.counts.oneSided})${s.counts.seals ? `, seals ${s.counts.seals}` : ''}`);
@@ -608,6 +618,7 @@ try {
608
618
 
609
619
  const options = {};
610
620
  if (args.issuerKeys.length) options.issuerKeys = args.issuerKeys;
621
+ if (args.issuerOrigins.length) options.issuerOrigins = args.issuerOrigins;
611
622
  if (args.sealKeys.length) options.sealKeys = args.sealKeys;
612
623
  if (args.stampServices.length) options.stampServices = args.stampServices;
613
624
  if (args.blocks.length) options.blocks = args.blocks;
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.6.0, a draft.** Published as it is, by one author in their
9
+ **Version 0.7.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
 
@@ -33,14 +33,16 @@ An Internet-Draft is a working document, not a standard.
33
33
  | `src/` | The library: the checker, the writers for every kind of record, the stub writer beside an agent and the connector for an agent's tools. No outside code |
34
34
  | `bin/provared-check.mjs` | The checker for the command line |
35
35
  | `bin/provared-headers.mjs`, `net/` | A command that fetches the chain of Bitcoin block headers from nodes you name, so that a block time-stamp can be checked with no node of your own. The only code here that makes a network request. Experimental |
36
- | `page/` | The checking page and the demonstration page |
36
+ | `page/` | The checking page, the signing page and the demonstration page |
37
37
  | `demo/` | The demonstration: an invented office agent works under a permission and goes outside it four times |
38
38
  | `samples/` | A sample record, written once, that the tests keep checking |
39
- | `tools/` | Three small tools: one wrote the sample record, one writes the checking page as a single file, one writes the shared test files |
39
+ | `tools/` | Small tools: one wrote the sample record, one writes the checking page as a single file, one writes the shared test files, one writes the record page at prova.red/records/, and one writes the files of the signing page's own site, sign.prova.red |
40
40
  | `test/` | The tests |
41
41
  | `test-vectors/` | Shared test files: records and values, each with the answer this library gives. Another implementation of the format can be tested against them |
42
- | `python/` | The Python version, `pip install provared`, which gives the same answers. See its own [README](../python/README.md), and its examples with three agent frameworks in `python/examples/`, and small packages for two of them in `python/integrations/` |
43
- | `site/` | The pages at [prova.red](https://prova.red/): what a record proves, with the checker's answer for the sample record, and a worked example. Published with GitHub Pages by `.github/workflows/pages.yml`, which also serves the checking page at `prova.red/check/` and as one file. The pages load nothing from anywhere and keep nothing about a reader |
42
+ | `integrations/claude-code/` | The recorder beside Claude Code, `npm install -g provared-claude-code`, also a Claude Code plugin. See its own [README](https://github.com/provared/provared/blob/main/integrations/claude-code/README.md) |
43
+ | `mcp-executor/` | An executor for MCP tool servers, `npm install -g provared-mcp-executor`: it holds the tool servers' credentials, checks each tool call against the permission, and countersigns each receipt with keys of its own. See its own [README](https://github.com/provared/provared/blob/main/mcp-executor/README.md) |
44
+ | `python/` | The Python version, `pip install provared`, which gives the same answers. See its own [README](https://github.com/provared/provared/blob/main/python/README.md), and its examples with three agent frameworks in `python/examples/`, and small packages for two of them in `python/integrations/` |
45
+ | `site/` | The pages at [prova.red](https://prova.red/): what a record proves, with the checker's answer for the sample record, and a worked example. Published with GitHub Pages by `.github/workflows/pages.yml`, which also serves the checking page at `prova.red/check/` and as one file. The signing page is served from a repository of its own at `sign.prova.red`, so that the passkeys made there belong to a name that serves nothing else. The pages load nothing from anywhere and keep nothing about a reader |
44
46
 
45
47
  The npm package holds `src/`, `bin/`, `net/`, `spec/`, `docs/`,
46
48
  `samples/` and the checking page. The tests, the shared test files, the
@@ -105,7 +107,7 @@ const passkey = await developmentPasskey();
105
107
  fills in the issuer (its key, page address and marked name) and the
106
108
  agent's keys; by default the slip runs from a minute ago for one day,
107
109
  holds no limits and names no other party, and its purpose says that it
108
- is for development. [examples/first-record.mjs](../examples/first-record.mjs)
110
+ is for development. [examples/first-record.mjs](https://github.com/provared/provared/blob/main/examples/first-record.mjs)
109
111
  is a whole first record in one file. In Python the same is
110
112
  `provared.dev` (`development_passkey`, `development_slip`,
111
113
  `development_recorder`), with the writer as `.writer`.
@@ -151,9 +153,9 @@ be loaded from anywhere. The tool prints the file's SHA-256 fingerprint,
151
153
  so that a copy can be compared with it. (Tried on 8 October 2026 in the
152
154
  three browser builds this repository is tested with, and on 13 October
153
155
  2026 in Chrome 155, Edge 155 and Brave 155.) The file for
154
- version 0.6.0 is attached to the release on GitHub. Its SHA-256
156
+ version 0.7.0 is attached to the release on GitHub. Its SHA-256
155
157
  fingerprint is
156
- `ae48c425616d70849aa8d43b1d71ef7ee56736da3ba6a00eaa4375c428e60a25`.
158
+ `c7b3d5fe1b9168628fc91308c7f952b970c05b3abeb908a132b8b2b78aaab1b5`.
157
159
 
158
160
  **Run the demonstration in a browser.**
159
161
 
@@ -761,6 +763,16 @@ not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
761
763
  though the tool has run and its stub is in the book. `recordTools`
762
764
  names no agent software, and works with any function.
763
765
 
766
+ [examples/frameworks/](https://github.com/provared/provared/tree/main/examples/frameworks) in the repository shows
767
+ `recordTools` beside a JavaScript agent framework, the Vercel AI SDK
768
+ (`ai`, version 7). The SDK's own tool calls the recorded tool from its
769
+ `execute` function, and the SDK hands the words of `NotTaken` back to the
770
+ model as a failed tool call, so the agent is told why a call was not run.
771
+ It runs with a scripted stand-in model and sends nothing anywhere; it has
772
+ its own `package.json`, with exact versions, and is not part of the npm
773
+ package. The Python examples are in
774
+ [python/examples/](https://github.com/provared/provared/tree/main/python/examples) in the repository.
775
+
764
776
  ### Denials: the calls the agent's side refused
765
777
 
766
778
  A book shows what the agent did, not how often it was stopped. Opened
@@ -832,6 +844,9 @@ section 27); any other such name is refused.
832
844
  | `provared.request-id` | An identifier the agent's side chose and sent with the call | `identifierFingerprint` |
833
845
  | `provared.event-id` | An identifier the platform gave for the call | `identifierFingerprint` |
834
846
  | `provared.error` | The error text the agent's software was given | `identifierFingerprint` |
847
+ | `provared.tool-name` | The name of the tool, exactly as the agent's software called it (not the slip's action name) | `identifierFingerprint` |
848
+ | `provared.tool-call-id` | The identifier the model gave the tool call, exactly as received | `identifierFingerprint` |
849
+ | `provared.tool-result` | The tool's result, in the one form of RFC 8785; never the result itself | `argumentsFingerprint` (the same one form) |
835
850
 
836
851
  `DOCUMENT_NAMES` lists them, each with the exact text it stands for. An
837
852
  HTTP request or answer is written as the signature base of RFC 9421
@@ -885,13 +900,42 @@ from its fingerprint.
885
900
 
886
901
  `recordTools` names the arguments `provared.arguments` by default (before
887
902
  version 0.6.0, `arguments`; every checker reads both); a tool can name
888
- them otherwise through its own `details` function.
903
+ them otherwise through its own `details` function. It does not name the
904
+ tool's name or its result: a tool's `details` function can add
905
+ `provared.tool-name`, and `provared.tool-result` is added after the
906
+ action, through `detailsAfter` of `act`. The MCP executor
907
+ (`mcp-executor/`) names all three where it has them. Each of the three
908
+ may be named once in a stub; a checker of version 0.5.0 or 0.6.0 refuses
909
+ a stub that names any of them, and one of 0.4.0 or earlier reads them as
910
+ labels.
911
+
912
+ To show each receipt on the trace span of its call in an observability
913
+ tool, use the OpenTelemetry bridge in `integrations/opentelemetry/` (not
914
+ yet published); a span is a pointer to the receipt, not evidence. It puts
915
+ `provared.trace-id` into a receipt only for a tool that you say sends the
916
+ trace out with its call, in a `traceparent` header, as this section
917
+ defines the name; it cannot see whether the tool really does. Where a
918
+ tool gives the model's identifier for the call (`callId`), the receipt
919
+ names it as `provared.tool-call-id`.
889
920
 
890
921
  ## How signing works
891
922
 
892
923
  - **The person** signs a slip with a passkey. A passkey signs a short
893
924
  value handed to it; that value is the SHA-256 fingerprint of the slip's
894
925
  exact signed bytes, so the signature covers the whole slip.
926
+ - **The signing page is served at an address of its own**,
927
+ [sign.prova.red](https://sign.prova.red/), which serves nothing else
928
+ (its files are copied from `page/` by `tools/make-sign-site.mjs` into
929
+ the public repository `provared/sign`). A passkey can be asked to sign
930
+ by any page under the website name it was made for, and what it signs
931
+ is a fingerprint the person cannot read on the device's prompt, so a
932
+ passkey made for a name that serves other pages could be made to sign
933
+ a permission by one of them. A passkey made at sign.prova.red can be
934
+ asked only there. Whoever checks a record can name the address the
935
+ person signs at (`issuerOrigins`, or `--origin`, or the box on the
936
+ checking page): a permission signed under the same passkey anywhere
937
+ else is then refused (`issuer-origin-not-expected`). The list of
938
+ threats, section 3, says what this closes and what it does not.
895
939
  - **The agent and the service** each sign with two keys at once: Ed25519,
896
940
  which is today's kind, and ML-DSA-87, which is quantum-safe. A record
897
941
  counts only if both signatures check. A forger must break both.
@@ -1087,6 +1131,10 @@ wrong. Never show it as fact: the checking page and the command-line
1087
1131
  checker do not.
1088
1132
 
1089
1133
  Options: `issuerKeys` (the thumbprints of issuer keys you trust),
1134
+ `issuerOrigins` (the addresses of the pages at which the person signs
1135
+ permissions, such as `https://sign.example.org`, each in the one form
1136
+ the format allows; a permission signed anywhere else is refused with
1137
+ `issuer-origin-not-expected`; see "How signing works"),
1090
1138
  `sealKeys` (the fingerprints of the key sets of the recorders you
1091
1139
  expect), `stampServices` (the fingerprints of the certificates of the
1092
1140
  time-stamp services you trust), `blocks` (the fingerprints of the
@@ -1110,8 +1158,9 @@ where the record has a seal, the recorder you expect), 1 (no problem
1110
1158
  found, but the agent is outside its slip, not every signature could be
1111
1159
  checked, or you did not name whom you trust) or 2 (a problem was found,
1112
1160
  or what you gave on the command line could not be used). A value that
1113
- is not a fingerprint is refused, never set aside. The checking page
1114
- gives its green answer on the same terms.
1161
+ is not a fingerprint, or an address given with `--origin` that is not a
1162
+ page address in its plain form, is refused, never set aside. The
1163
+ checking page gives its green answer on the same terms.
1115
1164
 
1116
1165
  Writing records: `prepareSlip` and `assembleSlip`, `prepareApproval` and
1117
1166
  `assembleApproval` (the person), `writeStub` (the agent), `countersign`,
@@ -91,20 +91,47 @@ follows the checking steps of the Web Authentication standard.
91
91
  section 6b. Slips should still be short-lived.
92
92
  - Who the issuer is. The slip proves which key signed, not whose key it
93
93
  is.
94
- - Against a page that has the passkey sign a record dressed as a sign-in.
95
- A passkey signs only a challenge, and the challenge for a slip, an
96
- approval or a cancellation is the fingerprint of the record, which the
97
- person cannot read on the device's prompt. Any page at an origin under
98
- the slip's `rpId` (the website's own operator, a script running on one
99
- of its pages, or the operator of any subdomain) can hand the device the
100
- fingerprint of a record it wrote as an ordinary sign-in challenge: the
101
- client data of a sign-in and of a record have the same shape, and the
102
- person sees a sign-in prompt. No device and no page need be broken into.
103
- The checker compares the origin and the rpId only with what the slip
104
- itself says. So a passkey that signs records should be registered to a
105
- relying party (an `rpId`) used for nothing else, and the page that asks
106
- for the signature should show the record first. The checker has no
107
- option yet by which a reader names the origin they expect.
94
+ - Against a page that has the passkey sign a record dressed as a sign-in,
95
+ where the passkey's website name (its `rpId`) serves anything but the
96
+ signing page. A passkey signs only a challenge, and the challenge for a
97
+ slip, an approval or a cancellation is the fingerprint of the record,
98
+ which the person cannot read on the device's prompt. Any page at an
99
+ origin under the slip's `rpId` (the website's own operator, a script
100
+ running on one of its pages, or the operator of any subdomain) can hand
101
+ the device the fingerprint of a record it wrote as an ordinary sign-in
102
+ challenge: the client data of a sign-in and of a record have the same
103
+ shape, and the person sees a sign-in prompt. No device and no page need
104
+ be broken into. The checker compares the origin and the rpId with what
105
+ the slip itself says; and, where the reader names the address they
106
+ expect the person to sign at (`issuerOrigins`, `--origin`, or the box
107
+ on the checking page; format description, section 4.4), with that
108
+ address too, refusing a slip signed anywhere else
109
+ (`issuer-origin-not-expected`). What each measure does:
110
+ - A passkey made for a name that serves only the signing page closes
111
+ this for that passkey: the device lets it sign only for pages under
112
+ that name, so no other page can have it sign anything. Provared's own
113
+ signing page is served at sign.prova.red, a name that serves nothing
114
+ else.
115
+ - A reader who names the address is protected by naming alone against
116
+ every other address under the `rpId`, whatever name the passkey was
117
+ made for: the client data the passkey signs must say that address, so
118
+ a record signed at any other address under the `rpId` is refused. It
119
+ does not protect against another page served at the named address
120
+ itself.
121
+ - A page that shows the record before it asks for the signature helps
122
+ only against a page that asks openly for a record to be signed. It
123
+ does nothing against a prompt dressed as a sign-in, which shows no
124
+ record.
125
+ - Naming an address protects only the reader who names it. It does not
126
+ stop the passkey signing records at other pages, which readers who
127
+ name no address, or name another one, accept.
128
+
129
+ What stays open: a reader who names no address is protected only by
130
+ which passkey they trust; whoever serves the named address (the host of
131
+ the site, or whoever holds its domain name) can serve another page
132
+ there, and a reader's naming does not tell that page from the real one;
133
+ and a passkey made for a name that serves other pages stays exposed to
134
+ every reader who does not name the address.
108
135
 
109
136
  ## 4. The Stub and the countersignature
110
137
 
@@ -144,8 +171,11 @@ platform gave the call (`provared.event-id`), of one the agent's side
144
171
  chose and sent (`provared.trace-id`, `provared.request-id`), of part
145
172
  of the request and the answer (`provared.http-request`: the method, the
146
173
  address and the body; `provared.http-response`: the status and the
147
- body; no header is covered), or of the error the agent was given
148
- (`provared.error`).
174
+ body; no header is covered), of the error the agent was given
175
+ (`provared.error`), of the name of the tool that was called
176
+ (`provared.tool-name`), of the identifier the model gave the tool call
177
+ (`provared.tool-call-id`) or of the tool's result (`provared.tool-result`,
178
+ in the one form of RFC 8785, as the arguments are).
149
179
 
150
180
  - *What it shows.* Someone who holds the platform's own log can work the
151
181
  fingerprint out again and match the two records, without either
@@ -158,7 +188,11 @@ body; no header is covered), or of the error the agent was given
158
188
  share one fingerprint, so a match with one of them does not show that
159
189
  it was the same call. Only `provared.trace-id`, `provared.request-id`
160
190
  and `provared.event-id` are meant to pick out one call, and only they
161
- may be named more than once in a stub.
191
+ may be named more than once in a stub. `provared.tool-call-id` picks
192
+ out the model's request for the call, which the agent's software may
193
+ carry out more than once (a retry): two stubs then name one identifier.
194
+ A tool's name and a tool's result are content: every call of one tool
195
+ shares its name's fingerprint, and identical results share one.
162
196
  - *The body's bytes* are what RFC 9530 digests: the content after any
163
197
  content coding (such as gzip) and before any transfer coding. A
164
198
  platform that logs the decoded body of a message sent compressed will
@@ -172,8 +206,13 @@ body; no header is covered), or of the error the agent was given
172
206
  at all: which one a platform writes into its log differs from one
173
207
  platform to another, and is for whoever relies on it to find out. That
174
208
  the fingerprint was made from the text the name says: a checker cannot
175
- tell; only someone who holds that text can. An identifier that is
176
- short or easy to guess gives no privacy through its fingerprint.
209
+ tell; only someone who holds that text can. An identifier, a tool's
210
+ name or a result that is short or easy to guess gives no privacy
211
+ through its fingerprint: a tool's name is found by trying the
212
+ framework's or the server's tool names, and a short result (`"ok"`,
213
+ `{}`) by trying the likely values. The record holds fingerprints and
214
+ not the result itself so that the record stays small and fixed, not to
215
+ hide the result.
177
216
  - *Against A1 and A3* (the agent's own side). The binding is written by
178
217
  the agent's side, so a dishonest one can leave it out, or name a
179
218
  wrong identifier; only the match with the platform's log, made later,
@@ -186,10 +225,14 @@ body; no header is covered), or of the error the agent was given
186
225
  match: whoever writes the stub must give the address as sent.
187
226
  - *A reserved name not on the list* is refused (`bad-field`), so that a
188
227
  name that may be given a meaning later cannot be used now with another;
189
- so is one of the four once-only names named twice (two `provared.arguments`
228
+ so is one of the seven once-only names named twice (two `provared.arguments`
190
229
  fingerprints in one stub would leave unclear which was the call's). A
191
230
  book written before this rule that holds such a name no longer passes
192
- its check.
231
+ its check. The three names added after version 0.6.0 (`provared.tool-name`,
232
+ `provared.tool-call-id`, `provared.tool-result`) are refused by a
233
+ checker of version 0.5.0 or 0.6.0, which knows the first seven only,
234
+ and read as labels by one of 0.4.0 or earlier: a book that names them
235
+ checks in full only with a later checker.
193
236
 
194
237
  ## 4a. Limits, conditions, approvals and the other side's records
195
238
 
@@ -1161,11 +1204,12 @@ it show a pass for a record that does not check.
1161
1204
  It asks for every header from the first block onwards, so a node does
1162
1205
  not learn which blocks matter to the person.
1163
1206
  - What the person hands the checking page beside a record (their own
1164
- copies of cancellations, disclosures for covered fields, the blocks
1165
- and fingerprints they trust) is read in the browser and sent nowhere.
1166
- A file that is not JSON, or a line that is not a fingerprint, stops
1167
- the check and says so: it is never set aside silently. The sample
1168
- record is checked without the files handed over.
1207
+ copies of cancellations, disclosures for covered fields, the blocks,
1208
+ the fingerprints and the signing page's address they trust) is read in
1209
+ the browser and sent nowhere. A file that is not JSON, or a line that
1210
+ is not a fingerprint or a page address in its plain form, stops the
1211
+ check and says so: it is never set aside silently. The sample record
1212
+ is checked without the files handed over.
1169
1213
  - The checker depends on no outside code. It uses only the cryptography
1170
1214
  built into the browser or Node.js.
1171
1215
  - **The checking page as one file** (`tools/make-single-page.mjs`) holds
@@ -1241,6 +1285,7 @@ fails if the two differ.
1241
1285
  | `passkey-user-not-verified` | A passkey signature where the device did not confirm the person |
1242
1286
  | `passkey-bad-data` | Passkey values that are cut short or malformed |
1243
1287
  | `issuer-not-expected` | A slip signed by a key other than the ones the checker was told to expect |
1288
+ | `issuer-origin-not-expected` | A slip signed at a page address other than the ones the checker was told to expect (section 3) |
1244
1289
  | `slip-missing` | A stub whose slip is not earlier in the book |
1245
1290
  | `slip-unusable` | A stub whose slip failed its own check |
1246
1291
  | `chain-broken` | A stub that is missing, moved, repeated or changed |
package/net/headers.js CHANGED
@@ -232,6 +232,7 @@ export function apart(a, b) {
232
232
  */
233
233
  export function nodeAddress(text) {
234
234
  const line = String(text).split('#')[0].trim();
235
+ // eslint-disable-next-line security/detect-unsafe-regex -- no repetition inside repetition can match the same text two ways (labels split by a full stop the labels cannot match), so matching takes time in proportion to the input.
235
236
  const m = line.match(/^\[([0-9a-fA-F:.]+)\](?::(\d{1,5}))?$/) ?? line.match(/^([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)\.?(?::(\d{1,5}))?$/);
236
237
  if (!m) return null;
237
238
  const host = m[1].toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "provared",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",
@@ -6,7 +6,7 @@
6
6
  // request at all.
7
7
 
8
8
  import { checkBook, checkShow } from '../src/check.js';
9
- import { readBlocks, readFingerprints, readHanded, renderResult, shown } from './render.js';
9
+ import { readBlocks, readFingerprints, readHanded, readOrigins, renderResult, shown } from './render.js';
10
10
 
11
11
  const status = document.getElementById('status');
12
12
  const result = document.getElementById('result');
@@ -37,6 +37,13 @@ function trusted() {
37
37
  throw new Error('A line in the box for blocks is not the fingerprint of a block. It is 64 characters: the digits 0 to 9 and the letters a to f. Correct it, or empty the box.');
38
38
  }
39
39
  if (blocks.values.length) options.blocks = blocks.values;
40
+ // The addresses at which the person expects the permissions to have been
41
+ // signed: each in the one form the format allows, or the check stops.
42
+ const origins = readOrigins(document.getElementById('trust-origins').value);
43
+ if (origins.bad.length) {
44
+ throw new Error('A line in the box for the address of the signing page is not a page address in its plain form: https://, a website name in lower case, a port only if it is needed, and nothing more, such as https://sign.example.org. Correct it, or empty the box.');
45
+ }
46
+ if (origins.values.length) options.issuerOrigins = origins.values;
40
47
  return options;
41
48
  }
42
49
 
package/page/index.html CHANGED
@@ -20,6 +20,8 @@
20
20
  <p class="quiet">A check says more when you tell it which keys and services you expect. Put one fingerprint on each line. Use fingerprints you obtained yourself, from the people concerned.</p>
21
21
  <label for="trust-issuer">The person's passkey (its thumbprint)</label>
22
22
  <textarea id="trust-issuer" rows="2" spellcheck="false"></textarea>
23
+ <label for="trust-origins">The address of the page at which the person signs permissions (such as https://sign.example.org; a permission signed at any other address is refused)</label>
24
+ <textarea id="trust-origins" rows="2" spellcheck="false"></textarea>
23
25
  <label for="trust-sealer">Whoever keeps and seals the book (the fingerprint of its key set)</label>
24
26
  <textarea id="trust-sealer" rows="2" spellcheck="false"></textarea>
25
27
  <label for="trust-stamps">Time-stamp services (the fingerprint of each service's certificate)</label>
package/page/render.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // is kept inside its own box, so that a record cannot hide, reorder or
8
8
  // draw over the words around it.
9
9
 
10
- import { DENIAL_SOURCES, REFUSAL_REASONS, conditionWords, limitWords, neverWords } from '../src/check.js';
10
+ import { DENIAL_SOURCES, REFUSAL_REASONS, conditionWords, isPageAddress, limitWords, neverWords } from '../src/check.js';
11
11
 
12
12
  // The same holds for characters that cannot be seen (zero-width characters,
13
13
  // soft hyphens, line separators, fillers), with which two different labels
@@ -54,6 +54,20 @@ export function readBlocks(text) {
54
54
  return { values: lines.filter((line) => /^[0-9a-fA-F]{64}$/.test(line)), bad: lines.filter((line) => !/^[0-9a-fA-F]{64}$/.test(line)) };
55
55
  }
56
56
 
57
+ /**
58
+ * Read the box of page addresses, one to a line: the addresses at which the
59
+ * person expects the permissions to have been signed, each in the one form
60
+ * the format allows (https://, a website name in lower case, a port only if
61
+ * it is needed, and nothing more). As with the other boxes, a line that is
62
+ * not one is never dropped silently.
63
+ * @param {string} text
64
+ * @returns {{values: string[], bad: string[]}}
65
+ */
66
+ export function readOrigins(text) {
67
+ const lines = String(text).split(/\s+/).filter((line) => line !== '');
68
+ return { values: lines.filter((line) => isPageAddress(line)), bad: lines.filter((line) => !isPageAddress(line)) };
69
+ }
70
+
57
71
  /**
58
72
  * @param {unknown} text
59
73
  * @returns {string} the text, safe to place among other words
@@ -82,6 +96,17 @@ export function el(tag, className, text) {
82
96
 
83
97
  const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
84
98
 
99
+ /**
100
+ * The heading of the card of the top fingerprint. It names the Seal only
101
+ * where the record holds one: a book, or the pages of a Show, with no seal
102
+ * has a top fingerprint and no seal.
103
+ * @param {{summary?: {counts?: {seals?: number}}}} result the checker's answer
104
+ * @returns {string}
105
+ */
106
+ export function topFingerprintHeading(result) {
107
+ return (result?.summary?.counts?.seals ?? 0) > 0 ? 'The top fingerprint (the Seal)' : 'The top fingerprint';
108
+ }
109
+
85
110
  /**
86
111
  * A time from a record, in words. The checker has already confirmed its
87
112
  * form; anything else is shown as it is.
@@ -625,7 +650,7 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
625
650
 
626
651
  if (result.root) {
627
652
  const seal = el('div', 'card seal');
628
- seal.append(el('h3', 'kind', 'The top fingerprint (the Seal)'));
653
+ seal.append(el('h3', 'kind', topFingerprintHeading(result)));
629
654
  const list = el('dl');
630
655
  row(list, 'Top fingerprint', result.root, true);
631
656
  row(list, 'Entries in the book', String(result.size));