provared 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (47) hide show
  1. {provared-0.2.0 → provared-0.3.0}/PKG-INFO +47 -4
  2. provared-0.2.0/src/provared.egg-info/PKG-INFO → provared-0.3.0/README.md +198 -176
  3. {provared-0.2.0 → provared-0.3.0}/pyproject.toml +4 -2
  4. provared-0.3.0/src/provared/dev.py +133 -0
  5. provared-0.2.0/README.md → provared-0.3.0/src/provared.egg-info/PKG-INFO +219 -157
  6. {provared-0.2.0 → provared-0.3.0}/src/provared.egg-info/SOURCES.txt +2 -0
  7. provared-0.3.0/tests/test_dev.py +97 -0
  8. {provared-0.2.0 → provared-0.3.0}/LICENSE +0 -0
  9. {provared-0.2.0 → provared-0.3.0}/LICENSE-CC-BY-4.0.txt +0 -0
  10. {provared-0.2.0 → provared-0.3.0}/setup.cfg +0 -0
  11. {provared-0.2.0 → provared-0.3.0}/src/provared/__init__.py +0 -0
  12. {provared-0.2.0 → provared-0.3.0}/src/provared/_js.py +0 -0
  13. {provared-0.2.0 → provared-0.3.0}/src/provared/actions.py +0 -0
  14. {provared-0.2.0 → provared-0.3.0}/src/provared/approval.py +0 -0
  15. {provared-0.2.0 → provared-0.3.0}/src/provared/blockstamp.py +0 -0
  16. {provared-0.2.0 → provared-0.3.0}/src/provared/book.py +0 -0
  17. {provared-0.2.0 → provared-0.3.0}/src/provared/check.py +0 -0
  18. {provared-0.2.0 → provared-0.3.0}/src/provared/compare.py +0 -0
  19. {provared-0.2.0 → provared-0.3.0}/src/provared/cover.py +0 -0
  20. {provared-0.2.0 → provared-0.3.0}/src/provared/der.py +0 -0
  21. {provared-0.2.0 → provared-0.3.0}/src/provared/encoding.py +0 -0
  22. {provared-0.2.0 → provared-0.3.0}/src/provared/fields.py +0 -0
  23. {provared-0.2.0 → provared-0.3.0}/src/provared/guard.py +0 -0
  24. {provared-0.2.0 → provared-0.3.0}/src/provared/headers.py +0 -0
  25. {provared-0.2.0 → provared-0.3.0}/src/provared/jws.py +0 -0
  26. {provared-0.2.0 → provared-0.3.0}/src/provared/keys.py +0 -0
  27. {provared-0.2.0 → provared-0.3.0}/src/provared/pass_.py +0 -0
  28. {provared-0.2.0 → provared-0.3.0}/src/provared/recorder.py +0 -0
  29. {provared-0.2.0 → provared-0.3.0}/src/provared/seal.py +0 -0
  30. {provared-0.2.0 → provared-0.3.0}/src/provared/service.py +0 -0
  31. {provared-0.2.0 → provared-0.3.0}/src/provared/signatures.py +0 -0
  32. {provared-0.2.0 → provared-0.3.0}/src/provared/slip.py +0 -0
  33. {provared-0.2.0 → provared-0.3.0}/src/provared/standing.py +0 -0
  34. {provared-0.2.0 → provared-0.3.0}/src/provared/stub.py +0 -0
  35. {provared-0.2.0 → provared-0.3.0}/src/provared/timestamp.py +0 -0
  36. {provared-0.2.0 → provared-0.3.0}/src/provared/tools.py +0 -0
  37. {provared-0.2.0 → provared-0.3.0}/src/provared/tree.py +0 -0
  38. {provared-0.2.0 → provared-0.3.0}/src/provared/webauthn.py +0 -0
  39. {provared-0.2.0 → provared-0.3.0}/src/provared.egg-info/dependency_links.txt +0 -0
  40. {provared-0.2.0 → provared-0.3.0}/src/provared.egg-info/requires.txt +0 -0
  41. {provared-0.2.0 → provared-0.3.0}/src/provared.egg-info/top_level.txt +0 -0
  42. {provared-0.2.0 → provared-0.3.0}/tests/test_record_function.py +0 -0
  43. {provared-0.2.0 → provared-0.3.0}/tests/test_recorder.py +0 -0
  44. {provared-0.2.0 → provared-0.3.0}/tests/test_records.py +0 -0
  45. {provared-0.2.0 → provared-0.3.0}/tests/test_review2.py +0 -0
  46. {provared-0.2.0 → provared-0.3.0}/tests/test_tree.py +0 -0
  47. {provared-0.2.0 → provared-0.3.0}/tests/test_vectors.py +0 -0
@@ -1,12 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: provared
3
- Version: 0.2.0
4
- Summary: Proof of what an AI agent was allowed to do, and what it then did. Evidence, not a verdict.
3
+ Version: 0.3.0
4
+ Summary: 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 checker. Evidence, not a verdict.
5
5
  Author: Pavel Izmaylov
6
6
  License-Expression: Apache-2.0 AND CC-BY-4.0
7
+ Project-URL: Homepage, https://prova.red
7
8
  Project-URL: Source, https://github.com/provared/provared
8
9
  Project-URL: Documentation, https://github.com/provared/provared/tree/main/python
9
10
  Project-URL: Issues, https://github.com/provared/provared/issues
11
+ Keywords: ai-agent,agent,audit-trail,permission,authorization,receipt,tamper-evident,passkey,webauthn,jws,ed25519,ml-dsa,post-quantum,evidence,langchain
10
12
  Classifier: Development Status :: 3 - Alpha
11
13
  Classifier: Programming Language :: Python :: 3
12
14
  Classifier: Topic :: Security :: Cryptography
@@ -32,7 +34,7 @@ particular to Python.
32
34
 
33
35
  ## Status
34
36
 
35
- **Version 0.2.0, a draft**, the same version as the JavaScript library.
37
+ **Version 0.3.0, a draft**, the same version as the JavaScript library.
36
38
  Published as it is, by one author in their own time; fixes as time
37
39
  allows. It gives the same answer, case for case, as the shared test
38
40
  files in [`test-vectors/`](https://github.com/provared/provared/tree/main/test-vectors) show: the same result, the
@@ -58,6 +60,44 @@ You need Python 3.11 or later. The one dependency is `cryptography`,
58
60
  version 48 or later: Python has no signatures built in, and version 48
59
61
  is the first whose ready-built packages hold ML-DSA (FIPS 204).
60
62
 
63
+ ## A first record
64
+
65
+ The permission is signed with a **development stand-in for a passkey**
66
+ (`provared.dev`): a key made in memory, which no person confirmed. Every
67
+ permission it signs says so in its issuer's name, and a checker shows
68
+ that name. A real permission is signed by a person, in a browser, with a
69
+ real passkey (the browser part of the JavaScript library, and the
70
+ checking page).
71
+
72
+ ```python
73
+ from provared import check_book
74
+ from provared.dev import development_recorder
75
+
76
+ SEND = 'provared.message.send'
77
+
78
+ # A permission: the agent may send messages, at most two.
79
+ made = development_recorder({'actions': [SEND], 'limits': [{'action': SEND, 'count': 2}]})
80
+
81
+ # Each action is asked for first, taken only if the permission allows it,
82
+ # and receipted after. The third message is outside it, so it is not sent.
83
+ for i in range(1, 4):
84
+ outcome = made.writer.act({'action': SEND}, lambda i=i: print(f'message {i} sent'))
85
+ if not outcome['done']:
86
+ print(f"message {i} not sent: {outcome['answer']['breaches'][0]['message']}")
87
+
88
+ book = made.writer.book() # the record, one entry to a line
89
+ summary = check_book(book, issuer_keys=made.issuer_keys)['summary']
90
+ print(summary['intact'], summary['withinSlips']) # True True
91
+ ```
92
+
93
+ The whole file is [`examples/first_record.py`](https://github.com/provared/provared/blob/main/python/examples/first_record.py).
94
+ `development_slip(fields)` gives the signed slip, its fingerprint, a
95
+ book holding it, the stand-in's thumbprint (`issuer_keys`) and the
96
+ agent's keys without opening the writer; `development_passkey()` gives
97
+ the stand-in alone, whose `sign(challenge)` signs a prepared slip,
98
+ approval or cancellation. The issuer's name always ends with
99
+ " (development)", whatever name is given.
100
+
61
101
  ## Use
62
102
 
63
103
  ```python
@@ -143,7 +183,10 @@ place into a `functools.partial` is no longer a parameter, so it is not
143
183
  in the record, and nor is any change that a decorator inside makes. The
144
184
  folder
145
185
  [`examples/`](https://github.com/provared/provared/tree/main/python/examples)
146
- shows this with three widely used agent frameworks. `key_set_from_seeds` and `key_set_seeds` keep an
186
+ shows this with three widely used agent frameworks, and the folder
187
+ [`integrations/`](https://github.com/provared/provared/tree/main/python/integrations)
188
+ holds small packages for two of them, each of which puts every tool call
189
+ of an agent behind the writer in one line. `key_set_from_seeds` and `key_set_seeds` keep an
147
190
  agent's keys between runs as two 32-byte seeds.
148
191
 
149
192
  Names follow Python's way (`check_book`, `write_stub`). Options may be
@@ -1,176 +1,198 @@
1
- Metadata-Version: 2.4
2
- Name: provared
3
- Version: 0.2.0
4
- Summary: Proof of what an AI agent was allowed to do, and what it then did. Evidence, not a verdict.
5
- Author: Pavel Izmaylov
6
- License-Expression: Apache-2.0 AND CC-BY-4.0
7
- Project-URL: Source, https://github.com/provared/provared
8
- Project-URL: Documentation, https://github.com/provared/provared/tree/main/python
9
- Project-URL: Issues, https://github.com/provared/provared/issues
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Topic :: Security :: Cryptography
13
- Requires-Python: >=3.11
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- License-File: LICENSE-CC-BY-4.0.txt
17
- Requires-Dist: cryptography>=48
18
- Dynamic: license-file
19
-
20
- # Provared for Python
21
-
22
- Proof of what an AI agent was allowed to do, and what it then did.
23
-
24
- *Evidence, not a verdict.*
25
-
26
- This is the Python version of Provared. The format, the reasons for it
27
- and what a record does and does not prove are described in the
28
- repository's own [README](https://github.com/provared/provared/blob/main/README.md), the
29
- [format description](https://github.com/provared/provared/blob/main/spec/provared-format.md) and the
30
- [list of threats](https://github.com/provared/provared/blob/main/docs/threat-model.md). This page says only what is
31
- particular to Python.
32
-
33
- ## Status
34
-
35
- **Version 0.2.0, a draft**, the same version as the JavaScript library.
36
- Published as it is, by one author in their own time; fixes as time
37
- allows. It gives the same answer, case for case, as the shared test
38
- files in [`test-vectors/`](https://github.com/provared/provared/tree/main/test-vectors) show: the same result, the
39
- same codes and the same words. Every shared file passes in both
40
- languages.
41
-
42
- One difference is stated, not hidden. The one package this version
43
- needs, `cryptography`, does not yet have SLH-DSA (FIPS 205), the method
44
- of a seal's third signature. So a seal's third signature is reported as
45
- "not checked on this device", and never as a pass, as the checking page
46
- in a browser reports it. Seals are checked in full by the JavaScript
47
- command-line checker.
48
-
49
- ## Install
50
-
51
- ```
52
- pip install provared
53
- ```
54
-
55
- Or, from a copy of the repository, `pip install ./python`.
56
-
57
- You need Python 3.11 or later. The one dependency is `cryptography`,
58
- version 48 or later: Python has no signatures built in, and version 48
59
- is the first whose ready-built packages hold ML-DSA (FIPS 204).
60
-
61
- ## Use
62
-
63
- ```python
64
- from provared import check_book
65
-
66
- with open('record.jsonl', encoding='utf-8') as f:
67
- result = check_book(f.read(), issuer_keys=['<the thumbprint of the passkey you trust>'])
68
-
69
- summary = result['summary']
70
- print(summary['intact'], summary['withinSlips'], summary['firstBreach'])
71
- ```
72
-
73
- The answer is the same JSON as the JavaScript library gives (see "What
74
- the checker returns" in the repository's README).
75
-
76
- Before an agent acts:
77
-
78
- ```python
79
- from provared import check_before
80
-
81
- answer = check_before(book_text, {
82
- 'slip': slip_fingerprint,
83
- 'action': 'supplies.order',
84
- 'amount': {'unit': 'GBP', 'value': 20},
85
- 'with': 'supplier',
86
- }, issuer_keys=[passkey_thumbprint])
87
-
88
- if not answer['allowed']:
89
- print(answer['problems'], answer['breaches'])
90
- ```
91
-
92
- Beside an agent, the stub writer asks the check before acting, takes the
93
- action only if the answer is yes, and writes the stub (see "Beside an
94
- agent" in the repository's README):
95
-
96
- ```python
97
- from provared import open_recorder
98
-
99
- writer = open_recorder(book_text, slip_fingerprint, private_keys, issuer_keys=[passkey_thumbprint])
100
- result = writer.act(
101
- {'action': 'supplies.order', 'amount': {'unit': 'GBP', 'value': 20}, 'with': 'supplier'},
102
- lambda: place_the_order(),
103
- )
104
- book_text, held = writer.snapshot() # keep both together, with the book
105
- ```
106
-
107
- Every call is an ordinary function. A writer's calls wait for one
108
- another, also across threads. The other side is asked in a thread of
109
- its own and given 30 seconds. An asynchronous agent calls the writer
110
- through `asyncio.to_thread`. A call to the writer from inside an action
111
- it is taking is refused at once, from the same thread or from a thread
112
- that carries the action's context; from a new thread that the action
113
- starts without it, the call waits until the action ends, so an action
114
- must not wait for such a call. `record_tools` puts an agent's tools
115
- behind the writer. `record_function` puts one ordinary function behind
116
- it and keeps the function's name, parameters and description, so that an
117
- agent framework describes it to its model as it would the function:
118
-
119
- ```python
120
- from provared import record_function
121
-
122
- def place_order(item: str, value: int) -> str:
123
- """Order supplies from the supplier."""
124
- ...
125
-
126
- place_order = record_function(writer, place_order, 'supplies.order', with_='supplier',
127
- amount=lambda args: {'unit': 'GBP', 'value': args['value']})
128
- ```
129
-
130
- A call that the slip does not allow raises `NotTaken`, and the function
131
- is not run. The arguments must be plain data (text, numbers, lists,
132
- dicts with text for names, True, False, None). A function with a
133
- parameter whose type can never be plain data (a class of its own, such
134
- as a model or a date, a tuple or a set) is refused when it is recorded,
135
- and so is a generator function. A whole number that JavaScript cannot
136
- hold exactly (beyond 2^53 - 1, unless a float holds it exactly) is
137
- refused, since two such numbers could share one fingerprint. The
138
- function is run with a copy of the arguments: a change it makes to a
139
- list or a dict it was handed does not reach the caller, and a default
140
- that is a list or a dict is a fresh copy at each call. The record holds the
141
- arguments the recorded function is called with: an argument bound in
142
- place into a `functools.partial` is no longer a parameter, so it is not
143
- in the record, and nor is any change that a decorator inside makes. The
144
- folder
145
- [`examples/`](https://github.com/provared/provared/tree/main/python/examples)
146
- shows this with three widely used agent frameworks. `key_set_from_seeds` and `key_set_seeds` keep an
147
- agent's keys between runs as two 32-byte seeds.
148
-
149
- Names follow Python's way (`check_book`, `write_stub`). Options may be
150
- given as keyword arguments (`issuer_keys`, `expected_root`,
151
- `stamp_services`, `seal_keys`, `vouchers`, `disclosures`,
152
- `cancellations`, `without_methods`), or as one dict with the names the
153
- JavaScript library uses (`issuerKeys` and so on). Records and answers
154
- are dicts and lists, with the same member names as in the format.
155
-
156
- `provared.check` holds the checker alone, for anyone who audits the
157
- checking.
158
-
159
- ## Tests
160
-
161
- From the `python` folder:
162
-
163
- ```
164
- python -m venv .venv
165
- .venv/bin/pip install "cryptography>=48" # on Windows: .venv\Scripts\pip
166
- PYTHONPATH=src .venv/bin/python -m unittest discover -s tests
167
- ```
168
-
169
- The tests read the shared test files in `../test-vectors/`, which the
170
- JavaScript library writes (`node tools/make-vectors.mjs`) and checks
171
- (`node --test test/vectors.test.mjs`).
172
-
173
- ## Licences
174
-
175
- As for the rest of the repository: the Apache License 2.0 for the code,
176
- and Creative Commons Attribution 4.0 for the documents.
1
+ # Provared for Python
2
+
3
+ Proof of what an AI agent was allowed to do, and what it then did.
4
+
5
+ *Evidence, not a verdict.*
6
+
7
+ This is the Python version of Provared. The format, the reasons for it
8
+ and what a record does and does not prove are described in the
9
+ repository's own [README](https://github.com/provared/provared/blob/main/README.md), the
10
+ [format description](https://github.com/provared/provared/blob/main/spec/provared-format.md) and the
11
+ [list of threats](https://github.com/provared/provared/blob/main/docs/threat-model.md). This page says only what is
12
+ particular to Python.
13
+
14
+ ## Status
15
+
16
+ **Version 0.3.0, a draft**, the same version as the JavaScript library.
17
+ Published as it is, by one author in their own time; fixes as time
18
+ allows. It gives the same answer, case for case, as the shared test
19
+ files in [`test-vectors/`](https://github.com/provared/provared/tree/main/test-vectors) show: the same result, the
20
+ same codes and the same words. Every shared file passes in both
21
+ languages.
22
+
23
+ One difference is stated, not hidden. The one package this version
24
+ needs, `cryptography`, does not yet have SLH-DSA (FIPS 205), the method
25
+ of a seal's third signature. So a seal's third signature is reported as
26
+ "not checked on this device", and never as a pass, as the checking page
27
+ in a browser reports it. Seals are checked in full by the JavaScript
28
+ command-line checker.
29
+
30
+ ## Install
31
+
32
+ ```
33
+ pip install provared
34
+ ```
35
+
36
+ Or, from a copy of the repository, `pip install ./python`.
37
+
38
+ You need Python 3.11 or later. The one dependency is `cryptography`,
39
+ version 48 or later: Python has no signatures built in, and version 48
40
+ is the first whose ready-built packages hold ML-DSA (FIPS 204).
41
+
42
+ ## A first record
43
+
44
+ The permission is signed with a **development stand-in for a passkey**
45
+ (`provared.dev`): a key made in memory, which no person confirmed. Every
46
+ permission it signs says so in its issuer's name, and a checker shows
47
+ that name. A real permission is signed by a person, in a browser, with a
48
+ real passkey (the browser part of the JavaScript library, and the
49
+ checking page).
50
+
51
+ ```python
52
+ from provared import check_book
53
+ from provared.dev import development_recorder
54
+
55
+ SEND = 'provared.message.send'
56
+
57
+ # A permission: the agent may send messages, at most two.
58
+ made = development_recorder({'actions': [SEND], 'limits': [{'action': SEND, 'count': 2}]})
59
+
60
+ # Each action is asked for first, taken only if the permission allows it,
61
+ # and receipted after. The third message is outside it, so it is not sent.
62
+ for i in range(1, 4):
63
+ outcome = made.writer.act({'action': SEND}, lambda i=i: print(f'message {i} sent'))
64
+ if not outcome['done']:
65
+ print(f"message {i} not sent: {outcome['answer']['breaches'][0]['message']}")
66
+
67
+ book = made.writer.book() # the record, one entry to a line
68
+ summary = check_book(book, issuer_keys=made.issuer_keys)['summary']
69
+ print(summary['intact'], summary['withinSlips']) # True True
70
+ ```
71
+
72
+ The whole file is [`examples/first_record.py`](https://github.com/provared/provared/blob/main/python/examples/first_record.py).
73
+ `development_slip(fields)` gives the signed slip, its fingerprint, a
74
+ book holding it, the stand-in's thumbprint (`issuer_keys`) and the
75
+ agent's keys without opening the writer; `development_passkey()` gives
76
+ the stand-in alone, whose `sign(challenge)` signs a prepared slip,
77
+ approval or cancellation. The issuer's name always ends with
78
+ " (development)", whatever name is given.
79
+
80
+ ## Use
81
+
82
+ ```python
83
+ from provared import check_book
84
+
85
+ with open('record.jsonl', encoding='utf-8') as f:
86
+ result = check_book(f.read(), issuer_keys=['<the thumbprint of the passkey you trust>'])
87
+
88
+ summary = result['summary']
89
+ print(summary['intact'], summary['withinSlips'], summary['firstBreach'])
90
+ ```
91
+
92
+ The answer is the same JSON as the JavaScript library gives (see "What
93
+ the checker returns" in the repository's README).
94
+
95
+ Before an agent acts:
96
+
97
+ ```python
98
+ from provared import check_before
99
+
100
+ answer = check_before(book_text, {
101
+ 'slip': slip_fingerprint,
102
+ 'action': 'supplies.order',
103
+ 'amount': {'unit': 'GBP', 'value': 20},
104
+ 'with': 'supplier',
105
+ }, issuer_keys=[passkey_thumbprint])
106
+
107
+ if not answer['allowed']:
108
+ print(answer['problems'], answer['breaches'])
109
+ ```
110
+
111
+ Beside an agent, the stub writer asks the check before acting, takes the
112
+ action only if the answer is yes, and writes the stub (see "Beside an
113
+ agent" in the repository's README):
114
+
115
+ ```python
116
+ from provared import open_recorder
117
+
118
+ writer = open_recorder(book_text, slip_fingerprint, private_keys, issuer_keys=[passkey_thumbprint])
119
+ result = writer.act(
120
+ {'action': 'supplies.order', 'amount': {'unit': 'GBP', 'value': 20}, 'with': 'supplier'},
121
+ lambda: place_the_order(),
122
+ )
123
+ book_text, held = writer.snapshot() # keep both together, with the book
124
+ ```
125
+
126
+ Every call is an ordinary function. A writer's calls wait for one
127
+ another, also across threads. The other side is asked in a thread of
128
+ its own and given 30 seconds. An asynchronous agent calls the writer
129
+ through `asyncio.to_thread`. A call to the writer from inside an action
130
+ it is taking is refused at once, from the same thread or from a thread
131
+ that carries the action's context; from a new thread that the action
132
+ starts without it, the call waits until the action ends, so an action
133
+ must not wait for such a call. `record_tools` puts an agent's tools
134
+ behind the writer. `record_function` puts one ordinary function behind
135
+ it and keeps the function's name, parameters and description, so that an
136
+ agent framework describes it to its model as it would the function:
137
+
138
+ ```python
139
+ from provared import record_function
140
+
141
+ def place_order(item: str, value: int) -> str:
142
+ """Order supplies from the supplier."""
143
+ ...
144
+
145
+ place_order = record_function(writer, place_order, 'supplies.order', with_='supplier',
146
+ amount=lambda args: {'unit': 'GBP', 'value': args['value']})
147
+ ```
148
+
149
+ A call that the slip does not allow raises `NotTaken`, and the function
150
+ is not run. The arguments must be plain data (text, numbers, lists,
151
+ dicts with text for names, True, False, None). A function with a
152
+ parameter whose type can never be plain data (a class of its own, such
153
+ as a model or a date, a tuple or a set) is refused when it is recorded,
154
+ and so is a generator function. A whole number that JavaScript cannot
155
+ hold exactly (beyond 2^53 - 1, unless a float holds it exactly) is
156
+ refused, since two such numbers could share one fingerprint. The
157
+ function is run with a copy of the arguments: a change it makes to a
158
+ list or a dict it was handed does not reach the caller, and a default
159
+ that is a list or a dict is a fresh copy at each call. The record holds the
160
+ arguments the recorded function is called with: an argument bound in
161
+ place into a `functools.partial` is no longer a parameter, so it is not
162
+ in the record, and nor is any change that a decorator inside makes. The
163
+ folder
164
+ [`examples/`](https://github.com/provared/provared/tree/main/python/examples)
165
+ shows this with three widely used agent frameworks, and the folder
166
+ [`integrations/`](https://github.com/provared/provared/tree/main/python/integrations)
167
+ holds small packages for two of them, each of which puts every tool call
168
+ of an agent behind the writer in one line. `key_set_from_seeds` and `key_set_seeds` keep an
169
+ agent's keys between runs as two 32-byte seeds.
170
+
171
+ Names follow Python's way (`check_book`, `write_stub`). Options may be
172
+ given as keyword arguments (`issuer_keys`, `expected_root`,
173
+ `stamp_services`, `seal_keys`, `vouchers`, `disclosures`,
174
+ `cancellations`, `without_methods`), or as one dict with the names the
175
+ JavaScript library uses (`issuerKeys` and so on). Records and answers
176
+ are dicts and lists, with the same member names as in the format.
177
+
178
+ `provared.check` holds the checker alone, for anyone who audits the
179
+ checking.
180
+
181
+ ## Tests
182
+
183
+ From the `python` folder:
184
+
185
+ ```
186
+ python -m venv .venv
187
+ .venv/bin/pip install "cryptography>=48" # on Windows: .venv\Scripts\pip
188
+ PYTHONPATH=src .venv/bin/python -m unittest discover -s tests
189
+ ```
190
+
191
+ The tests read the shared test files in `../test-vectors/`, which the
192
+ JavaScript library writes (`node tools/make-vectors.mjs`) and checks
193
+ (`node --test test/vectors.test.mjs`).
194
+
195
+ ## Licences
196
+
197
+ As for the rest of the repository: the Apache License 2.0 for the code,
198
+ and Creative Commons Attribution 4.0 for the documents.
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "provared"
7
- version = "0.2.0"
8
- description = "Proof of what an AI agent was allowed to do, and what it then did. Evidence, not a verdict."
7
+ version = "0.3.0"
8
+ 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 checker. Evidence, not a verdict."
9
+ keywords = ["ai-agent", "agent", "audit-trail", "permission", "authorization", "receipt", "tamper-evident", "passkey", "webauthn", "jws", "ed25519", "ml-dsa", "post-quantum", "evidence", "langchain"]
9
10
  readme = "README.md"
10
11
  authors = [{ name = "Pavel Izmaylov" }]
11
12
  license = "Apache-2.0 AND CC-BY-4.0"
@@ -21,6 +22,7 @@ classifiers = [
21
22
  ]
22
23
 
23
24
  [project.urls]
25
+ Homepage = "https://prova.red"
24
26
  Source = "https://github.com/provared/provared"
25
27
  Documentation = "https://github.com/provared/provared/tree/main/python"
26
28
  Issues = "https://github.com/provared/provared/issues"
@@ -0,0 +1,133 @@
1
+ # A development stand-in for a passkey, and a stub writer opened under a
2
+ # slip it signed: for trying the library in an evening, and for tests.
3
+ #
4
+ # It is NOT a passkey. Its private key is an ordinary key made in this
5
+ # process's memory, and no person confirmed anything. It returns the same
6
+ # three values a passkey returns (the authenticator data, the client data,
7
+ # the signature), in the forms the W3C Web Authentication standard sets
8
+ # out, so that every record it signs passes the checker exactly as a real
9
+ # one would. To keep such a record from being mistaken for a person's, the
10
+ # name of the issuer in every slip it signs ends with " (development)", and
11
+ # the checker shows that name beside the slip.
12
+ #
13
+ # A real slip is signed by the person, in a browser, with a real passkey:
14
+ # see the browser part of the JavaScript library and the checking page.
15
+ # Nothing here is part of the checker, and nothing here reaches the network.
16
+
17
+ import json
18
+
19
+ from cryptography.hazmat.primitives import hashes
20
+ from cryptography.hazmat.primitives.asymmetric import ec
21
+
22
+ from .book import write_book
23
+ from .encoding import fingerprint, format_time, from_base64url, now_ms, sha256, to_base64url, utf8
24
+ from .keys import thumbprint
25
+ from .recorder import open_recorder
26
+ from .signatures import generate_key_set
27
+ from .slip import assemble_slip, prepare_slip
28
+
29
+ __all__ = ['DEVELOPMENT_MARK', 'DevelopmentPasskey', 'DevelopmentSlip', 'development_passkey', 'development_slip', 'development_recorder']
30
+
31
+ DEVELOPMENT_MARK = '(development)'
32
+ """The words every development slip carries in its issuer's name."""
33
+
34
+ _RP_ID = 'localhost'
35
+ _ORIGIN = 'http://localhost'
36
+
37
+
38
+ class DevelopmentPasskey:
39
+ """A development stand-in for a passkey (ES256, the method most passkeys
40
+ use). It signs on this computer, for the page address http://localhost,
41
+ which the format allows for development only. It is not a passkey."""
42
+
43
+ def __init__(self):
44
+ self._private = ec.generate_private_key(ec.SECP256R1())
45
+ numbers = self._private.public_key().public_numbers()
46
+ self.key = {'alg': 'ES256', 'crv': 'P-256', 'kty': 'EC',
47
+ 'x': to_base64url(numbers.x.to_bytes(32, 'big')),
48
+ 'y': to_base64url(numbers.y.to_bytes(32, 'big'))}
49
+ self.rp_id = _RP_ID
50
+ self.origin = _ORIGIN
51
+
52
+ def sign(self, challenge):
53
+ """The three values a passkey returns, for a challenge from
54
+ prepare_slip, prepare_approval or prepare_cancellation: the
55
+ authenticator data, the client data and the signature."""
56
+ client_data_json = utf8(json.dumps({'type': 'webauthn.get', 'challenge': to_base64url(challenge),
57
+ 'origin': _ORIGIN, 'crossOrigin': False}, separators=(',', ':')))
58
+ # Flags 0x05: "user present" and "user verified". Nobody was: this is a stand-in.
59
+ authenticator_data = sha256(utf8(_RP_ID)) + bytes([0x05, 0, 0, 0, 1])
60
+ signature = self._private.sign(authenticator_data + sha256(client_data_json), ec.ECDSA(hashes.SHA256()))
61
+ return authenticator_data, client_data_json, signature
62
+
63
+
64
+ def development_passkey():
65
+ """A development stand-in for a passkey."""
66
+ return DevelopmentPasskey()
67
+
68
+
69
+ def _marked_name(name):
70
+ given = name.strip() if isinstance(name, str) and name.strip() else 'Development passkey'
71
+ return given if given.endswith(DEVELOPMENT_MARK) else f'{given} {DEVELOPMENT_MARK}'
72
+
73
+
74
+ class DevelopmentSlip:
75
+ """What development_slip hands back: the signed slip, its fingerprint, a
76
+ book holding the slip alone, the thumbprint of the stand-in passkey (to
77
+ hand to a checker as the passkey you trust), the agent's keys and the
78
+ stand-in passkey itself."""
79
+
80
+ def __init__(self, slip, slip_fingerprint, book, issuer_keys, agent_keys, agent_private_keys, passkey):
81
+ self.slip = slip
82
+ self.slip_fingerprint = slip_fingerprint
83
+ self.book = book
84
+ self.issuer_keys = issuer_keys
85
+ self.agent_keys = agent_keys
86
+ self.agent_private_keys = agent_private_keys
87
+ self.passkey = passkey
88
+ self.writer = None
89
+
90
+
91
+ def development_slip(fields=None, **named):
92
+ """A slip signed with a development stand-in for a passkey, and new keys
93
+ for the agent. Give the slip's members as for prepare_slip, as one dict
94
+ or as keyword arguments; "actions" is required. "issuer" and the agent's
95
+ keys are filled in, and the issuer's name is made to end with
96
+ " (development)". By default the slip runs from a minute ago for one
97
+ day, holds no limits, names no other party, and its purpose says that
98
+ it is for development."""
99
+ given = {} if fields is None else fields
100
+ if not isinstance(given, dict):
101
+ raise TypeError('development_slip: give the members of the slip as a dict, with at least "actions".')
102
+ given = {**given, **named}
103
+ passkey = DevelopmentPasskey()
104
+ agent_keys, agent_private_keys = generate_key_set()
105
+ now = now_ms()
106
+ issuer = given.get('issuer') if isinstance(given.get('issuer'), dict) else {}
107
+ agent = given.get('agent') if isinstance(given.get('agent'), dict) else {}
108
+ content = {
109
+ 'validFrom': format_time(now - 60_000),
110
+ 'validUntil': format_time(now + 24 * 3_600_000),
111
+ 'purpose': 'For development: a record made with a stand-in for a passkey, which no person signed.',
112
+ 'limits': [],
113
+ 'with': [],
114
+ **given,
115
+ 'issuer': {'name': _marked_name(issuer.get('name')), 'key': passkey.key, 'rpId': passkey.rp_id, 'origin': passkey.origin},
116
+ 'agent': {'name': agent['name'] if isinstance(agent.get('name'), str) else 'Development agent', **agent, 'keys': agent_keys},
117
+ }
118
+ prepared = prepare_slip(content)
119
+ slip = assemble_slip(prepared, *passkey.sign(prepared['challenge']))
120
+ slip_fingerprint = fingerprint(from_base64url(slip['payload']))
121
+ return DevelopmentSlip(slip, slip_fingerprint, write_book([{'slip': slip}]), [thumbprint(passkey.key)],
122
+ agent_keys, agent_private_keys, passkey)
123
+
124
+
125
+ def development_recorder(fields=None, more=None, **named):
126
+ """The stub writer, opened under a development slip, in one call: the
127
+ shortest way to a first record. The same as development_slip followed by
128
+ open_recorder; "more" (a dict) is passed to open_recorder: "options",
129
+ "now", "countersign_within". What it hands back has the writer as
130
+ ".writer", beside everything development_slip gives."""
131
+ made = development_slip(fields, **named)
132
+ made.writer = open_recorder(made.book, made.slip_fingerprint, made.agent_private_keys, made.issuer_keys, **(more or {}))
133
+ return made