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 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