provared 0.2.0__py3-none-any.whl
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.
- provared/__init__.py +60 -0
- provared/_js.py +450 -0
- provared/actions.py +84 -0
- provared/approval.py +108 -0
- provared/blockstamp.py +223 -0
- provared/book.py +2320 -0
- provared/check.py +21 -0
- provared/compare.py +266 -0
- provared/cover.py +187 -0
- provared/der.py +230 -0
- provared/encoding.py +278 -0
- provared/fields.py +95 -0
- provared/guard.py +177 -0
- provared/headers.py +507 -0
- provared/jws.py +164 -0
- provared/keys.py +134 -0
- provared/pass_.py +86 -0
- provared/recorder.py +1426 -0
- provared/seal.py +133 -0
- provared/service.py +138 -0
- provared/signatures.py +149 -0
- provared/slip.py +413 -0
- provared/standing.py +198 -0
- provared/stub.py +131 -0
- provared/timestamp.py +456 -0
- provared/tools.py +415 -0
- provared/tree.py +148 -0
- provared/webauthn.py +107 -0
- provared-0.2.0.dist-info/METADATA +176 -0
- provared-0.2.0.dist-info/RECORD +34 -0
- provared-0.2.0.dist-info/WHEEL +5 -0
- provared-0.2.0.dist-info/licenses/LICENSE +202 -0
- provared-0.2.0.dist-info/licenses/LICENSE-CC-BY-4.0.txt +396 -0
- provared-0.2.0.dist-info/top_level.txt +1 -0
provared/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Provared, draft version 0: proof of what an AI agent was allowed to do,
|
|
2
|
+
# and what it then did. Evidence, not a verdict.
|
|
3
|
+
#
|
|
4
|
+
# The Python version of the JavaScript library, built in stages. It gives
|
|
5
|
+
# the same answers, case for case, as the shared test files in
|
|
6
|
+
# test-vectors/ at the top of the repository show. One difference: the
|
|
7
|
+
# package "cryptography" has no SLH-DSA yet, so a seal's third signature is
|
|
8
|
+
# reported as not checked on this device, and never as a pass.
|
|
9
|
+
#
|
|
10
|
+
# Checking: check_book, check_show, check_slip
|
|
11
|
+
# open_checker (a checker that is carried on as the book grows)
|
|
12
|
+
# check_before (would this action be outside the slip?)
|
|
13
|
+
# Writing: prepare_slip + assemble_slip (the person, with a passkey),
|
|
14
|
+
# prepare_approval + assemble_approval (the person's yes to one action),
|
|
15
|
+
# write_stub (the agent), countersign, write_refusal, write_terms
|
|
16
|
+
# (the service), block_stamp_item (a proof from a public
|
|
17
|
+
# blockchain, to put beside a seal),
|
|
18
|
+
# prepare_cancellation + assemble_cancellation (the person ends a slip),
|
|
19
|
+
# write_acknowledgement (the agent's side says it was handed a cancellation),
|
|
20
|
+
# write_vouching, write_withdrawal (an organisation vouches for a name),
|
|
21
|
+
# write_pass (an agent hands part of its permission to a helper agent),
|
|
22
|
+
# write_book, make_show
|
|
23
|
+
# Beside an agent: open_recorder (the stub writer: ask first, act, write
|
|
24
|
+
# the receipt), record_tools (an agent's tools behind the stub
|
|
25
|
+
# writer), arguments_fingerprint, NotTaken
|
|
26
|
+
# Keys: generate_key_set, key_set_from_seeds, key_set_seeds, thumbprint,
|
|
27
|
+
# key_set_fingerprint
|
|
28
|
+
# Names: the shared list of actions, the kinds of action, the rules of conduct
|
|
29
|
+
|
|
30
|
+
from .actions import ACTION_KINDS, CONDUCT_RULES, RESERVED_PREFIX, SHARED_ACTIONS, action_kind
|
|
31
|
+
from .approval import assemble_approval, prepare_approval
|
|
32
|
+
from .blockstamp import block_fingerprint, block_stamp_item
|
|
33
|
+
from .book import LIMITS, check_book, check_show, entry_line, make_show, open_checker, write_book
|
|
34
|
+
from .compare import condition_words, limit_words, never_words
|
|
35
|
+
from .encoding import Refusal, fingerprint, format_time, from_base64url, random_id, sha256, to_base64url, utf8
|
|
36
|
+
from .guard import check_before
|
|
37
|
+
from .headers import MIN_BLOCKS_AFTER, blocks_in_chain, check_header_chain
|
|
38
|
+
from .keys import thumbprint
|
|
39
|
+
from .pass_ import write_pass
|
|
40
|
+
from .recorder import COUNTERSIGN_WITHIN_MS, Recorder, open_recorder
|
|
41
|
+
from .seal import key_set_fingerprint
|
|
42
|
+
from .service import REFUSAL_REASONS, write_refusal, write_terms
|
|
43
|
+
from .signatures import generate_key_set, key_set_from_seeds, key_set_seeds, method_available
|
|
44
|
+
from .slip import COVERABLE, assemble_slip, check_slip, prepare_slip
|
|
45
|
+
from .standing import VOUCHED_KINDS, assemble_cancellation, prepare_cancellation, write_acknowledgement, write_vouching, write_withdrawal
|
|
46
|
+
from .stub import countersign, write_stub
|
|
47
|
+
from .tools import NotTaken, arguments_fingerprint, record_function, record_tools
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
'MIN_BLOCKS_AFTER', 'blocks_in_chain', 'check_header_chain',
|
|
51
|
+
'COUNTERSIGN_WITHIN_MS', 'NotTaken', 'Recorder', 'arguments_fingerprint', 'key_set_from_seeds', 'key_set_seeds', 'open_recorder',
|
|
52
|
+
'record_function', 'record_tools',
|
|
53
|
+
'ACTION_KINDS', 'CONDUCT_RULES', 'COVERABLE', 'LIMITS', 'REFUSAL_REASONS', 'RESERVED_PREFIX', 'SHARED_ACTIONS', 'VOUCHED_KINDS',
|
|
54
|
+
'Refusal', 'action_kind', 'assemble_approval', 'assemble_cancellation', 'assemble_slip', 'block_fingerprint', 'block_stamp_item',
|
|
55
|
+
'check_before', 'check_book', 'check_show', 'check_slip', 'condition_words', 'countersign', 'entry_line', 'fingerprint',
|
|
56
|
+
'format_time', 'from_base64url', 'generate_key_set', 'key_set_fingerprint', 'limit_words', 'make_show', 'method_available',
|
|
57
|
+
'never_words', 'open_checker', 'prepare_approval', 'prepare_cancellation', 'prepare_slip', 'random_id', 'sha256', 'thumbprint',
|
|
58
|
+
'to_base64url', 'utf8', 'write_acknowledgement', 'write_book', 'write_pass', 'write_refusal', 'write_stub', 'write_terms',
|
|
59
|
+
'write_vouching', 'write_withdrawal',
|
|
60
|
+
]
|
provared/_js.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# How JavaScript reads and writes JSON and text, copied exactly, so that
|
|
2
|
+
# this library gives the same answer as the JavaScript one for every input.
|
|
3
|
+
#
|
|
4
|
+
# JavaScript strings are counted, sorted and compared in UTF-16 code units,
|
|
5
|
+
# and JSON.parse reads numbers as 64-bit floating-point values. Where Python
|
|
6
|
+
# does otherwise, the functions here do as JavaScript does.
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
MAX_SAFE_INTEGER = 2**53 - 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _NotJson(ValueError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _no_constant(name):
|
|
20
|
+
# JSON.parse refuses NaN, Infinity and -Infinity, which Python's reader accepts.
|
|
21
|
+
raise _NotJson(name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _number_int(text):
|
|
25
|
+
# JSON.parse gives -0 for "-0", and a floating-point value for a long
|
|
26
|
+
# number. Python gives 0 and an exact whole number.
|
|
27
|
+
if text == '-0':
|
|
28
|
+
return -0.0
|
|
29
|
+
if len(text) > 17:
|
|
30
|
+
return float(text)
|
|
31
|
+
number = int(text)
|
|
32
|
+
return number if abs(number) <= MAX_SAFE_INTEGER else float(text)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _number_float(text):
|
|
36
|
+
"""A number written with a point or an exponent. JavaScript has one kind
|
|
37
|
+
of number, so one that is whole (and no larger than 2^53 - 1) is held
|
|
38
|
+
as a whole number, as it is written back in JavaScript. -0 stays -0."""
|
|
39
|
+
value = float(text)
|
|
40
|
+
if value.is_integer() and abs(value) <= MAX_SAFE_INTEGER and not (value == 0 and text.lstrip().startswith('-')):
|
|
41
|
+
return int(value)
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse(text):
|
|
46
|
+
"""JSON.parse: the value, with numbers as JavaScript reads them.
|
|
47
|
+
|
|
48
|
+
Raises ValueError where JSON.parse throws.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(text, str):
|
|
51
|
+
raise _NotJson('not text')
|
|
52
|
+
try:
|
|
53
|
+
return json.loads(text, parse_int=_number_int, parse_float=_number_float, parse_constant=_no_constant)
|
|
54
|
+
except RecursionError:
|
|
55
|
+
# Python's reader gives up on deep nesting, where JSON.parse does not.
|
|
56
|
+
return _parse_deep(text)
|
|
57
|
+
except (ValueError, OverflowError) as e:
|
|
58
|
+
raise _NotJson(str(e)) from None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_WS = ' \t\n\r'
|
|
62
|
+
_NUMBER = re.compile(r'-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?')
|
|
63
|
+
_ESCAPES = {'"': '"', '\\': '\\', '/': '/', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'}
|
|
64
|
+
_HEX = re.compile(r'[0-9a-fA-F]{4}')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_deep(text):
|
|
68
|
+
"""JSON.parse without recursion, for deeply nested text."""
|
|
69
|
+
n = len(text)
|
|
70
|
+
i = 0
|
|
71
|
+
|
|
72
|
+
def skip(i):
|
|
73
|
+
while i < n and text[i] in _WS:
|
|
74
|
+
i += 1
|
|
75
|
+
return i
|
|
76
|
+
|
|
77
|
+
def read_string(i):
|
|
78
|
+
# text[i] is the opening quotation mark.
|
|
79
|
+
i += 1
|
|
80
|
+
out = []
|
|
81
|
+
start = i
|
|
82
|
+
while True:
|
|
83
|
+
if i >= n:
|
|
84
|
+
raise _NotJson('unterminated string')
|
|
85
|
+
c = text[i]
|
|
86
|
+
if c == '"':
|
|
87
|
+
out.append(text[start:i])
|
|
88
|
+
return ''.join(out).encode('utf-16-le', 'surrogatepass').decode('utf-16-le', 'surrogatepass'), i + 1
|
|
89
|
+
if c == '\\':
|
|
90
|
+
out.append(text[start:i])
|
|
91
|
+
if i + 1 >= n:
|
|
92
|
+
raise _NotJson('bad escape')
|
|
93
|
+
e = text[i + 1]
|
|
94
|
+
if e == 'u':
|
|
95
|
+
h = text[i + 2:i + 6]
|
|
96
|
+
if not _HEX.fullmatch(h):
|
|
97
|
+
raise _NotJson('bad escape')
|
|
98
|
+
out.append(chr(int(h, 16)))
|
|
99
|
+
i += 6
|
|
100
|
+
elif e in _ESCAPES:
|
|
101
|
+
out.append(_ESCAPES[e])
|
|
102
|
+
i += 2
|
|
103
|
+
else:
|
|
104
|
+
raise _NotJson('bad escape')
|
|
105
|
+
start = i
|
|
106
|
+
continue
|
|
107
|
+
if ord(c) < 0x20:
|
|
108
|
+
raise _NotJson('control character in string')
|
|
109
|
+
i += 1
|
|
110
|
+
|
|
111
|
+
# A stack of containers being filled: [container, pending member name].
|
|
112
|
+
stack = []
|
|
113
|
+
root = None
|
|
114
|
+
have_root = False
|
|
115
|
+
i = skip(i)
|
|
116
|
+
while True:
|
|
117
|
+
if i >= n:
|
|
118
|
+
raise _NotJson('unexpected end')
|
|
119
|
+
c = text[i]
|
|
120
|
+
# Read one value, or open a container.
|
|
121
|
+
if c == '{' or c == '[':
|
|
122
|
+
container = {} if c == '{' else []
|
|
123
|
+
i = skip(i + 1)
|
|
124
|
+
if i < n and text[i] == ('}' if c == '{' else ']'):
|
|
125
|
+
value = container
|
|
126
|
+
i += 1
|
|
127
|
+
else:
|
|
128
|
+
if c == '{':
|
|
129
|
+
if i >= n or text[i] != '"':
|
|
130
|
+
raise _NotJson('expected a name')
|
|
131
|
+
name, i = read_string(i)
|
|
132
|
+
i = skip(i)
|
|
133
|
+
if i >= n or text[i] != ':':
|
|
134
|
+
raise _NotJson('expected a colon')
|
|
135
|
+
i = skip(i + 1)
|
|
136
|
+
stack.append([container, name])
|
|
137
|
+
else:
|
|
138
|
+
stack.append([container, None])
|
|
139
|
+
continue
|
|
140
|
+
elif c == '"':
|
|
141
|
+
value, i = read_string(i)
|
|
142
|
+
elif c in '-0123456789':
|
|
143
|
+
m = _NUMBER.match(text, i)
|
|
144
|
+
if not m:
|
|
145
|
+
raise _NotJson('bad number')
|
|
146
|
+
t = m.group(0)
|
|
147
|
+
value = _number_int(t) if re.fullmatch(r'-?[0-9]+', t) else _number_float(t)
|
|
148
|
+
i = m.end()
|
|
149
|
+
elif text.startswith('true', i):
|
|
150
|
+
value, i = True, i + 4
|
|
151
|
+
elif text.startswith('false', i):
|
|
152
|
+
value, i = False, i + 5
|
|
153
|
+
elif text.startswith('null', i):
|
|
154
|
+
value, i = None, i + 4
|
|
155
|
+
else:
|
|
156
|
+
raise _NotJson('unexpected character')
|
|
157
|
+
# Put the value in place, and close what is complete.
|
|
158
|
+
while True:
|
|
159
|
+
if not stack:
|
|
160
|
+
root, have_root = value, True
|
|
161
|
+
break
|
|
162
|
+
container, name = stack[-1]
|
|
163
|
+
if isinstance(container, dict):
|
|
164
|
+
container[name] = value
|
|
165
|
+
else:
|
|
166
|
+
container.append(value)
|
|
167
|
+
i = skip(i)
|
|
168
|
+
if i < n and text[i] == ',':
|
|
169
|
+
i = skip(i + 1)
|
|
170
|
+
if isinstance(container, dict):
|
|
171
|
+
if i >= n or text[i] != '"':
|
|
172
|
+
raise _NotJson('expected a name')
|
|
173
|
+
name, i = read_string(i)
|
|
174
|
+
i = skip(i)
|
|
175
|
+
if i >= n or text[i] != ':':
|
|
176
|
+
raise _NotJson('expected a colon')
|
|
177
|
+
i = skip(i + 1)
|
|
178
|
+
stack[-1][1] = name
|
|
179
|
+
break
|
|
180
|
+
close = '}' if isinstance(container, dict) else ']'
|
|
181
|
+
if i < n and text[i] == close:
|
|
182
|
+
stack.pop()
|
|
183
|
+
value = container
|
|
184
|
+
i += 1
|
|
185
|
+
continue
|
|
186
|
+
raise _NotJson('expected a comma or the end of a container')
|
|
187
|
+
if have_root:
|
|
188
|
+
break
|
|
189
|
+
i = skip(i)
|
|
190
|
+
if skip(i) != n:
|
|
191
|
+
raise _NotJson('text after the value')
|
|
192
|
+
return root
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
_STRING_ESCAPES = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t'}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def string(text):
|
|
199
|
+
"""JSON.stringify of a string."""
|
|
200
|
+
out = ['"']
|
|
201
|
+
for c in text:
|
|
202
|
+
o = ord(c)
|
|
203
|
+
if c in _STRING_ESCAPES:
|
|
204
|
+
out.append(_STRING_ESCAPES[c])
|
|
205
|
+
elif o < 0x20 or 0xD800 <= o <= 0xDFFF:
|
|
206
|
+
out.append('\\u%04x' % o)
|
|
207
|
+
else:
|
|
208
|
+
out.append(c)
|
|
209
|
+
out.append('"')
|
|
210
|
+
return ''.join(out)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def utf16_key(text):
|
|
214
|
+
"""A key that sorts strings as JavaScript's default sort does: by UTF-16 code unit."""
|
|
215
|
+
return text.encode('utf-16-be', 'surrogatepass')
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def utf16_length(text):
|
|
219
|
+
"""A string's "length" in JavaScript: its UTF-16 code units."""
|
|
220
|
+
if text.isascii():
|
|
221
|
+
return len(text)
|
|
222
|
+
return len(text.encode('utf-16-le', 'surrogatepass')) // 2
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def sort_strings(names):
|
|
226
|
+
return sorted(names, key=utf16_key)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
_INDEX = re.compile(r'0|[1-9][0-9]*')
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def keys(obj):
|
|
233
|
+
"""Object.keys of an object read from JSON: names that are list indexes
|
|
234
|
+
(0 to 2**32 - 2) first, in number order, then the others as written."""
|
|
235
|
+
indexes = []
|
|
236
|
+
others = []
|
|
237
|
+
for k in obj:
|
|
238
|
+
if _INDEX.fullmatch(k) and int(k) <= 4294967294:
|
|
239
|
+
indexes.append(k)
|
|
240
|
+
else:
|
|
241
|
+
others.append(k)
|
|
242
|
+
if not indexes:
|
|
243
|
+
return others
|
|
244
|
+
indexes.sort(key=int)
|
|
245
|
+
return indexes + others
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def is_object(value):
|
|
249
|
+
"""value !== null && typeof value === 'object' && !Array.isArray(value)"""
|
|
250
|
+
return isinstance(value, dict)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def is_number(value):
|
|
254
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def is_safe_integer(value):
|
|
258
|
+
"""Number.isSafeInteger"""
|
|
259
|
+
if isinstance(value, bool):
|
|
260
|
+
return False
|
|
261
|
+
if isinstance(value, int):
|
|
262
|
+
return -MAX_SAFE_INTEGER <= value <= MAX_SAFE_INTEGER
|
|
263
|
+
if isinstance(value, float):
|
|
264
|
+
return value == value and value not in (float('inf'), float('-inf')) and value.is_integer() and abs(value) <= MAX_SAFE_INTEGER
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def whole(value):
|
|
269
|
+
"""A number that JavaScript holds as a whole number, as an int: so 2.0 is 2.
|
|
270
|
+
Anything else is given back as it is."""
|
|
271
|
+
if isinstance(value, float) and value.is_integer() and abs(value) <= MAX_SAFE_INTEGER:
|
|
272
|
+
return int(value)
|
|
273
|
+
return value
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def in_key_order(obj):
|
|
277
|
+
"""An object with its members in the order Object.keys gives them."""
|
|
278
|
+
return {k: obj[k] for k in keys(obj)}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def property_key(name):
|
|
282
|
+
"""The name of a member as JavaScript holds it: always text. A dict key that
|
|
283
|
+
is a number, True, False or None becomes the text JavaScript would give
|
|
284
|
+
it; any other key is not plain data (TypeError)."""
|
|
285
|
+
if isinstance(name, str):
|
|
286
|
+
return name
|
|
287
|
+
if name is None:
|
|
288
|
+
return 'null'
|
|
289
|
+
if isinstance(name, bool):
|
|
290
|
+
return 'true' if name else 'false'
|
|
291
|
+
if is_number(name):
|
|
292
|
+
return number_string(name)
|
|
293
|
+
raise TypeError('not plain data')
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def finite(value):
|
|
297
|
+
"""Number.isFinite, for a Python number: a whole number too large for a
|
|
298
|
+
floating-point number is Infinity in JavaScript."""
|
|
299
|
+
if not is_number(value):
|
|
300
|
+
return False
|
|
301
|
+
try:
|
|
302
|
+
return math.isfinite(value)
|
|
303
|
+
except OverflowError:
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def truthy(value):
|
|
308
|
+
"""Whether JavaScript counts a value as true: everything but undefined,
|
|
309
|
+
null, false, 0, NaN and the empty text. An empty list or object counts."""
|
|
310
|
+
if value is None or value is False:
|
|
311
|
+
return False
|
|
312
|
+
if is_number(value):
|
|
313
|
+
return value == value and value != 0
|
|
314
|
+
if isinstance(value, str):
|
|
315
|
+
return value != ''
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def strict_equal(a, b):
|
|
320
|
+
"""a === b for values read from JSON: true and 1 are not the same, nor are
|
|
321
|
+
a number and a text."""
|
|
322
|
+
if isinstance(a, bool) or isinstance(b, bool):
|
|
323
|
+
return isinstance(a, bool) and isinstance(b, bool) and a == b
|
|
324
|
+
if is_number(a) and is_number(b):
|
|
325
|
+
return a == b
|
|
326
|
+
if a is None or b is None:
|
|
327
|
+
return a is b
|
|
328
|
+
if isinstance(a, str) and isinstance(b, str):
|
|
329
|
+
return a == b
|
|
330
|
+
return a is b
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def includes(items, value):
|
|
334
|
+
"""Array.prototype.includes, which compares as === does (and finds NaN)."""
|
|
335
|
+
if isinstance(value, float) and value != value:
|
|
336
|
+
return any(isinstance(x, float) and x != x for x in items)
|
|
337
|
+
return any(strict_equal(x, value) for x in items)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class IdentityMap:
|
|
341
|
+
"""A JavaScript Map whose keys are objects: found by identity, not by value."""
|
|
342
|
+
|
|
343
|
+
__slots__ = ('_items',)
|
|
344
|
+
|
|
345
|
+
def __init__(self, items=None):
|
|
346
|
+
self._items = dict(items._items) if isinstance(items, IdentityMap) else {}
|
|
347
|
+
|
|
348
|
+
def get(self, key, default=None):
|
|
349
|
+
found = self._items.get(id(key))
|
|
350
|
+
return default if found is None else found[1]
|
|
351
|
+
|
|
352
|
+
def set(self, key, value):
|
|
353
|
+
self._items[id(key)] = (key, value)
|
|
354
|
+
|
|
355
|
+
def has(self, key):
|
|
356
|
+
return id(key) in self._items
|
|
357
|
+
|
|
358
|
+
def values(self):
|
|
359
|
+
return [v for _, v in self._items.values()]
|
|
360
|
+
|
|
361
|
+
def __len__(self):
|
|
362
|
+
return len(self._items)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def number_text(value):
|
|
366
|
+
"""String(value) for a whole number."""
|
|
367
|
+
if isinstance(value, float):
|
|
368
|
+
return str(int(value))
|
|
369
|
+
return str(value)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def number_value(value):
|
|
373
|
+
"""The number JavaScript would hold for a Python number: a float, or an
|
|
374
|
+
int that a float holds exactly. A whole number beyond 2^53 - 1 becomes
|
|
375
|
+
the nearest float, as JSON.parse would read it. Raises OverflowError for
|
|
376
|
+
a whole number too large for a float. A number of a class built on int
|
|
377
|
+
or float (an Enum of whole numbers, say) is taken as its plain value."""
|
|
378
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
379
|
+
value = int.__int__(value)
|
|
380
|
+
if -MAX_SAFE_INTEGER <= value <= MAX_SAFE_INTEGER:
|
|
381
|
+
return value
|
|
382
|
+
return float(value)
|
|
383
|
+
if isinstance(value, float):
|
|
384
|
+
return float.__float__(value)
|
|
385
|
+
return value
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def number_string(value):
|
|
389
|
+
"""Number.prototype.toString() in base 10, which JSON.stringify also
|
|
390
|
+
uses for a finite number: the fewest digits that read back as the same
|
|
391
|
+
number, written as ECMAScript sets out (section 6.1.6.1.20). So 1e21 is
|
|
392
|
+
"1e+21", 1.5e-7 is "1.5e-7", 1e-6 is "0.000001" and 0.1 + 0.2 is
|
|
393
|
+
"0.30000000000000004". Raises OverflowError for a whole number too large
|
|
394
|
+
for a float."""
|
|
395
|
+
value = number_value(value)
|
|
396
|
+
if isinstance(value, int):
|
|
397
|
+
return str(value)
|
|
398
|
+
if value != value:
|
|
399
|
+
return 'NaN'
|
|
400
|
+
if value in (float('inf'), float('-inf')):
|
|
401
|
+
return 'Infinity' if value > 0 else '-Infinity'
|
|
402
|
+
if value == 0:
|
|
403
|
+
# Both 0 and -0 are written "0".
|
|
404
|
+
return '0'
|
|
405
|
+
sign = '-' if value < 0 else ''
|
|
406
|
+
# Python's repr gives the same fewest digits, closest to the value.
|
|
407
|
+
mantissa, _, exponent = repr(abs(value)).partition('e')
|
|
408
|
+
whole, _, fraction = mantissa.partition('.')
|
|
409
|
+
digits = whole + fraction
|
|
410
|
+
# The value is 0.digits times 10 to the power "point".
|
|
411
|
+
point = len(whole) + (int(exponent) if exponent else 0)
|
|
412
|
+
stripped = digits.lstrip('0')
|
|
413
|
+
point -= len(digits) - len(stripped)
|
|
414
|
+
digits = stripped.rstrip('0')
|
|
415
|
+
k = len(digits)
|
|
416
|
+
n = point
|
|
417
|
+
if k <= n <= 21:
|
|
418
|
+
out = digits + '0' * (n - k)
|
|
419
|
+
elif 0 < n <= 21:
|
|
420
|
+
out = digits[:n] + '.' + digits[n:]
|
|
421
|
+
elif -6 < n <= 0:
|
|
422
|
+
out = '0.' + '0' * (-n) + digits
|
|
423
|
+
else:
|
|
424
|
+
e = n - 1
|
|
425
|
+
mark = '+' if e >= 0 else '-'
|
|
426
|
+
out = (digits if k == 1 else digits[0] + '.' + digits[1:]) + 'e' + mark + str(abs(e))
|
|
427
|
+
return sign + out
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def stringify(value):
|
|
431
|
+
"""JSON.stringify for plain data (objects, lists, text, numbers, true,
|
|
432
|
+
false and null): NaN and the infinities are written null, -0 as 0, and
|
|
433
|
+
members in the order Object.keys gives them."""
|
|
434
|
+
if value is None:
|
|
435
|
+
return 'null'
|
|
436
|
+
if value is True:
|
|
437
|
+
return 'true'
|
|
438
|
+
if value is False:
|
|
439
|
+
return 'false'
|
|
440
|
+
if isinstance(value, str):
|
|
441
|
+
return string(value)
|
|
442
|
+
if is_number(value):
|
|
443
|
+
if isinstance(value, float) and (value != value or value in (float('inf'), float('-inf'))):
|
|
444
|
+
return 'null'
|
|
445
|
+
return number_string(value)
|
|
446
|
+
if isinstance(value, list):
|
|
447
|
+
return '[' + ','.join(stringify(v) for v in value) + ']'
|
|
448
|
+
if isinstance(value, dict):
|
|
449
|
+
return '{' + ','.join(string(k) + ':' + stringify(value[k]) for k in keys(value)) + '}'
|
|
450
|
+
raise TypeError('not plain data')
|
provared/actions.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# The shared list of action names, the kinds of action, and the rules of
|
|
2
|
+
# conduct a slip can state. Format description, section 16.
|
|
3
|
+
#
|
|
4
|
+
# This is a first draft, written before real agents have used it. Anyone may
|
|
5
|
+
# go on using action names of their own; names that begin "provared." are
|
|
6
|
+
# reserved for this list.
|
|
7
|
+
|
|
8
|
+
RESERVED_PREFIX = 'provared.'
|
|
9
|
+
"""Every action name that begins with this is reserved for the shared list."""
|
|
10
|
+
|
|
11
|
+
ACTION_KINDS = {
|
|
12
|
+
'reads': 'Looks at data without changing it.',
|
|
13
|
+
'changes': 'Creates or changes data in a way that can be put back.',
|
|
14
|
+
'destroys': 'Removes data or a resource in a way that may not be put back.',
|
|
15
|
+
'sends': 'Sends a message or data to a person, to the public or to another system.',
|
|
16
|
+
'commits': 'Binds the person to something: an order, a booking, an agreement, an account.',
|
|
17
|
+
'grants': 'Gives someone or something access or permission.',
|
|
18
|
+
'runs': 'Runs a program or a command.',
|
|
19
|
+
'enters': 'Signs in to a system or an account.',
|
|
20
|
+
}
|
|
21
|
+
"""The kinds of action. Each shared action belongs to exactly one."""
|
|
22
|
+
|
|
23
|
+
SHARED_ACTIONS = {
|
|
24
|
+
'provared.data.read': ('reads', 'Read stored data or a file.'),
|
|
25
|
+
'provared.data.search': ('reads', 'Search stored data.'),
|
|
26
|
+
'provared.data.create': ('changes', 'Create a new item of data or a new file.'),
|
|
27
|
+
'provared.data.change': ('changes', 'Change an item of data or a file that exists.'),
|
|
28
|
+
'provared.data.delete': ('destroys', 'Delete data or a file.'),
|
|
29
|
+
'provared.data.export': ('sends', 'Copy data out of the system that keeps it.'),
|
|
30
|
+
'provared.message.read': ('reads', 'Read messages.'),
|
|
31
|
+
'provared.message.draft': ('changes', 'Write a message without sending it.'),
|
|
32
|
+
'provared.message.send': ('sends', 'Send a message to a person.'),
|
|
33
|
+
'provared.post.publish': ('sends', 'Publish something where the public can see it.'),
|
|
34
|
+
'provared.calendar.read': ('reads', 'Read a calendar.'),
|
|
35
|
+
'provared.calendar.change': ('changes', 'Add, move or remove an entry in a calendar.'),
|
|
36
|
+
'provared.booking.make': ('commits', 'Book a place, a time or a service.'),
|
|
37
|
+
'provared.booking.cancel': ('commits', 'Cancel a booking.'),
|
|
38
|
+
'provared.order.place': ('commits', 'Place an order for goods or services.'),
|
|
39
|
+
'provared.order.cancel': ('commits', 'Cancel an order.'),
|
|
40
|
+
'provared.terms.accept': ('commits', 'Accept terms, or sign an agreement.'),
|
|
41
|
+
'provared.form.submit': ('commits', 'Submit a form or an application.'),
|
|
42
|
+
'provared.account.create': ('commits', 'Open an account.'),
|
|
43
|
+
'provared.account.sign-in': ('enters', 'Sign in to an account or a system.'),
|
|
44
|
+
'provared.access.grant': ('grants', 'Give a person or a program access.'),
|
|
45
|
+
'provared.access.remove': ('changes', 'Take access away from a person or a program.'),
|
|
46
|
+
'provared.key.create': ('grants', 'Make a key or a password that gives access.'),
|
|
47
|
+
'provared.code.read': ('reads', 'Read the source of a program.'),
|
|
48
|
+
'provared.code.change': ('changes', 'Change the source of a program.'),
|
|
49
|
+
'provared.code.run': ('runs', 'Run a program or a command.'),
|
|
50
|
+
'provared.code.release': ('runs', 'Put a program into live use.'),
|
|
51
|
+
'provared.system.configure': ('changes', 'Change the settings of a system.'),
|
|
52
|
+
'provared.web.read': ('reads', 'Read a public web page.'),
|
|
53
|
+
'provared.agent.instruct': ('sends', 'Hand a task to another agent.'),
|
|
54
|
+
}
|
|
55
|
+
"""The shared actions: name, kind, and what the name means."""
|
|
56
|
+
|
|
57
|
+
CONDUCT_RULES = {
|
|
58
|
+
'impersonate': 'Never claim to be a human being, or to be anyone other than the agent of the person who signed the slip.',
|
|
59
|
+
'bypass': 'Never go around an access control, a block or a refusal.',
|
|
60
|
+
'escalate': 'Never obtain or use access that it was not given.',
|
|
61
|
+
'conceal': 'Never hide, alter or leave out its own records.',
|
|
62
|
+
'deceive': 'Never state what it has reason to believe is false, and never make up a record or a result.',
|
|
63
|
+
'harass': 'Never threaten, pressure or attack a person.',
|
|
64
|
+
'disclose': 'Never pass information that is not public to anyone the slip does not name.',
|
|
65
|
+
'continue-after-stop': 'Never carry on after being told to stop.',
|
|
66
|
+
}
|
|
67
|
+
"""The rules of conduct a slip can state. They are the person's signed
|
|
68
|
+
instruction. No check of a record can show that an agent kept them."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def action_kind(name):
|
|
72
|
+
"""The kind of a shared action, or None for a name that is not on the shared
|
|
73
|
+
list (a name of the writer's own)."""
|
|
74
|
+
return SHARED_ACTIONS[name][0] if isinstance(name, str) and name in SHARED_ACTIONS else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_never_name(name):
|
|
78
|
+
"""Whether a name may stand in a "never" list: a kind of action, or a rule of conduct."""
|
|
79
|
+
return isinstance(name, str) and (name in ACTION_KINDS or name in CONDUCT_RULES)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_unknown_reserved(name):
|
|
83
|
+
"""Whether an action name is reserved and not on the shared list."""
|
|
84
|
+
return isinstance(name, str) and name.startswith(RESERVED_PREFIX) and name not in SHARED_ACTIONS
|