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.
- package/CHANGELOG.md +514 -0
- package/README.md +13 -1
- package/bin/provared-check.mjs +20 -9
- package/docs/guide.md +60 -11
- package/docs/threat-model.md +71 -26
- package/net/headers.js +1 -0
- package/package.json +1 -1
- package/page/check-page.js +8 -1
- package/page/index.html +2 -0
- package/page/render.js +27 -2
- package/spec/provared-format.md +86 -16
- package/src/book.js +6 -2
- package/src/check.d.ts +1 -0
- package/src/check.js +1 -1
- package/src/denial.js +1 -1
- package/src/der.js +1 -0
- package/src/documents.js +8 -3
- package/src/signatures.js +6 -1
- package/src/slip.js +72 -5
- package/src/types.d.ts +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,519 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 (1 November 2026)
|
|
4
|
+
|
|
5
|
+
Version 0.7.0 makes three more document names part of the format (section 27 of the format description: `provared.tool-name`, `provared.tool-call-id` and `provared.tool-result`), and lets whoever checks a record name the addresses at which a permission must have been signed (`issuerOrigins`, Python `issuer_origins`, `--origin` on the command line, and a box on the checking page), so that a permission that another page under the passkey's website name had the passkey sign, by presenting it as a sign-in, is refused. Beside the library, two programs are published on npm for the first time, each at 0.1.0: `provared-mcp-executor`, an executor for MCP tool servers, which holds the tool servers' credentials, refuses a tool call the permission does not allow, and countersigns each receipt with keys of its own; and `provared-claude-code`, the recorder beside Claude Code, which can now also be installed as a Claude Code plugin. Releases are now published from the author's signed tag by a GitHub workflow, and each npm version waits for the author's approval with a passkey. What a reader must know: no record's bytes change, and a book written by 0.6.0 checks as it did, since 0.6.0 refused the three new names and so never wrote them; but a book that names any of the three new names checks in full only with a checker of 0.7.0 or later, because a checker of 0.5.0 or 0.6.0 refuses it (`bad-field`, as for every `provared.` name it does not know) and one of 0.4.0 or earlier reads the names as labels. The MCP executor writes two of them in every receipt, so its books need 0.7.0. The framework packages `provared-langchain` and `pydantic-ai-provared` stay at 0.2.0, unchanged: they ask for `provared` 0.6.0 or later, and what they write is the same with 0.7.0.
|
|
6
|
+
|
|
7
|
+
- **Versions.** The library is 0.7.0, in `package.json` and `python/pyproject.toml`. Ranges on the library: `provared-mcp-executor` and the OpenTelemetry bridge (not published) now ask for `^0.7.0`, where they asked for `>0.6.0 <0.8.0`; the recorder beside Claude Code, the gateway and the sealer (neither of the last two published) ask for `>=0.6.0 <0.8.0`, where they asked for `^0.6.0`, which left 0.7.0 out (the recorder's `package-lock.json`, which a Claude Code plugin installs from, still holds 0.6.0, which the new range accepts, and the plugin's test of that lockfile now takes a range of that form as well as a caret range); the Hermes plugin (not published) asks for `>=0.4.0,<0.8`, where it asked for `<0.7`. The README and the guide name the two packages now on npm, with their install lines.
|
|
8
|
+
|
|
9
|
+
- The gateway's test "M3: an agent may have only so many calls waiting"
|
|
10
|
+
no longer depends on how busy the computer is: each wait has 120
|
|
11
|
+
seconds, not 20, and the status is shown to answer without waiting for a
|
|
12
|
+
call by the upstream still holding that call, not by a time under one
|
|
13
|
+
second. It checks the same limit.
|
|
14
|
+
- **Three more reserved document names** (format description, section
|
|
15
|
+
27), in both languages: `provared.tool-name` (the fingerprint of the
|
|
16
|
+
tool's name, exactly as the agent's software called it, which is not the
|
|
17
|
+
slip's action name), `provared.tool-call-id` (the fingerprint of the
|
|
18
|
+
identifier the model gave the tool call, exactly as received) and
|
|
19
|
+
`provared.tool-result` (the fingerprint of the tool's result in the one
|
|
20
|
+
form of RFC 8785, as `provared.arguments` is of the arguments; never the
|
|
21
|
+
result itself). Each may be named once in a record; the three
|
|
22
|
+
identifiers of 0.5.0 are still the only names that may be named more
|
|
23
|
+
than once. The first two are fingerprinted with `identifierFingerprint`
|
|
24
|
+
(`identifier_fingerprint`), the third with `argumentsFingerprint`
|
|
25
|
+
(`arguments_fingerprint`); no new function. `recordTools`,
|
|
26
|
+
`record_tools` and `record_function` still name the arguments only, so
|
|
27
|
+
what the published framework packages write is unchanged. The MCP
|
|
28
|
+
executor writes `provared.tool-name` and `provared.tool-result` in place
|
|
29
|
+
of its labels `mcp.tool` and `mcp.result`; the OpenTelemetry bridge
|
|
30
|
+
writes `provared.tool-call-id` where a tool gives `callId`, which before
|
|
31
|
+
went on the span only, and still never writes `provared.event-id`, the
|
|
32
|
+
platform's identifier, which only the caller can name. Both packages now
|
|
33
|
+
need 0.7.0 or later (`^0.7.0`). Shared test files:
|
|
34
|
+
`stub-contents.json`, `http-fingerprints.json` and `denial-contents.json`
|
|
35
|
+
remade, with the three names accepted once, refused twice, and names
|
|
36
|
+
close to them (`provared.Tool-result`, `provared.tool_result`,
|
|
37
|
+
`provared.tool-call`) refused as unknown. **Compatibility:** no book
|
|
38
|
+
written by version 0.6.0 fails its check because of this change, since
|
|
39
|
+
0.6.0 refused these names and so never wrote them; a checker of version
|
|
40
|
+
0.5.0 or 0.6.0 refuses a book that names any of the three
|
|
41
|
+
(`bad-field`, as for every `provared.` name it does not know), and a
|
|
42
|
+
checker of 0.4.0 or earlier, from before the names were reserved, reads
|
|
43
|
+
them as labels; so a book written with them checks in full only with a
|
|
44
|
+
later version. The guide, the Python README, the list of threats and the
|
|
45
|
+
type declarations say so.
|
|
46
|
+
- **An executor for MCP tool servers**, `mcp-executor/`
|
|
47
|
+
(`provared-mcp-executor` 0.1.0), stage 1: the stdio
|
|
48
|
+
transport. An executor, in the words of the Internet-Draft
|
|
49
|
+
`draft-izmaylov-agent-permission-receipts-02`, is software other than
|
|
50
|
+
the agent that holds what its actions need and refuses an action the
|
|
51
|
+
permission does not allow. The agent's MCP client starts the executor as
|
|
52
|
+
if it were the tool server; the executor starts the real server with its
|
|
53
|
+
credential (from a file or the executor's own environment, never the
|
|
54
|
+
agent's), and passes every message of either revision of the protocol
|
|
55
|
+
through unchanged except two. `tools/call` is matched to an action by
|
|
56
|
+
the tool's name in a rules file, checked by the check before acting
|
|
57
|
+
through the agent's stub writer, which the executor holds; a call the
|
|
58
|
+
permission allows is sent on and gets a receipt that names the executor
|
|
59
|
+
in `with`, binds the arguments and the server's result by fingerprint,
|
|
60
|
+
and is countersigned with the executor's own key set once the outcome is
|
|
61
|
+
known; a call it does not allow is answered as a tool error that says
|
|
62
|
+
why, and written as a denial. `tools/list` comes back without the tools
|
|
63
|
+
that have no rule. Beside the book the executor keeps its own copy, one
|
|
64
|
+
tagged line before each call is sent (made sure to be on the disk
|
|
65
|
+
first) and one once its receipt is in the book, and `compare` holds that
|
|
66
|
+
copy against a book to find a receipt that was left out, which the draft
|
|
67
|
+
leaves unspecified. A call the executor cannot record is not sent. A
|
|
68
|
+
call cut off by a stop gets its receipt at the next start, from the copy,
|
|
69
|
+
naming `provared.error`. Lines from the agent are read strictly (no
|
|
70
|
+
batch, no member name given twice, an id that is text or a whole number),
|
|
71
|
+
and refused otherwise. The design, the HTTP transport and the content
|
|
72
|
+
rules of the plan's F-157 (design only), and 36 numbered choices for
|
|
73
|
+
review are in `mcp-executor/DESIGN.md`. Nothing in the library changes.
|
|
74
|
+
The test workflow runs the package's tests, and ESLint covers the folder.
|
|
75
|
+
An independent review of the first draft found one serious fault and
|
|
76
|
+
six of medium weight, each now fixed and a test (`test/review1.test.mjs`):
|
|
77
|
+
a receipt lost for good where the book file could not be written after a
|
|
78
|
+
call (the copy now keeps the outcome and the next start writes the
|
|
79
|
+
receipt from it); a late answer from a server that ignores cancellation
|
|
80
|
+
taken for a later call's under the same client id (each call now goes to
|
|
81
|
+
the server under an id of the executor's own); a `tools/call` sent as a
|
|
82
|
+
notification forwarded unchecked (dropped); the process staying alive
|
|
83
|
+
after its server ended (it ends with the server's code); no cap on calls
|
|
84
|
+
or list requests waiting (`maxWaiting`, default 16; 256 lists); a bare
|
|
85
|
+
program name found through the client's `PATH` (named by path, or
|
|
86
|
+
`server.path`, with a warning otherwise); a result that cannot be
|
|
87
|
+
fingerprinted leaving no outcome in the receipt (`provared.error` with a
|
|
88
|
+
fixed text). Smaller points: each copy line carries its number, so a
|
|
89
|
+
line taken out, put twice or moved stops the start; `compare` tells a
|
|
90
|
+
receipt the executor could not write apart from one the book lacks, does
|
|
91
|
+
not agree where the book has a problem, and makes no key; a whole number
|
|
92
|
+
beyond 2^53 - 1 anywhere in a line is refused; `check` writes nothing;
|
|
93
|
+
a permission naming other services beside the executor is warned about.
|
|
94
|
+
The founder's decision F-158 (content rules in the executor's settings,
|
|
95
|
+
fingerprinted into receipts and into the permission's `agent.software`;
|
|
96
|
+
the automatic late receipt kept; reserved names for a tool's name and
|
|
97
|
+
result to come in the library and draft -03) is recorded in `DESIGN.md`.
|
|
98
|
+
A narrow second review of those fixes found one serious and one medium
|
|
99
|
+
fault in the id mapping, both fixed and tests (`test/review2.test.mjs`):
|
|
100
|
+
a server answer holding a whole number beyond 2^53 - 1 was refused as
|
|
101
|
+
unreadable, so the call timed out and the receipt named a false timeout
|
|
102
|
+
(the server's lines are now read without that rule, reach the client in
|
|
103
|
+
the server's own bytes with only the id replaced, and such a result is
|
|
104
|
+
named as one that cannot be fingerprinted; an answer that cannot be read
|
|
105
|
+
at all still reaches the client, and the receipt says so); and only tool
|
|
106
|
+
calls were renamed, with guessable ids, so a `ping` under a call's id
|
|
107
|
+
could settle the call with the ping's answer (every request now goes
|
|
108
|
+
under an id with a random part). Requests the server never answers are
|
|
109
|
+
forgotten past 1,000. A receipt written at a restart names
|
|
110
|
+
`executor.written-late`, so that the book itself says so. A final check
|
|
111
|
+
found two small points, fixed: a server line naming `id` twice at its
|
|
112
|
+
top level, or a batch, is matched with no call and handed on as it is,
|
|
113
|
+
and the call ends by the timeout with a "no answer" receipt.
|
|
114
|
+
|
|
115
|
+
- **The recorder beside Claude Code as a Claude Code plugin.** It can now
|
|
116
|
+
be installed, with its hooks, in two commands in Claude Code:
|
|
117
|
+
`/plugin marketplace add provared/provared`, then
|
|
118
|
+
`/plugin install provared-recorder@provared`. A marketplace file at the
|
|
119
|
+
top of the repository (`.claude-plugin/marketplace.json`, the place
|
|
120
|
+
`/plugin marketplace add` looks) lists one plugin, the folder
|
|
121
|
+
`integrations/claude-code`, which gains `.claude-plugin/plugin.json`
|
|
122
|
+
(the name `provared-recorder`, the package's version) and
|
|
123
|
+
`hooks/hooks.json`: the hooks `init` writes, each run through the
|
|
124
|
+
plugin's entry, `plugin.mjs`, by the plugin's own paths
|
|
125
|
+
(`${CLAUDE_PLUGIN_ROOT}`), and at the start of a session a new command,
|
|
126
|
+
`session-start`. It starts the recorder as `ensure` does and, where
|
|
127
|
+
something stands in the way (no library, no permission yet, a recorder
|
|
128
|
+
that does not start or is still starting, the recorder's hooks in force
|
|
129
|
+
twice), tells the person and Claude in Claude Code's own fields for
|
|
130
|
+
that, within 27 seconds; where all is well it says nothing. Nothing
|
|
131
|
+
signs for the person: they run `init` (which, run through the plugin,
|
|
132
|
+
writes no hooks), sign at https://sign.prova.red/ with their own
|
|
133
|
+
passkey, and run `use`, as the first session tells them. Before each
|
|
134
|
+
call the hook, where no permission was ever put in place, now says that,
|
|
135
|
+
with the same steps, rather than only that the recorder is not running,
|
|
136
|
+
and it waits for the recorder for at most 25 seconds from its own start.
|
|
137
|
+
|
|
138
|
+
- **The plugin's library, by an exact path.** Claude Code 2.1.167 copied a
|
|
139
|
+
plugin fetched from a git repository into its cache without the library
|
|
140
|
+
`provared`, so every call was refused and the start of a session failed;
|
|
141
|
+
and Node.js would have looked for the name in the folders above the
|
|
142
|
+
plugin, such as the person's home folder. The plugin's entry
|
|
143
|
+
(`plugin.mjs`, with `plugin-library.mjs`) now takes the library from
|
|
144
|
+
the plugin's own `node_modules` where Claude Code put it there, or else
|
|
145
|
+
installs it itself, at a session's start or at a command the person runs,
|
|
146
|
+
with `npm ci --ignore-scripts` from the folder's new `package-lock.json`
|
|
147
|
+
(exactly 0.6.0, the package checked by npm against the fingerprint
|
|
148
|
+
the lockfile holds, no install script run, npm's own program run by Node.js
|
|
149
|
+
with no shell and stopped for certain at its time limit), into the
|
|
150
|
+
plugin's data folder, and loads it from there alone, in this program and
|
|
151
|
+
in every program it starts. After that, the copy is trusted by where it
|
|
152
|
+
sits, and is not checked again before each use. Whatever goes wrong in
|
|
153
|
+
the entry itself (a damaged copy, a missing lockfile, a Node.js without
|
|
154
|
+
`module.registerHooks`), a call is refused before it is taken and Claude
|
|
155
|
+
Code is told after it, as the hook itself does. A settings file with
|
|
156
|
+
comments is looked through for the recorder's hooks as JSON with
|
|
157
|
+
comments. Where it cannot, the session says so,
|
|
158
|
+
every call is refused, and each refusal gives the one command that mends
|
|
159
|
+
it (`plugin.mjs install-library`). Checked with Claude Code 2.1.167's own
|
|
160
|
+
`claude plugin validate`, with and without `--strict`, and by installing
|
|
161
|
+
the plugin, from a marketplace whose entry names a git copy of this
|
|
162
|
+
branch, into a Claude Code settings folder of its own, then running its
|
|
163
|
+
hooks and `session-start` from the copy in the plugin cache.
|
|
164
|
+
|
|
165
|
+
- **Not recorded twice.** Claude Code runs a plugin's hook and the same
|
|
166
|
+
hook in a settings file side by side. Before each call, the plugin's
|
|
167
|
+
hook (and only it: Claude Code names the plugin's folder to it) looks in
|
|
168
|
+
the person's and the project's settings files for a hook that runs a
|
|
169
|
+
recorder's `hook.mjs`, in the forms the README lists (a word joined from
|
|
170
|
+
quoted and unquoted parts as a shell joins it; the project's and the
|
|
171
|
+
home folder written as variables or `~`; a copy since deleted, on a path
|
|
172
|
+
that names Provared); where it finds one, it refuses the call and says
|
|
173
|
+
how to take one of the two out. It reads no settings file larger than
|
|
174
|
+
1 MB, looks at no more than 20,000 words, for no more than 2 seconds,
|
|
175
|
+
and refuses the call past any of these. After a call it records it
|
|
176
|
+
whatever it finds. A new command, `remove-hooks`, takes the hooks `init`
|
|
177
|
+
wrote, from any copy of the recorder, out of a project's
|
|
178
|
+
`.claude/settings.local.json`, and nothing else, by writing a new file
|
|
179
|
+
that then takes the old one's place, and only where the file did not
|
|
180
|
+
change while it was read. `init` gains `--no-hooks`, refuses any other
|
|
181
|
+
word after it, and warns where it writes hooks while the plugin is
|
|
182
|
+
installed. The new file `settings-hooks.mjs` is in the npm package.
|
|
183
|
+
|
|
184
|
+
- **The heading of the top fingerprint.** The checking page heads the card
|
|
185
|
+
of a record's top fingerprint "The top fingerprint (the Seal)" only where
|
|
186
|
+
the record holds a seal, and "The top fingerprint" where it holds none
|
|
187
|
+
(`topFingerprintHeading` in `page/render.js`). The "try it" page no
|
|
188
|
+
longer relabels the heading itself. Nothing else the page shows changes,
|
|
189
|
+
and the command-line checker, the Python version and the shared test
|
|
190
|
+
files never held these words.
|
|
191
|
+
|
|
192
|
+
- **OpenSSF Scorecard.** A workflow, `.github/workflows/scorecard.yml`,
|
|
193
|
+
runs the Open Source Security Foundation's Scorecard on each push to
|
|
194
|
+
main, every Sunday and when a branch protection rule changes, and
|
|
195
|
+
publishes the result to api.scorecard.dev and to this repository's code
|
|
196
|
+
scanning page. Laid out as the Scorecard project's own example; no
|
|
197
|
+
permission at the top, and only `security-events: write` and
|
|
198
|
+
`id-token: write` in its one job. It is the first workflow here to use
|
|
199
|
+
an action that is not GitHub's own (`ossf/scorecard-action` v2.5.0,
|
|
200
|
+
pinned to its commit). Pinning fixes the action's own files, not all
|
|
201
|
+
that runs: at that commit the action runs the container
|
|
202
|
+
`ghcr.io/ossf/scorecard-action:v2.5.0`, chosen by its tag (on
|
|
203
|
+
31 October 2026 the tag named the image
|
|
204
|
+
sha256:1e1481c65fc2407d3fd6ab18a23c741eb4d1c0d9359ebafc4b3ab13aa61d8627).
|
|
205
|
+
It cannot be pinned by that fingerprint, because the Scorecard service
|
|
206
|
+
takes a result only from the named action. The job's token can only
|
|
207
|
+
send results to the code scanning page and ask for an identity token,
|
|
208
|
+
so at worst a changed container could publish a false result. The
|
|
209
|
+
README shows the Scorecard badge.
|
|
210
|
+
|
|
211
|
+
- **Static analysis.** `.github/workflows/tests.yml` gains a job that runs
|
|
212
|
+
ESLint over the JavaScript and Ruff over the Python on every push and
|
|
213
|
+
pull request, with rules that look for faults (eval, unsafe regular
|
|
214
|
+
expressions, child processes, weak fingerprint methods, subprocess with
|
|
215
|
+
shell=True and the like), not style. Both are development tools only,
|
|
216
|
+
kept in `tools/lint/` at exact versions (its `package-lock.json`, and
|
|
217
|
+
`requirements.txt` with each file's fingerprint), and are in neither
|
|
218
|
+
published package. A finding that is not a fault is set aside on its
|
|
219
|
+
own line, with a comment giving the reason. Faults found and fixed, none
|
|
220
|
+
changing what the library does: the Hermes plugin's `cli.py` used a
|
|
221
|
+
backslash inside an f-string's braces, which Python 3.11, the oldest
|
|
222
|
+
the plugin supports, cannot read; three Python tests expected any
|
|
223
|
+
exception where the library raises `Refusal`, and now expect
|
|
224
|
+
`Refusal`; three imports were never used. ESLint also covers
|
|
225
|
+
`conformance/`, and Ruff also covers `tools/release/`; there
|
|
226
|
+
`compare_wheel.py` now reads only `https://` addresses, since the
|
|
227
|
+
second address it reads comes from the index's own answer.
|
|
228
|
+
|
|
229
|
+
- **Warnings are failures.** The Python tests run with `python -W error`
|
|
230
|
+
(with two filters: a warning raised inside a LangChain dependency on
|
|
231
|
+
Python 3.14, and, in the Hermes plugin's job, the plugin's own notices to
|
|
232
|
+
the person, which it raises inside Hermes on purpose). The JavaScript tests no longer hide Node.js's warnings,
|
|
233
|
+
and a step after each run fails on any warning other than Node.js's
|
|
234
|
+
notice that ML-DSA or SLH-DSA is experimental
|
|
235
|
+
(`tools/lint/node-warnings.sh`); each run keeps its own exit code
|
|
236
|
+
(`set -o pipefail`), so a failing run is not hidden by the step that
|
|
237
|
+
saves its output. One was found and fixed: the seal
|
|
238
|
+
writer handed a Web Crypto key of another method to Node.js's
|
|
239
|
+
`createPrivateKey`, which Node.js has deprecated (DEP0203); such a key
|
|
240
|
+
is now refused before it is handed over, with the same answer as before.
|
|
241
|
+
|
|
242
|
+
- **Smaller changes.** The site's publishing workflow gives each job only
|
|
243
|
+
the permissions it needs (`pages: write` and `id-token: write` in the
|
|
244
|
+
deploy job alone) and checks out without keeping a token.
|
|
245
|
+
`CONTRIBUTING.md` names GitHub's private reporting form beside email
|
|
246
|
+
for security faults, and says that a change in behaviour comes with
|
|
247
|
+
tests.
|
|
248
|
+
|
|
249
|
+
- **A reader may name the address a permission must have been signed
|
|
250
|
+
at** (25 to 31 October 2026), in both languages. A passkey can be asked
|
|
251
|
+
to sign by any page under the website name it was made for (its relying
|
|
252
|
+
party), and what it signs is a fingerprint the person cannot read on the
|
|
253
|
+
device's prompt; so a page served under that name, or a script on one,
|
|
254
|
+
could have a passkey made for `prova.red` sign a permission it wrote, by
|
|
255
|
+
presenting the permission's fingerprint as a sign-in challenge, and a
|
|
256
|
+
checker, which compared the address only with what the permission itself
|
|
257
|
+
said, would pass it (the list of threats, section 3, said so since 0.6.0,
|
|
258
|
+
with no option to close it). Now the checker's option `issuerOrigins`
|
|
259
|
+
(Python `issuer_origins`; the command line `--origin`, which may be
|
|
260
|
+
repeated; a box on the checking page) names the addresses at which
|
|
261
|
+
permissions must have been signed, each in the one form the format
|
|
262
|
+
allows for `issuer.origin` (`isPageAddress`, Python `is_page_address`,
|
|
263
|
+
gives the form). A permission signed anywhere else is refused with the
|
|
264
|
+
new code `issuer-origin-not-expected`, after the passkey has been
|
|
265
|
+
compared as before, and the stubs under it are not compared; a
|
|
266
|
+
permission signed at an address named gets a note that says so; a list
|
|
267
|
+
that is not in that form, or longer than 64, is a problem with the whole
|
|
268
|
+
check (`bad-field`), never set aside. An approval and a cancellation are
|
|
269
|
+
held to the permission's address by the passkey check already. The
|
|
270
|
+
command-line checker says "Signed at an address you named?" where
|
|
271
|
+
`--origin` was given, and refuses a value not in its plain form. Nothing
|
|
272
|
+
changes for a reader who names no address, and no record's bytes change.
|
|
273
|
+
Format description, section 4.4; the list of threats, sections 3, 7 and
|
|
274
|
+
8; the guide, "How signing works". Shared test files: `slips.json` is
|
|
275
|
+
made again with eleven new cases (every record in it is signed afresh),
|
|
276
|
+
`denial-constants.json` with the new code, and `issuer-origins.json`
|
|
277
|
+
and `issuer-origin-shows.json` are new. Tests:
|
|
278
|
+
`test/issuer-origins.test.mjs`, `python/tests/test_issuer_origins.py`,
|
|
279
|
+
and cases in `test/cli.test.mjs`, `test/page.test.mjs` and
|
|
280
|
+
`test/refusals.test.mjs`.
|
|
281
|
+
|
|
282
|
+
- **The signing page says why it is served at a name of its own**, and
|
|
283
|
+
the one tool that writes that site. The page (`page/sign.html`) now
|
|
284
|
+
says that the passkey it makes belongs to the page's website name and
|
|
285
|
+
to nothing else, why the page is served at a name that serves nothing
|
|
286
|
+
else, and that whoever checks a record can name that address; shown
|
|
287
|
+
inside another page, it now says to open sign.prova.red in a tab of its
|
|
288
|
+
own (before: prova.red/sign/; the pages name no address with its
|
|
289
|
+
scheme, which a test holds). `tools/make-sign-site.mjs`
|
|
290
|
+
writes the whole of the repository `provared/sign`, from which
|
|
291
|
+
sign.prova.red is served, from this repository: under `site/` the page
|
|
292
|
+
as `index.html`, its script, the renderer, the style sheet and every
|
|
293
|
+
module of the library, each byte for byte, with `site/files.sha256` in
|
|
294
|
+
the form `sha256sum --check` reads; and the repository's own README,
|
|
295
|
+
publishing workflow (with the actions pinned to the commits this
|
|
296
|
+
repository's workflow pins) and licence. It takes `--from <tag or
|
|
297
|
+
commit>`, which the README and the workflow name as where the copies
|
|
298
|
+
come from, and reads every file with git at that tag or commit, never
|
|
299
|
+
from the working tree: a file not committed, or changed and not
|
|
300
|
+
committed, cannot reach the site, and the modules copied are those git
|
|
301
|
+
holds directly under `src/` there. A name that git cannot find is
|
|
302
|
+
refused (code 2), and nothing is written. A tag is named as the tag;
|
|
303
|
+
any other name as the commit's full hash. With `--check` it compares a
|
|
304
|
+
folder written before with the same files, read the same way, and names
|
|
305
|
+
what differs. Run against the copy made by hand on 25 October 2026 from
|
|
306
|
+
tag v0.6.0, with `--from v0.6.0` it finds every file equal; with
|
|
307
|
+
`--from` this branch's commit it finds the seven files that this change
|
|
308
|
+
and the one above change (the page, its script, the renderer, and
|
|
309
|
+
`src/book.js`, `check.js`, `denial.js` and `slip.js`) and their list of
|
|
310
|
+
fingerprints different, and the README and the workflow, which name
|
|
311
|
+
where the copies come from. Test: `test/sign-site.test.mjs`, which
|
|
312
|
+
builds a small repository of its own with a tag, so that it needs
|
|
313
|
+
neither this repository's tags nor its history.
|
|
314
|
+
The guide's table of folders names the tool.
|
|
315
|
+
|
|
316
|
+
- **The recorder beside Claude Code and the Hermes plugin print links to
|
|
317
|
+
https://sign.prova.red/** (their `SIGNING_PAGE`), where they printed
|
|
318
|
+
links to prova.red/sign/; their READMEs say what a link printed by an
|
|
319
|
+
earlier version begins with and how to open it at the new address.
|
|
320
|
+
|
|
321
|
+
- **The recorder beside Claude Code: a passkey can be retired.** The
|
|
322
|
+
recorder's list of passkeys, `trusted-passkeys.json`, served two
|
|
323
|
+
purposes with one list: which passkeys may sign a new permission, and
|
|
324
|
+
which passkeys' permissions it opens its books with (the book in force
|
|
325
|
+
and the kept ones, for late receipts and for counting limits across
|
|
326
|
+
books), and it refuses to start where the book in force holds a
|
|
327
|
+
permission from a passkey not on it. So a passkey that had signed
|
|
328
|
+
permissions in the record could not be taken off the list without
|
|
329
|
+
stopping the recorder. Now the file holds two lists: `passkeys`, every
|
|
330
|
+
passkey whose permissions are in the record, written and added to as
|
|
331
|
+
before and never shortened; and among them `retired`, written by the
|
|
332
|
+
new commands `retire-passkey <thumbprint>` and
|
|
333
|
+
`admit-passkey <thumbprint>`. A retired passkey's permissions stand,
|
|
334
|
+
the recorder still starts on the books that hold them and still counts
|
|
335
|
+
limits across those books, and a new permission from it is refused,
|
|
336
|
+
with or without `--new-passkey`, by `renew`, by `use`, by the running
|
|
337
|
+
service and at start-up where a new permission was put in place by
|
|
338
|
+
hand, until the person admits it again; only a passkey on the list can
|
|
339
|
+
be retired. The retired list is read from the file at each renewal, so
|
|
340
|
+
the command counts at once, while the recorder runs or not. The list is
|
|
341
|
+
read again just before it is written, and the two are merged: every
|
|
342
|
+
passkey on the file and every one added, never fewer, and the retired
|
|
343
|
+
list with the one change asked for, written to a temporary file of its
|
|
344
|
+
own and renamed into place; so `retire-passkey` run at the same moment
|
|
345
|
+
as a renewal that adds a new passkey keeps both changes (a narrow
|
|
346
|
+
window remains between that reading and the renaming, since no lock is
|
|
347
|
+
held); a list that would hold no passkey is never written. At start-up,
|
|
348
|
+
only the permission last in force is taken without the checks: the
|
|
349
|
+
last permission line of `book.jsonl`, or, where a switch to a new book
|
|
350
|
+
was interrupted and there is no `book.jsonl`, the last permission line
|
|
351
|
+
of the last kept book. Any other permission put in `slip.json`, even one
|
|
352
|
+
that either book holds further up, goes through the checks, a retired
|
|
353
|
+
passkey's among them, and no line is added for it. What is left: a
|
|
354
|
+
program on this computer that puts back the permission last in force
|
|
355
|
+
(removing `book.jsonl` where it was last in force in the kept book) can
|
|
356
|
+
resume it, even if its passkey was retired afterwards, and so can one
|
|
357
|
+
that adds a permission's line at the end of either book. Retirement
|
|
358
|
+
stops new permissions; it does not stop a program that can change the
|
|
359
|
+
recorder's files. A file written by an earlier version holds
|
|
360
|
+
`passkeys` alone and is read as retiring none, so a recorder updated in
|
|
361
|
+
place starts on its folder as it is; a `retired` that is not a list of
|
|
362
|
+
thumbprints from `passkeys` stops the recorder, as a malformed
|
|
363
|
+
`passkeys` does. `status` names the retired passkeys (`retired`). Test:
|
|
364
|
+
`integrations/claude-code/test/retire.test.mjs`.
|
|
365
|
+
|
|
366
|
+
- **A workflow that publishes a release from the founder's signed tag**
|
|
367
|
+
(25 October 2026). `.github/workflows/publish.yml` runs only when a tag
|
|
368
|
+
such as `v0.7.0` is pushed. Before anything is published it checks that
|
|
369
|
+
the tag is signed by the release key in `.github/release-signers` and
|
|
370
|
+
by nothing else, that the tag, `package.json`, `python/pyproject.toml`
|
|
371
|
+
and the newest heading of this change log name the same version, and
|
|
372
|
+
that the JavaScript and Python libraries pass their tests on the tagged
|
|
373
|
+
commit, with the tests of every framework package against the library
|
|
374
|
+
from that commit. It then publishes the Python packages and stages the
|
|
375
|
+
npm package, through each registry's trusted publishing, with no stored
|
|
376
|
+
password or token, and makes the GitHub release with the checking page
|
|
377
|
+
as one file and its SHA-256. The npm version is staged
|
|
378
|
+
(`npm stage publish`) and becomes public only when the author approves
|
|
379
|
+
it on npmjs.com; the run shows the stage ID and the package's
|
|
380
|
+
integrity value, so that the author approves that one stage, and the
|
|
381
|
+
release notes say, for each registry, what the run sent there. The
|
|
382
|
+
Python files are checked
|
|
383
|
+
against the SHA-256 recorded when they were built before they are
|
|
384
|
+
uploaded. A version already on a registry is not published again, so a
|
|
385
|
+
framework package is published only when its own version is raised;
|
|
386
|
+
where its version is already published and its code differs, the run
|
|
387
|
+
stops before anything is published. The scripts it runs are in
|
|
388
|
+
`tools/release/`. Nothing in the npm package or the Python packages
|
|
389
|
+
changes. Version 0.6.0 and earlier were published by hand.
|
|
390
|
+
- `tools/make-single-page.mjs` refuses an option, or more than one
|
|
391
|
+
argument, and writes nothing: `--out x` used to write a file named
|
|
392
|
+
`--out`. It still takes one optional argument, the file to write.
|
|
393
|
+
|
|
394
|
+
- **A conformance kit**, in `conformance/` (not published): another
|
|
395
|
+
implementation of the format, in any language, is tested against the
|
|
396
|
+
shared test files through a small program of its own (an adapter),
|
|
397
|
+
which the kit hands one case at a time as a line of JSON and whose
|
|
398
|
+
answer it compares. By default it compares the codes and the shape of
|
|
399
|
+
each answer, not the words meant for people, as `test-vectors/README.md`
|
|
400
|
+
sets out; `--words` compares every value. It ends with code 0 only where
|
|
401
|
+
every case compared matched. An adapter for Provared's own JavaScript
|
|
402
|
+
library passes all 14,742 cases word for word. Passing it shows
|
|
403
|
+
agreement on these cases only; it is not a certificate. Its tests run in
|
|
404
|
+
the test workflow beside the sealer's.
|
|
405
|
+
|
|
406
|
+
- **The conformance kit, two changes.** By default it no longer counts the
|
|
407
|
+
sentences in the lists named `notes` and `limits`: another implementation
|
|
408
|
+
may say the same thing in more or fewer sentences, so only the codes,
|
|
409
|
+
the items that are not texts and the shape of each answer are compared;
|
|
410
|
+
`--words` still compares every word and every sentence. And a second,
|
|
411
|
+
example adapter, `conformance/adapters/provared-public.mjs`, is written
|
|
412
|
+
only on the functions the npm package offers (`provared` and
|
|
413
|
+
`provared/check`), and can be pointed at an installed copy of the
|
|
414
|
+
package with `--package`: it answers 3,099 of the 14,742 cases, all of
|
|
415
|
+
them matching word for word, and skips the other 11,643, which test
|
|
416
|
+
parts of the library that are not offered to other programs, saying why
|
|
417
|
+
for each.
|
|
418
|
+
|
|
419
|
+
- **The conformance kit, after an independent review.** By default, the
|
|
420
|
+
findings of an entry (`problems`, `breaches`, `disclosureProblems`) are
|
|
421
|
+
compared in any order, as the format description says their order is
|
|
422
|
+
not part of the format; and an entry's own `problems` may hold only
|
|
423
|
+
some of those stored, at least one, since a checker may stop at the
|
|
424
|
+
first way an entry fails its own check (where it stopped before the
|
|
425
|
+
countersignature, "unchecked" is taken in its place). Breaches are
|
|
426
|
+
still compared in full. `--words` keeps the stored order. The verdict
|
|
427
|
+
now says how many cases were skipped and not tested, the functions
|
|
428
|
+
skipped entirely are listed, and `--no-skip` counts any skip as a
|
|
429
|
+
failure. A reply holding both an answer and a skip is a fault of the
|
|
430
|
+
adapter. The example adapter reports an error it does not expect as a
|
|
431
|
+
crash, where it gave the code `check-failed`. The guide gains a table
|
|
432
|
+
of every function the shared test files name: its section of the
|
|
433
|
+
format description, whether another implementation may skip it, where
|
|
434
|
+
its answer is declared, and how its cases are written; and it lists
|
|
435
|
+
the stored values that are particular to Provared's own library.
|
|
436
|
+
|
|
437
|
+
- **An OpenTelemetry bridge**, `provared-opentelemetry` 0.1.0, in
|
|
438
|
+
`integrations/opentelemetry/` (not published). It puts each tool call's
|
|
439
|
+
receipt on the trace span that observability tools already show, in one
|
|
440
|
+
call: `traceTools` (as `recordTools`) or `traceAct` (as the stub
|
|
441
|
+
writer's `act`). The span
|
|
442
|
+
gets `provared.outcome`, `provared.action`, `provared.stub.fingerprint`,
|
|
443
|
+
`provared.stub.seq`, `provared.stub.countersigned`,
|
|
444
|
+
`provared.stub.trace_bound`, `provared.slip.fingerprint` and, for a call
|
|
445
|
+
not taken, `provared.check.codes` and `provared.denial.fingerprint`; a span
|
|
446
|
+
the bridge starts is an `execute_tool` span as OpenTelemetry's
|
|
447
|
+
conventions for generative AI describe it. The receipt names the
|
|
448
|
+
fingerprint of the span's trace id as `provared.trace-id` only for a tool
|
|
449
|
+
that the caller says sends the trace out with its call, in a
|
|
450
|
+
`traceparent` header (`sendsTrace: true`; off by default), as section
|
|
451
|
+
27 of the format description defines the name; the bridge cannot see
|
|
452
|
+
whether the tool really does. The model's identifier for the call goes
|
|
453
|
+
on the span as `gen_ai.tool.call.id`, never into the receipt. Only
|
|
454
|
+
fingerprints, names and codes reach a span: never arguments, amounts,
|
|
455
|
+
documents, keys, the words of a finding or an error's message. A span is
|
|
456
|
+
the agent's side's own word; the receipt and the checker are the
|
|
457
|
+
evidence. `@opentelemetry/api` and `provared` are peer dependencies; the
|
|
458
|
+
library itself still has none. An error that the tool, or one of its own
|
|
459
|
+
functions, throws is the tool's error, even a `NotTaken` from another
|
|
460
|
+
stub writer. `traceAct` adds nothing to a request that comes with an
|
|
461
|
+
approval made beforehand, or whose `details` is not a list. Its tests run
|
|
462
|
+
in a job of their own in the test workflow, at the locked versions and
|
|
463
|
+
again at the newest releases of OpenTelemetry.
|
|
464
|
+
- **An example for a JavaScript agent framework** in
|
|
465
|
+
`examples/frameworks/` (not in the npm package): an agent built with the
|
|
466
|
+
Vercel AI SDK (`ai` 7.0.153), with its tools put behind the stub writer
|
|
467
|
+
with `recordTools`. One order within the slip is run and receipted; one
|
|
468
|
+
over its limit is not run, leaves no stub, and the agent is told why; the
|
|
469
|
+
checker finds the book intact and within its slip, and the output says
|
|
470
|
+
that the slip was signed by a development stand-in, not a person. It
|
|
471
|
+
uses the SDK's own mock model and passes `telemetry: { isEnabled: false }`.
|
|
472
|
+
It runs with a guard (`_offline.mjs`) open from before any module is
|
|
473
|
+
loaded until the program ends, which replaces with refusing functions
|
|
474
|
+
Node.js's socket connections, `dgram` sends, `dns` look-ups (both
|
|
475
|
+
`Resolver` classes included), the functions that start a program (`child_process`, `ChildProcess.prototype.spawn` and, where it exists, `process.execve`),
|
|
476
|
+
`worker_threads.Worker`, `fetch`, `WebSocket`, and `process.binding`
|
|
477
|
+
for the bindings beneath them; the tests check that nothing was refused.
|
|
478
|
+
Its own `package.json` and lock file pin every version, so the library
|
|
479
|
+
keeps no dependencies. A new job in the test workflow runs it. In
|
|
480
|
+
`docs/guide.md`, the links to the examples and to the Python version's
|
|
481
|
+
examples now give their full addresses on GitHub, since `examples/` and
|
|
482
|
+
`python/` are not in the npm package.
|
|
483
|
+
|
|
484
|
+
- **A "try it" page** at prova.red/try/ (`site/try/`, live since 25 October 2026).
|
|
485
|
+
One button. Where the browser has Ed25519 and ML-DSA-87 built in (Chrome,
|
|
486
|
+
Edge and Brave 155, tried on 13 October 2026), the page makes a new
|
|
487
|
+
record with the development stand-in for a passkey, under a limit on
|
|
488
|
+
orders in GBP that the visitor may change, writes a receipt for each of
|
|
489
|
+
four orders (on purpose, even where the check before acting says no) and
|
|
490
|
+
checks the record. Elsewhere it reads the sample record and checks it one
|
|
491
|
+
entry at a time, and says why. The words about the record, what the page
|
|
492
|
+
does not do and what the check trusts are those of the path shown.
|
|
493
|
+
Nothing is sent, nothing is stored, and the record made has no seal; the
|
|
494
|
+
card of its top fingerprint is then headed "The top fingerprint". Linked
|
|
495
|
+
from the home page and the site map; its logic (`site/try/story.js`) is
|
|
496
|
+
tested in `test/try-page.test.mjs`. The site's style sheet gains the
|
|
497
|
+
checking page's rules for the checker's answer, scoped to the new page.
|
|
498
|
+
|
|
499
|
+
- **Signing moved to https://sign.prova.red** (25 October 2026). A browser
|
|
500
|
+
lets a passkey sign only for the website name it was made for and the
|
|
501
|
+
addresses under it, so a passkey made at prova.red/sign/ could be asked
|
|
502
|
+
to sign by any other page under prova.red (the list of threats, section
|
|
503
|
+
3). The signing page is now served at sign.prova.red from its own
|
|
504
|
+
repository, https://github.com/provared/sign, which holds only that page
|
|
505
|
+
and the library's files, each a byte-for-byte copy of the files at tag
|
|
506
|
+
v0.6.0, and serves nothing else. prova.red/sign/ now says that
|
|
507
|
+
signing has moved and offers no signing (`site/sign/index.html`; the
|
|
508
|
+
publishing workflow no longer copies the signing page there). The
|
|
509
|
+
gateway's README points to the new address. (The recorder beside Claude
|
|
510
|
+
Code, the Hermes plugin and the signing page itself named the old
|
|
511
|
+
address until the change above; their READMEs said how to open such a
|
|
512
|
+
link at the new address.)
|
|
513
|
+
- The test workflow is no longer started by a push that changes only the
|
|
514
|
+
record page and the sitemap.
|
|
515
|
+
|
|
516
|
+
- On the "try it" page the checker's answer entry by entry is folded away behind "Show every entry", which opens in place; the story, the summary and what the check does not show stay open.
|
|
3
517
|
## 0.6.0 (25 October 2026)
|
|
4
518
|
|
|
5
519
|
Version 0.6.0 adds three kinds of record and the services that use them: the denial (the agent's own side states that an action it was asked for was not taken), seals held elsewhere and handed over beside a book, and witness statements beside a seal, with consistency proofs; and, beside the library, a central sealer, a gateway that writes the receipts for agents that cannot be wrapped, and a recorder beside Claude Code that numbers its books and has them witnessed (none of these three programs is published). What a reader of a book made by 0.5.0 or earlier must know: every such book reads as it did, apart from the cases below; but a checker of 0.5.0 or earlier refuses a book that holds a denial, a seal that gives `book`, or a seal that holds `witnesses`, so a book made with these needs a checker of 0.6.0. The fingerprint of a tool call's arguments is now named `provared.arguments` by default, where it was `arguments`; the fingerprint itself is the same. And a checker of 0.6.0 refuses some time-stamps and keys that 0.5.0 accepted, each listed below: a time-stamp that is not strict DER, that names its signing certificate twice in the same form, or that is signed with Ed25519 under a method other than SHA-512 or with parameters; an ES256 key that is not a point on its curve; and an Ed25519 signature whose number S is not below the order L. A passkey and a time-stamp service that follow their standards produce none of these; the sample record gets the same findings and the same exit code from the command-line checker of both versions, and 0.6.0 adds only a line on witnesses and, with `--json`, the empty members for denials and witnesses.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# <img src="https://prova.red/favicon.svg" alt="" width="36"> Provared
|
|
2
2
|
|
|
3
|
+
[](https://scorecard.dev/viewer/?uri=github.com/provared/provared)
|
|
4
|
+
|
|
3
5
|
Proof of what an AI agent was allowed to do, and what it then did.
|
|
4
6
|
|
|
5
7
|
A person signs the permission with a passkey. The agent signs a receipt
|
|
@@ -88,6 +90,8 @@ agent = create_agent(model, tools=[order_supplies, send_message],
|
|
|
88
90
|
|
|
89
91
|
The package is listed among the middleware in
|
|
90
92
|
[LangChain's own documentation](https://docs.langchain.com/oss/python/integrations/middleware).
|
|
93
|
+
For JavaScript agents, [examples/frameworks/](examples/frameworks/) shows the
|
|
94
|
+
same with the Vercel AI SDK, through `recordTools`.
|
|
91
95
|
|
|
92
96
|
## The four parts
|
|
93
97
|
|
|
@@ -139,6 +143,14 @@ each part protects against, is in
|
|
|
139
143
|
- **[The Python version](python/README.md)**, which gives the same
|
|
140
144
|
answers, with examples for three agent frameworks and packages for
|
|
141
145
|
two of them.
|
|
146
|
+
- **Two programs beside an agent, each on npm with its own README:**
|
|
147
|
+
[the recorder beside Claude Code](integrations/claude-code/README.md)
|
|
148
|
+
(`npm install -g provared-claude-code`, also installable as a Claude
|
|
149
|
+
Code plugin), which checks and receipts every tool call Claude Code
|
|
150
|
+
makes; and [the executor for MCP tool servers](mcp-executor/README.md)
|
|
151
|
+
(`npm install -g provared-mcp-executor`), which stands between an
|
|
152
|
+
agent and its tool servers, holds their credentials, and countersigns
|
|
153
|
+
each receipt with keys of its own.
|
|
142
154
|
- **[The pages at prova.red](https://prova.red/)**: the checker's answer
|
|
143
155
|
for the sample record, a worked example for developers, and the same
|
|
144
156
|
in plain words for a reader who does not write code.
|
|
@@ -159,7 +171,7 @@ each part protects against, is in
|
|
|
159
171
|
|
|
160
172
|
## Status
|
|
161
173
|
|
|
162
|
-
**Version 0.
|
|
174
|
+
**Version 0.7.0, a draft.** Published as it is, by one author in their
|
|
163
175
|
own time, built with AI tools since 3 October 2026; fixes as time
|
|
164
176
|
allows. The code has been through more than 80 rounds of review by AI reviewers,
|
|
165
177
|
each reviewer a fresh session given the code, the documents and a brief
|