sayfirst-cli 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.
@@ -0,0 +1,1162 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The proof harness: what the interpreter reported, against what the plane recorded.
3
+
4
+ Layer 3 of the instrumentation chain, and the one piece of it that trusts
5
+ nothing else in the chain. It watches the interpreter's own audit events and
6
+ reads the scope's evidence, and it holds no other source: no bookkeeping of the
7
+ engine's, no answer the boundary kept, nothing a pack said it had done. A proof
8
+ that trusts the thing it is proving is not a proof, which is why this file's
9
+ imports of the layers below it are the ones that RUN the program and read what
10
+ a pack declares — never the ones that would tell it what happened. (It names
11
+ the engine for one thing only: the exception class the engine raises out of the
12
+ program's own import, which is a refusal of the invocation and not a report of
13
+ anything that happened.)
14
+
15
+ **Why a process of its own.** The hook that stops an effect raises, and a hook
16
+ installed on this interpreter can never be removed. That irreversibility
17
+ disqualifies an audit hook as the engine — article 9 asks the primary mode to
18
+ be reversible — and is exactly what a proof harness wants. So the hook lives
19
+ and dies with this process, and the command that spawns it keeps none of it.
20
+
21
+ **What it concludes, in three words and no fourth.** `governed`: the effect
22
+ happened and a decision preceded it. `ungoverned`: the effect happened and no
23
+ decision preceded it. `not-exercised`: this run never walked the path. The
24
+ third is never a pass, and this file never turns it into one — it counts events
25
+ and writes them down; the command reads the report and decides the exit.
26
+
27
+ **One thing more is written down, and it is a COUNT rather than a fourth
28
+ word.** An event can arrive that this proof cannot judge at all: its argument
29
+ names one of the program's own start files after the program has begun, which
30
+ is the one shape `Watch` cannot tell from the hand-off's own reading of the
31
+ same file. Such an event is counted on the point it would have been judged
32
+ against and published as `unjudged`, beside a verdict that stays about the
33
+ events that WERE judged. It is not a verdict and not one of the three words:
34
+ every way of spelling it as one says something the run did not measure. What it
35
+ does is refuse the run a pass — `verify` cannot answer 0 while any count is
36
+ non-zero — which is the direction article 2 requires, and the direction this
37
+ rule failed in for as long as the same events were silently dropped.
38
+
39
+ **A fourth thing can happen, and it is not a verdict.** The chain may be
40
+ unreadable while the program runs. « No record exists » and « this client could
41
+ not read the chain » are different facts, and `exit_codes.py` keeps them apart:
42
+ a consultation in which not one read was answered ends the run with « could not
43
+ read » and no findings at all, never with `ungoverned`. The effect is still
44
+ aborted, because an effect whose governance could not be established is not one
45
+ this harness may let through — but nothing is concluded about the program.
46
+
47
+ **The wait, and why it is not a caller's to set.** Article 10 makes the chain
48
+ write asynchronous, so an effect's record may arrive after the effect was
49
+ allowed. The poll therefore has patience, and the patience is fixed here: a
50
+ verifier whose patience a caller could set to nothing would report `ungoverned`
51
+ for a daemon that was merely slow, which is a false finding rather than a
52
+ configurable one.
53
+
54
+ **Whose act was it.** `Watch` answers that question and nothing else, and it
55
+ exists because this harness runs inside the process it is watching: its own
56
+ chain reads, its own report write and the interpreter's own reading of the
57
+ program's file all raise the events a pack may have named. `Watch` says what
58
+ each rule excludes and why.
59
+
60
+ **Nothing here names a library.** The events it watches for are read off the
61
+ manifests the invocation designated — `tests/test_engine_is_agnostic.py` binds
62
+ this file for the same reason it binds the engine: a harness that knew one
63
+ library's event name would be the special case article 4 forbids, arriving
64
+ inside the one component whose whole job is to be impartial about what it
65
+ watches.
66
+ """
67
+
68
+ from __future__ import annotations
69
+
70
+ import atexit
71
+ import json
72
+ import os
73
+ import sys
74
+ import threading
75
+ import time
76
+ from collections.abc import Callable, Iterator, Mapping, Sequence
77
+ from contextlib import contextmanager, suppress
78
+ from dataclasses import dataclass, field
79
+ from pathlib import Path
80
+ from typing import Final
81
+
82
+ from sayfirst_contract.client import Answered, Result
83
+ from sayfirst_contract.problems import Problem, ProblemCode
84
+ from sayfirst_contract.transport.socket_client import (
85
+ ProfileMisuse,
86
+ SocketClientProblem,
87
+ SocketProfile,
88
+ VerifiedConnection,
89
+ connect,
90
+ )
91
+
92
+ from .. import exit_codes, pages, reads, render
93
+ from . import engine, launch, manifest
94
+
95
+ #: An effect happened and a decision preceded it.
96
+ GOVERNED: Final[str] = "governed"
97
+
98
+ #: An effect happened and no decision preceded it.
99
+ UNGOVERNED: Final[str] = "ungoverned"
100
+
101
+ #: This run never walked the path. Never rendered as a pass (article 2).
102
+ NOT_EXERCISED: Final[str] = "not-exercised"
103
+
104
+ #: The closed vocabulary, so that a fourth word cannot be spelled by accident.
105
+ VERDICTS: Final[tuple[str, ...]] = (GOVERNED, UNGOVERNED, NOT_EXERCISED)
106
+
107
+ #: Whose act one event was. Three answers and not two, which is what the false
108
+ #: all-clear this rule was corrected for cost: an event whose argument names one of the
109
+ #: program's own start files, arriving after the gate opened, is neither the
110
+ #: hand-off's act nor one this proof can judge — and folding it into « not the
111
+ #: program's » hid an unjudged effect behind a judged one. These are NOT
112
+ #: verdicts and are deliberately not in `VERDICTS`: a verdict is about a point
113
+ #: across a whole run, and the vocabulary above stays three words wide.
114
+ THE_PROGRAMS: Final[str] = "the program's"
115
+ THE_HAND_OFFS: Final[str] = "the hand-off's"
116
+ NOT_JUDGED: Final[str] = "not judged"
117
+
118
+ #: What the report calls the count of events a point could not judge. A COUNT
119
+ #: and never a verdict — the run carries both, because « one effect on this
120
+ #: point was governed » and « another was not judged » are two facts and one
121
+ #: word cannot carry them.
122
+ UNJUDGED: Final[str] = "unjudged"
123
+
124
+ #: The kind of chain entry that records an effect, and the outcome that let it
125
+ #: happen. Both are the plane's own words, read and never translated.
126
+ EFFECT: Final[str] = "effect"
127
+ ALLOW: Final[str] = "allow"
128
+
129
+ #: How long one consultation waits for the record of an effect to appear.
130
+ #: Article 10 writes the chain asynchronously; see the module docstring for why
131
+ #: this is not an option.
132
+ CHAIN_WAIT: Final[float] = 2.0
133
+
134
+ #: How long between two poll cycles. Short enough that the wait above is spent
135
+ #: waiting rather than sleeping.
136
+ POLL_INTERVAL: Final[float] = 0.05
137
+
138
+ #: How many entries one read asks for, which is the daemon's own maximum.
139
+ PAGE_SIZE: Final[int] = 100
140
+
141
+ #: What separates this harness's own arguments from the program's.
142
+ SEPARATOR: Final[str] = "--"
143
+
144
+ #: What a run says when it never saw the program's own code begin. It is not a
145
+ #: verdict and it is not `not-exercised`: nothing was watched, so nothing about
146
+ #: the program was established — not even an absence.
147
+ NEVER_STARTED: Final[str] = (
148
+ "the verifier never saw the program's own code start, so nothing about this program "
149
+ "was watched and no verdict is reported: the interpreter ran it from a code object "
150
+ "that carries no file this run resolved, which is what a module shipped as bytecode "
151
+ "with no source beside it looks like"
152
+ )
153
+
154
+ #: The configuration member naming the file this harness says its own ending in.
155
+ OUTCOME_FILE_MEMBER: Final[str] = "outcome"
156
+
157
+ #: The findings were written; whatever else is wrong with them is the reader's
158
+ #: to say.
159
+ REPORTED: Final[str] = "reported"
160
+
161
+ #: The invocation was refused and the program never started.
162
+ INVOCATION_REFUSED: Final[str] = "invocation_refused"
163
+
164
+ #: The chain could not be read before the program started, so nothing was ever
165
+ #: watched.
166
+ CHAIN_UNREADABLE_BEFORE: Final[str] = "chain_unreadable_before"
167
+
168
+ #: The chain could not be read while the program ran, so an effect was aborted
169
+ #: on an unknown and nothing about the program is claimed.
170
+ CHAIN_UNREADABLE_DURING: Final[str] = "chain_unreadable_during"
171
+
172
+ #: The gate never opened: the interpreter never reported one of the program's
173
+ #: own code objects, so this proof never began watching.
174
+ GATE_NEVER_OPENED: Final[str] = "gate_never_opened"
175
+
176
+ #: The closed vocabulary of this harness's own endings, so a sixth cannot be
177
+ #: spelled by accident. They are NOT verdicts: a verdict is about a point across
178
+ #: a run, and these are about the run itself.
179
+ OUTCOMES: Final[tuple[str, ...]] = (
180
+ REPORTED,
181
+ INVOCATION_REFUSED,
182
+ CHAIN_UNREADABLE_BEFORE,
183
+ CHAIN_UNREADABLE_DURING,
184
+ GATE_NEVER_OPENED,
185
+ )
186
+
187
+ #: The members the configuration must carry, and the shape each has to have.
188
+ #: Read through a table so that « declared as the wrong thing » and « not
189
+ #: declared at all » take one path to one refusal.
190
+ CONFIGURED: Final[tuple[str, ...]] = (
191
+ "packs",
192
+ "socket",
193
+ "mode",
194
+ "daemon_user",
195
+ "scope",
196
+ "principal",
197
+ "governed",
198
+ "report",
199
+ OUTCOME_FILE_MEMBER,
200
+ )
201
+
202
+
203
+ class HarnessMisuse(ValueError):
204
+ """This harness cannot run what it was asked to, and nothing was proven."""
205
+
206
+
207
+ class ChainUnreadable(RuntimeError):
208
+ """An effect could not be verified, because the chain could not be read at all.
209
+
210
+ Raised into the target, so the effect is aborted rather than let through on
211
+ an unknown. It is deliberately NOT the same exception as an ungoverned
212
+ effect, and it produces no finding: the problem is carried on the watch and
213
+ the run ends with « could not read ». Publishing exit 6 — "a finding this
214
+ client made" — for a plane that was never read would be exactly the
215
+ collision between a finding and an unanswerable question that
216
+ `exit_codes.py` exists to prevent (articles 1 and 2).
217
+ """
218
+
219
+
220
+ @dataclass
221
+ class Watched:
222
+ """One interposition point, and what this run observed about it."""
223
+
224
+ pack: str
225
+ point: manifest.Point
226
+ events: int = 0
227
+ refused: bool = False
228
+ #: Events on this point that this run could not judge, because an argument
229
+ #: named one of the program's own start files after the gate had opened —
230
+ #: the one shape `Watch` cannot tell from the hand-off's own reading of the
231
+ #: same file. Counted rather than dropped, and carried beside the verdict
232
+ #: rather than folded into it: the command reads it and cannot answer 0.
233
+ unjudged: int = 0
234
+
235
+ @property
236
+ def verdict(self) -> str:
237
+ """The three words, in the one order that cannot flatter the run.
238
+
239
+ An effect that was ever unproven is `ungoverned` whatever happened
240
+ afterwards: a verdict is about the whole run, and a later decision does
241
+ not retroactively decide an effect that already went unproven. A point
242
+ no event reached is `not-exercised`, which is an absence and is said as
243
+ one rather than counted with the sound ones (article 2).
244
+
245
+ `unjudged` is not in here and must not be: it is a count of what this
246
+ run could not establish, and every way of turning it into one of the
247
+ three words says something the run did not measure — `ungoverned` would
248
+ be a finding nobody made, `not-exercised` would deny the event that did
249
+ arrive, and `governed` would be the false all-clear. So the verdict
250
+ stays honest about the events that WERE judged, the count is published
251
+ beside it, and the exit code is what refuses to call the run a pass
252
+ (`verify._exit_for`).
253
+ """
254
+ if self.refused:
255
+ return UNGOVERNED
256
+ return NOT_EXERCISED if self.events == 0 else GOVERNED
257
+
258
+ def to_document(self) -> dict[str, object]:
259
+ return {
260
+ "pack": self.pack,
261
+ "module": self.point.module,
262
+ "attribute": self.point.attribute,
263
+ "capability": self.point.capability,
264
+ "audit_event": self.point.audit_event,
265
+ "verdict": self.verdict,
266
+ "events": self.events,
267
+ UNJUDGED: self.unjudged,
268
+ }
269
+
270
+
271
+ class Watch:
272
+ """Whose act an event was: the program's, or this harness's own.
273
+
274
+ The question exists because the proof runs inside the process it is
275
+ proving. Four rules, each excluding a different kind of act that is not the
276
+ program's, and each one measurable rather than argued.
277
+
278
+ **Armed only while the program runs.** Nothing is consulted before the
279
+ launcher hands over or after the program is DONE. Before: the engine's load
280
+ of a pack's execution module and the lookup of the target's own name.
281
+ After: the report write and the traceback the interpreter is about to
282
+ render. The hook is installed before the engine on purpose — an interpreter
283
+ that could be asked to forget a hook would make the proof optional — so the
284
+ arming, and not the installation, is what draws the line.
285
+
286
+ « Done » is not « its main module returned ». A non-daemon thread the
287
+ program started outlives that, and so does a handler it registered to run
288
+ at exit; both are the program's own code, and the interpreter runs both
289
+ after this process would otherwise have concluded.
290
+ `_wait_for_the_program` is what makes this paragraph true, and says what it
291
+ cost to leave untrue.
292
+
293
+ **Not before the program's own code begins.** Handing a program over means
294
+ locating it, reading it, reading or writing its bytecode cache, and only
295
+ then executing it; every one of those is the hand-off's act and the
296
+ interpreter reports them all. So nothing is judged until the interpreter
297
+ reports one of the program's OWN code objects — the launcher names every
298
+ file whose execution is the program running, and the FIRST of them to
299
+ execute opens the gate. For a package named to `-m` that is its
300
+ `__init__`, not its `__main__`: the `__init__` is the program's own code
301
+ and it runs first, and a gate that waited for the `__main__` left every
302
+ effect the `__init__` made unjudged and unaborted — measured as exit 0 over
303
+ a spawn no decision covered.
304
+
305
+ The gate is also what makes the count exact rather than nearly exact: the
306
+ import system opens a bytecode cache under a name it derives itself and
307
+ then writes through a bare descriptor, and neither can be recognised by any
308
+ path a launcher could have named in advance.
309
+
310
+ Its failure mode is the safe one, which is why it is allowed to be the rule
311
+ that matters. A target whose executed code object does not carry a file the
312
+ lookup resolved — a module shipped as bytecode with no source beside it —
313
+ leaves this gate shut, and a run whose gate never opened concludes nothing
314
+ at all: it says so and answers « could not read », rather than reporting
315
+ the absence it never established (`_prove` says where).
316
+
317
+ **Not inside a consultation.** A consultation reads the chain, and those
318
+ reads are themselves acts the interpreter reports. Per thread, so an event
319
+ arriving on another thread is still the program's and is still judged.
320
+
321
+ **Not the files the hand-off itself reads, while it is still reading
322
+ them.** The program's own file, a package's `__main__`, and the bytecode
323
+ cache the import system keeps for each — named by the launcher, which asks
324
+ the import system where a cache lives rather than spelling it. The import
325
+ system's derivation from a cache is the one clause that outlives the gate
326
+ opening: it writes a cache by creating a file named after that cache plus a
327
+ number of its own, and a number known to nobody in advance can only be
328
+ matched by the derivation, whenever it fires.
329
+
330
+ **And whose act it was is three answers, not two.** The trade this rule
331
+ used to make was stated here, and stated too weakly: « what it excludes and
332
+ should not is a program that opens one of its OWN start files or their
333
+ caches; that shows up as an absence — `not-exercised` — and never as a
334
+ pass, which is the direction this trade has to fail in. » That held only
335
+ when EVERY event on the point was excluded. Mixed with one judged event, the
336
+ excluded ones vanished behind it and the run answered `governed`, exit 0 —
337
+ measured through the shipped command on two pairs of programs differing in
338
+ one argument — one pair opening a store under the program's own path
339
+ instead of an ordinary one, the other spawning the program's own file as
340
+ the executable instead of an ordinary command — each second effect real,
341
+ and covered by no decision. That is the false all-clear article 2 forbids,
342
+ in the component whose whole value is catching it.
343
+
344
+ So an argument that names one of the program's own start files or their
345
+ caches, arriving AFTER the gate opened, is answered `NOT_JUDGED` rather
346
+ than `THE_HAND_OFFS`: the hand-off has finished reading those files by
347
+ then, and this proof cannot tell the program naming its own file from the
348
+ import system finishing with it. The caller counts it on the point and the
349
+ run cannot be a pass. Measured on the six ordinary forms — a script, a
350
+ module and a package, each cold and warm — every exclusion of either clause
351
+ fires before the gate opens, so the count is zero in all of them and this
352
+ answer is reserved for the shape it was written for.
353
+
354
+ This is the ONE place an event's arguments are read, and reading them
355
+ decides only whose act it was. Which arguments identify an effect is the
356
+ pack's declaration and the boundary's digest (article 11); a verifier with
357
+ an opinion of its own about them would be a second policy.
358
+ """
359
+
360
+ def __init__(self) -> None:
361
+ self.armed = False
362
+ self.started = False
363
+ self.excluded: frozenset[str] = frozenset()
364
+ #: The problem of a consultation in which no read was answered. Carried
365
+ #: here as well as raised, so a target that caught the exception cannot
366
+ #: bury the fact that nothing was verified.
367
+ self.unreadable: Problem | None = None
368
+ #: Whether the gate EVER opened. `disarm` clears `started`, and the run
369
+ #: has to be able to tell « this program walked no such path » from
370
+ #: « this proof never began watching » afterwards.
371
+ self.ever_started = False
372
+ self._starts: frozenset[str] = frozenset()
373
+ self._derived: frozenset[str] = frozenset()
374
+ self._ours = threading.local()
375
+
376
+ @property
377
+ def judging(self) -> bool:
378
+ """Whether an event now is one this run may conclude anything from."""
379
+ return self.armed and self.started
380
+
381
+ def arm(self, starts: tuple[str, ...], own: tuple[str, ...], caches: tuple[str, ...]) -> None:
382
+ """The launcher is about to hand over.
383
+
384
+ `starts` are the program's OWN files — every one whose execution is the
385
+ program running rather than the hand-off preparing it; the first of them
386
+ to execute opens the gate. `own` are the files the hand-off reads to get
387
+ there, and `caches` are those of them the import system derives further
388
+ names from. All three come from the launcher, which is the only place
389
+ they are known: resolving them again here, on a different import path,
390
+ would be a different question with a different answer.
391
+ """
392
+ self._starts = _the_paths(starts)
393
+ self.excluded = _the_paths(own)
394
+ self._derived = _the_paths(caches)
395
+ self.started = False
396
+ self.armed = True
397
+
398
+ def opening(self, arguments: tuple[object, ...]) -> None:
399
+ """Open the gate once the interpreter reports the program's code, and alone.
400
+
401
+ The interpreter reports running a code object by handing a hook that
402
+ code object and nothing besides. Other events carry one too, and one of
403
+ them comes first: the import system marshals a freshly compiled module
404
+ in order to write its bytecode cache. So « the code object, by itself »
405
+ is what tells « this is being run » from « this is being written down ».
406
+ Measured on the way to this line: keyed on the code object wherever it
407
+ appeared, the gate opened on the cache write and two of the hand-off's
408
+ own file events were judged as the program's.
409
+
410
+ It is deliberately NOT keyed on the event's name. Every audit event
411
+ this harness knows is read from a pack's manifest, and the interpreter's
412
+ own name for running code is read from nobody's — a name spelled here
413
+ would be this file holding a vocabulary of its own, which is the thing
414
+ article 4 denies it. The shape is an interpreter detail, so it is
415
+ measured rather than trusted: `tests/test_instrument_verify.py` counts
416
+ the events a `-m` run produces, cold and warm, and a shape that stops
417
+ being unique arrives there as a red test.
418
+ """
419
+ if not self.armed or self.started or not self._starts:
420
+ return
421
+ if len(arguments) == 1 and _the_code_of(arguments[0]) in self._starts:
422
+ self.started = True
423
+ self.ever_started = True
424
+
425
+ def disarm(self) -> None:
426
+ """The program is done. Nothing after this is the program's act."""
427
+ self.armed = False
428
+ self.started = False
429
+
430
+ @contextmanager
431
+ def ours(self) -> Iterator[None]:
432
+ """This harness's own work, for the length of the block."""
433
+ self._ours.busy = True
434
+ try:
435
+ yield
436
+ finally:
437
+ self._ours.busy = False
438
+
439
+ def whose(self, arguments: tuple[object, ...]) -> str:
440
+ """Whose act this event was: the program's, this process's own, or nobody's.
441
+
442
+ The third answer is the one that cannot be folded into the second. Read
443
+ in the order below, because the clauses are not alternatives: an
444
+ argument the import system derived is the import system's whenever it
445
+ appears, and only what is left over can be the program naming its own
446
+ start file.
447
+ """
448
+ if not self.judging or getattr(self._ours, "busy", False):
449
+ return THE_HAND_OFFS
450
+ named = [name for name in map(_resolved_argument, arguments) if name is not None]
451
+ if any(self._derived_from_a_cache(name) for name in named):
452
+ # A cache the import system derives is never the program's act, so
453
+ # it is not judged and not aborted; but the gate is open here, and
454
+ # a write this proof did not judge is COUNTED, so the run can never
455
+ # read as a pass over it (article 2). Every ordinary form writes its
456
+ # caches before the gate opens, so this count is zero where it is
457
+ # green today; a program naming its own cache path is the one case.
458
+ return NOT_JUDGED
459
+ if any(name in self.excluded for name in named):
460
+ return NOT_JUDGED
461
+ return THE_PROGRAMS
462
+
463
+ def _derived_from_a_cache(self, named: str) -> bool:
464
+ """Whether a name is one the import system derived from a cache it was given.
465
+
466
+ It writes a bytecode cache by creating a file named after that cache
467
+ plus a number of its own, and renaming it. The number is the import
468
+ system's and is never known in advance, so only the derivation can
469
+ match it — which is why the caches arrive separately from the rest, and
470
+ why this is the clause that is true whenever it fires rather than only
471
+ while the hand-off is still reading.
472
+ """
473
+ return any(named.startswith(f"{cache}.") for cache in self._derived)
474
+
475
+
476
+ def _the_paths(paths: tuple[str, ...]) -> frozenset[str]:
477
+ """Those of these this process can resolve, as it will see them reported."""
478
+ return frozenset(
479
+ resolved for resolved in (_resolved(path) for path in paths) if resolved is not None
480
+ )
481
+
482
+
483
+ def _resolved(path: str) -> str | None:
484
+ """One path as this process will see it reported, or `None` if unusable."""
485
+ try:
486
+ return str(Path(path).resolve())
487
+ except (OSError, ValueError, RuntimeError):
488
+ return None
489
+
490
+
491
+ def _the_code_of(argument: object) -> str | None:
492
+ """The file an argument was compiled from, if the argument is a code object.
493
+
494
+ Read as its own question rather than through the path reader below, because
495
+ the gate asks something narrower: not « does this event mention a file »
496
+ but « is this event the interpreter executing the program's own code ». No
497
+ event carries a code object for the program's file before that.
498
+ """
499
+ compiled = getattr(argument, "co_filename", None)
500
+ return _resolved(compiled) if isinstance(compiled, str) else None
501
+
502
+
503
+ def _resolved_argument(argument: object) -> str | None:
504
+ """The file an event's argument names, if it names one.
505
+
506
+ An argument may be a path, the bytes of one, something that supplies one,
507
+ or a code object carrying the file it was compiled from. Anything else —
508
+ a mode, a flag, an open descriptor — names no file and is not one.
509
+ """
510
+ if isinstance(argument, str):
511
+ named: str | None = argument
512
+ elif isinstance(argument, bytes):
513
+ try:
514
+ named = argument.decode()
515
+ except UnicodeDecodeError:
516
+ return None
517
+ elif isinstance(argument, os.PathLike):
518
+ named = os.fspath(argument) if isinstance(os.fspath(argument), str) else None
519
+ else:
520
+ compiled = getattr(argument, "co_filename", None)
521
+ named = compiled if isinstance(compiled, str) else None
522
+ return None if named is None else _resolved(named)
523
+
524
+
525
+ @dataclass(frozen=True)
526
+ class Configuration:
527
+ """What the command asked this harness to prove, read strictly."""
528
+
529
+ packs: tuple[Path, ...]
530
+ profile: SocketProfile
531
+ principal: str | None
532
+ governed: bool
533
+ report: Path
534
+ #: Where this harness says which of its own endings happened. Validated like
535
+ #: `report`, because a run that said it nowhere would leave the command
536
+ #: reading the target's exit status again (`_write_outcome` says what that
537
+ #: cost).
538
+ outcome: Path
539
+
540
+
541
+ @dataclass(frozen=True)
542
+ class Consultation:
543
+ """What one consultation of the chain established, and what it could not.
544
+
545
+ Three outcomes rather than two, which is article 2's rule about a status
546
+ surface applied to a single read: a record was found, no record was found
547
+ in a chain that WAS read, or the chain was not read at all. Only the second
548
+ is a finding about the program.
549
+ """
550
+
551
+ matched: int | None
552
+ read_something: bool
553
+ problem: Problem | None
554
+
555
+
556
+ @dataclass
557
+ class Chain:
558
+ """The scope's evidence, as far as this harness has consumed it.
559
+
560
+ The cursor is the sequence of the last entry a consultation matched. A read
561
+ starts after it, so one recorded effect cannot answer for two events: the
562
+ second consultation never sees it again.
563
+ """
564
+
565
+ connection: VerifiedConnection
566
+ scope: str
567
+ cursor: int
568
+ #: One consultation at a time, because an audit event can arrive on any
569
+ #: thread and two of them sharing one HTTP connection would interleave two
570
+ #: reads on one socket.
571
+ lock: threading.Lock = field(default_factory=threading.Lock)
572
+
573
+
574
+ def _write_outcome(
575
+ path: Path, outcome: str, detail: str, *, problem: Problem | None = None
576
+ ) -> None:
577
+ """Say which of this harness's own endings happened, where the target cannot.
578
+
579
+ The command that spawned this process used to read the PROGRAM's exit
580
+ status to tell « this invocation was refused » from « this run concluded
581
+ nothing », and a program that calls `os._exit(64)` chose that reading for
582
+ it. An exit status carries one number and there are two facts; this file
583
+ carries the second.
584
+
585
+ The channel is honest against the ordinary program rather than against a
586
+ hostile one: a target running as this user could write or unlink the file
587
+ itself if it went looking for it. What it cannot do is choose the reading by
588
+ accident, which is the whole of the collision this closes — and a target
589
+ that wrote a false outcome deliberately would be a target lying about a
590
+ proof it also controls the events of.
591
+
592
+ **The transport's own classification travels with it where there is one.**
593
+ Two of these endings are a chain read that did not answer, and the read
594
+ carried a `Problem` the contract classified. Writing its code here is what
595
+ stops the command that reads this file from minting a classification of its
596
+ own for a problem the control plane already named — which is the same rule
597
+ as « never derive an answer the control plane did not give » (article 1),
598
+ applied to a problem rather than to a decision.
599
+
600
+ Written best-effort: a run that could not write it is a run the caller
601
+ reports as one that concluded nothing, which is what an absent file already
602
+ means. Failing here would replace a readable non-answer with a traceback.
603
+ The vocabulary check is not best-effort and is not an `assert`: assertions
604
+ are stripped under `-O`, which this process inherits from its environment,
605
+ and a word outside `OUTCOMES` written there would be read back as nothing
606
+ at all.
607
+ """
608
+ if outcome not in OUTCOMES:
609
+ raise ValueError(f"{outcome!r} is not one of this harness's own endings: {OUTCOMES}")
610
+ said: dict[str, object] = {"outcome": outcome, "detail": detail}
611
+ if problem is not None:
612
+ code = problem.code
613
+ said["problem_code"] = code.value if isinstance(code, ProblemCode) else code.raw
614
+ with suppress(OSError):
615
+ path.write_text(json.dumps(said) + "\n", encoding="utf-8")
616
+
617
+
618
+ def main(argv: Sequence[str] | None = None) -> int:
619
+ """Prove one program, write the report, and leave the verdict to the caller.
620
+
621
+ The exit code here is about this harness: zero once the report is written,
622
+ whatever the report says. The command that spawned it reads the report and
623
+ the outcome file, never this number and never the program's — because « the
624
+ program ended », « the program was governed » and « this harness got as far
625
+ as X » are three different facts and one number cannot carry them.
626
+ """
627
+ forwarded = list(sys.argv[1:] if argv is None else argv)
628
+ try:
629
+ configured, target = _asked(forwarded)
630
+ except HarnessMisuse as misuse:
631
+ # There is no configuration, so there is no path to say this on. The
632
+ # caller reads the absence of the file as « the harness wrote nothing at
633
+ # all », which is exactly what happened.
634
+ sys.stderr.write(f"{misuse}\n")
635
+ return exit_codes.EXIT_MISUSE
636
+ try:
637
+ packs = [manifest.read_pack(path) for path in configured.packs]
638
+ except manifest.PackInvalid as misuse:
639
+ _write_outcome(configured.outcome, INVOCATION_REFUSED, str(misuse))
640
+ sys.stderr.write(f"{misuse}\n")
641
+ return exit_codes.EXIT_MISUSE
642
+ try:
643
+ connection = connect(configured.profile)
644
+ except SocketClientProblem as failure:
645
+ # No question was ever put and no chain was ever read, so there is no
646
+ # report to write: nothing was proven and nothing is claimed.
647
+ _write_outcome(
648
+ configured.outcome,
649
+ CHAIN_UNREADABLE_BEFORE,
650
+ failure.problem.message,
651
+ problem=failure.problem,
652
+ )
653
+ sys.stderr.write(
654
+ f"the chain could not be read, so nothing was verified: {failure.problem.message}\n"
655
+ )
656
+ return exit_codes.EXIT_COULD_NOT_ASK
657
+ try:
658
+ return _prove(configured, packs, target, connection)
659
+ finally:
660
+ connection.close()
661
+
662
+
663
+ def _prove(
664
+ configured: Configuration,
665
+ packs: Sequence[manifest.Pack],
666
+ target: Sequence[str],
667
+ connection: VerifiedConnection,
668
+ ) -> int:
669
+ """Read the chain's head, install the hook, hand the program over, report."""
670
+ watched = [Watched(pack.name, point) for pack in packs for point in pack.points]
671
+ by_event: dict[str, list[Watched]] = {}
672
+ for item in watched:
673
+ by_event.setdefault(item.point.audit_event, []).append(item)
674
+ watch = Watch()
675
+ chain = Chain(connection, configured.profile.scope, cursor=0)
676
+ with watch.ours():
677
+ head = _head_of_the_chain(chain)
678
+ if isinstance(head, Problem):
679
+ _write_outcome(configured.outcome, CHAIN_UNREADABLE_BEFORE, head.message, problem=head)
680
+ sys.stderr.write(f"the chain could not be read, so nothing was verified: {head.message}\n")
681
+ return exit_codes.EXIT_COULD_NOT_ASK
682
+ chain.cursor = head - 1
683
+ # Installed before the hand-off — and before the engine, so an interpreter
684
+ # that could be asked to forget a hook cannot make the proof optional. What
685
+ # draws the line between this harness's work and the program's is the
686
+ # ARMING, which the launcher does at the last instant (`Watch` says why).
687
+ sys.addaudithook(_consulting(chain, by_event, watch))
688
+ ending: int | None = None
689
+ refused_the_invocation = False
690
+ try:
691
+ ending = _run_the_target(configured, packs, target, watch)
692
+ except HarnessMisuse as misuse:
693
+ # Before the hand-off every failure is this invocation's and the program
694
+ # has not started (`launch.py` draws that line): there is nothing to
695
+ # report about a program that never ran, and findings written anyway
696
+ # would say `not-exercised` about a path no program was there to walk.
697
+ refused_the_invocation = True
698
+ _write_outcome(configured.outcome, INVOCATION_REFUSED, str(misuse))
699
+ sys.stderr.write(f"{misuse}\n")
700
+ return exit_codes.EXIT_MISUSE
701
+ except engine.EngineMisuse as misuse:
702
+ # A point naming an attribute a module does not have, discovered inside
703
+ # the program's own import — the one check that cannot be made before
704
+ # the hand-off (`engine.py` says why). The pack is the invocation's and
705
+ # the program is innocent: no question was ever put, so this is the same
706
+ # refusal `instrument run` gives the same mistake, and the command that
707
+ # spawned this reads it off the outcome rather than off a number.
708
+ refused_the_invocation = True
709
+ _write_outcome(configured.outcome, INVOCATION_REFUSED, str(misuse))
710
+ sys.stderr.write(f"{misuse}\n")
711
+ return exit_codes.EXIT_MISUSE
712
+ except ChainUnreadable:
713
+ # The hook aborted an effect it could not verify. It is answered after
714
+ # the `finally`, from the watch rather than from here, so that a target
715
+ # which caught the exception cannot bury it.
716
+ pass
717
+ finally:
718
+ # The program is not done because its main module returned, so the
719
+ # watch stays armed until it is — its own threads run out and its own
720
+ # exit handlers run, under the watch. Only then does consultation stop,
721
+ # and it stops before anything else this process does: the report write
722
+ # and the traceback the interpreter is about to render.
723
+ _wait_for_the_program(chain)
724
+ watch.disarm()
725
+ if not refused_the_invocation and watch.unreadable is None and watch.ever_started:
726
+ with watch.ours():
727
+ _write_report(configured, packs, watched, head, ending, connection)
728
+ if watch.unreadable is not None:
729
+ _write_outcome(
730
+ configured.outcome,
731
+ CHAIN_UNREADABLE_DURING,
732
+ watch.unreadable.message,
733
+ problem=watch.unreadable,
734
+ )
735
+ sys.stderr.write(
736
+ f"the chain could not be read while the program ran, so nothing was verified: "
737
+ f"{watch.unreadable.message}\n"
738
+ )
739
+ return exit_codes.EXIT_COULD_NOT_ASK
740
+ if not watch.ever_started:
741
+ # The one fact that separates « this program walked no such path » from
742
+ # « this proof never began watching ». Without it both rendered as
743
+ # `not-exercised`, byte for byte — so a run that watched nothing at all
744
+ # read exactly like a run that established an absence, which is the
745
+ # distinction the rest of this file is fastidious about (article 2).
746
+ _write_outcome(configured.outcome, GATE_NEVER_OPENED, " ".join(target))
747
+ sys.stderr.write(f"{NEVER_STARTED}\n")
748
+ return exit_codes.EXIT_COULD_NOT_ASK
749
+ # No detail: the only path that reads this one is a report that will not
750
+ # parse, and the report lives in a directory the command removes before the
751
+ # reader ever sees the sentence — a path nobody can open is worse than none.
752
+ _write_outcome(configured.outcome, REPORTED, "")
753
+ return exit_codes.EXIT_ALLOW
754
+
755
+
756
+ def _wait_for_the_program(chain: Chain) -> None:
757
+ """Run out everything the program left to do, before anything is concluded.
758
+
759
+ A main module returning is not a program ending. Two things outlive it and
760
+ both are the program's own code: a non-daemon thread it started, and a
761
+ handler it registered to run at exit. The interpreter runs both — threads
762
+ first, then the exit handlers — after this process would otherwise have
763
+ disarmed the watch and written its findings. Measured before this existed,
764
+ each against a chain holding one record and a program that spawned once
765
+ itself: a thread that spawned 0.6 s later, and a handler that spawned at
766
+ exit, each answered **exit 0 and `governed`** while BOTH processes ran —
767
+ the second with no decision behind it, unjudged, unaborted and unreported.
768
+ That is the false all-clear article 2 forbids, and a worker thread is the
769
+ ordinary shape of the runtimes this chain exists to instrument.
770
+
771
+ So both are run here, in the interpreter's own order, while the watch is
772
+ still armed. **The exit handlers are run by this function rather than left
773
+ to the interpreter**, and the choice is not about ordering among handlers —
774
+ registering this harness's own work first would indeed run it last. It is
775
+ about what a handler needs while it runs: the chain connection, which
776
+ `main` closes on its way out, and a thread the handler may itself start,
777
+ which nothing after the interpreter's handlers would join. Leaving the
778
+ normal path to shutdown would put a consultation after the connection it
779
+ reads through was closed. Doing it here keeps the whole sequence in one
780
+ place a reader can see, and `atexit` clears its own register as it runs, so
781
+ the interpreter's later call finds nothing and no handler runs twice.
782
+
783
+ A handler's own failure is the program's, and `atexit` reports it the way
784
+ the interpreter would and carries on — including the abort this harness
785
+ raises into an ungoverned one, which is why a handler that is stopped does
786
+ not stop the report.
787
+
788
+ Every other handler in the register runs too, a few levels down in the
789
+ standard library's own imports among them. That is not a liberty: the
790
+ interpreter would run every one of them moments later, and nothing this
791
+ process has left to do — writing a file, closing a socket — is anything
792
+ they could take away.
793
+
794
+ Daemon threads are not joined, because the interpreter does not join them
795
+ either — it ends them at shutdown — so waiting for one could wait for ever
796
+ on a thread the program never meant to finish. Waiting is therefore exactly
797
+ as long as the interpreter itself would wait, and no longer; a program that
798
+ hangs a non-daemon thread hangs this run, which is bounded by the command's
799
+ own timeout rather than pretended away here.
800
+
801
+ Threads are enumerated in a loop, and joined again after the handlers,
802
+ because each of those can start another.
803
+
804
+ Then the lock, which a consultation holds while it polls: findings written
805
+ out from under a consultation still in flight would be findings made
806
+ without the answer they were waiting for.
807
+ """
808
+ _join_the_programs_threads()
809
+ # Not guarded against an exception: `atexit` prints what a handler raised
810
+ # and goes on to the next, exactly as the interpreter does, so there is
811
+ # nothing here to catch that it has not already reported.
812
+ atexit._run_exitfuncs()
813
+ _join_the_programs_threads()
814
+ with chain.lock:
815
+ pass
816
+
817
+
818
+ def _join_the_programs_threads() -> None:
819
+ """Join every thread the interpreter itself would wait for, and no other."""
820
+ while True:
821
+ left = [
822
+ thread
823
+ for thread in threading.enumerate()
824
+ if thread is not threading.current_thread() and not thread.daemon and thread.is_alive()
825
+ ]
826
+ if not left:
827
+ break
828
+ for thread in left:
829
+ thread.join()
830
+
831
+
832
+ def _run_the_target(
833
+ configured: Configuration,
834
+ packs: Sequence[manifest.Pack],
835
+ target: Sequence[str],
836
+ watch: Watch,
837
+ ) -> int | None:
838
+ """Hand the program over, in the mode the invocation asked for.
839
+
840
+ With `governed`, exactly what the launcher does, boundary and engine
841
+ included: the verifier then measures the shipped mode. Without it, the same
842
+ hand-off with nothing in front of the program, which is how the chain alone
843
+ is asked whether an effect was decided.
844
+
845
+ Either way the launcher arms the watch at the last instant before the
846
+ program starts, and tells it which files the interpreter reads to start it.
847
+
848
+ `LaunchMisuse` is the invocation's mistake and not a verdict, so it ends
849
+ this harness with no report rather than with an unproven `governed`.
850
+ """
851
+ try:
852
+ if configured.governed:
853
+ return launch.run(
854
+ list(packs),
855
+ configured.profile,
856
+ list(target),
857
+ principal=configured.principal,
858
+ out=sys.stdout,
859
+ err=sys.stderr,
860
+ starting=watch.arm,
861
+ )
862
+ return launch.hand_over(list(target), err=sys.stderr, starting=watch.arm)
863
+ except launch.LaunchMisuse as misuse:
864
+ raise HarnessMisuse(str(misuse)) from misuse
865
+
866
+
867
+ def _head_of_the_chain(chain: Chain) -> int | Problem:
868
+ """The sequence a record written by this run would take, or the problem that it is unknown.
869
+
870
+ The chain is walked to its end once, before the program starts, so that a
871
+ record already there cannot be mistaken for one this run produced. An empty
872
+ chain answers 1, which is the sequence the first entry would be given.
873
+
874
+ The `Problem` and not only its message, because the caller writes its code
875
+ into the outcome file: a read the daemon answered in a generation this
876
+ client does not speak was classified by the transport, and a command that
877
+ was handed that classification and published another of its own would be
878
+ deriving an answer nobody gave it (article 1).
879
+ """
880
+ from_sequence = 1
881
+ highest = 0
882
+ while True:
883
+ # Bound as a default so the read is of THIS page and not of whatever
884
+ # the walk moved on to: the callable outlives one turn of the loop.
885
+ result = reads.read(lambda at=from_sequence: _one_page(chain, at))
886
+ if not isinstance(result, Answered):
887
+ return result.problem
888
+ entries, _, next_from = result.value
889
+ for entry in entries:
890
+ sequence = entry["sequence"]
891
+ if isinstance(sequence, int) and sequence > highest:
892
+ highest = sequence
893
+ if next_from is None:
894
+ return highest + 1
895
+ from_sequence = next_from
896
+
897
+
898
+ def _one_page(
899
+ chain: Chain, from_sequence: int
900
+ ) -> Result[tuple[list[Mapping[str, object]], Mapping[str, object], int | None]]:
901
+ """Re-open the connection, verify the far end again, and read one page.
902
+
903
+ Rule C4: the transport re-opens no address on a caller's behalf, and the
904
+ daemon closes the connection after some answers. Re-verifying before every
905
+ read keeps that explicit — it is cheap over a Unix socket — and it puts the
906
+ re-open inside the reader, so a socket that went away mid-run is a
907
+ transport problem this client classifies rather than an exception raised
908
+ out of an audit hook.
909
+ """
910
+ chain.connection.reconnect()
911
+ result = chain.connection.read_evidence(chain.scope, from_sequence, PAGE_SIZE)
912
+ if not isinstance(result, Answered):
913
+ return result
914
+ return Answered(pages.members(result.value, from_sequence), result.contract_generation)
915
+
916
+
917
+ def _consulting(
918
+ chain: Chain, by_event: Mapping[str, list[Watched]], watch: Watch
919
+ ) -> Callable[[str, tuple[object, ...]], None]:
920
+ """The audit hook: for an event a pack named, ask the chain and act on the answer.
921
+
922
+ Every other event returns on a dictionary lookup, which is what a hook
923
+ installed for the length of somebody's program has to cost.
924
+
925
+ A point the chain cannot account for raises, and the raise is the whole
926
+ mechanism: it aborts the effect inside the target rather than reporting
927
+ afterwards that an ungoverned effect had already happened. A point whose
928
+ chain could not be read raises too, and for the same reason — but with a
929
+ different exception and no finding, because that is not the same fact.
930
+
931
+ Whose act an event was is `Watch`'s question, asked first and answered
932
+ there; this function judges only what is left — and COUNTS what is neither
933
+ judged nor the hand-off's, which is the one thing it may not drop
934
+ (`Watch.whose` says what that shape is and what dropping it cost).
935
+ """
936
+
937
+ def consult(event: str, arguments: tuple[object, ...]) -> None:
938
+ if not watch.judging:
939
+ # Either nothing has been handed over yet, or the hand-off is still
940
+ # locating and reading the program. Both are the watch's question
941
+ # and not this function's; the second is also where the gate opens.
942
+ watch.opening(arguments)
943
+ return
944
+ points = by_event.get(event)
945
+ if points is None:
946
+ return
947
+ whose = watch.whose(arguments)
948
+ if whose == THE_HAND_OFFS:
949
+ return
950
+ if whose == NOT_JUDGED:
951
+ # No chain read and no raise: nothing was established about this
952
+ # event, and inventing either answer would be a verdict nobody
953
+ # measured. It is counted on every point the event would have been
954
+ # judged against, which is what makes the run not a pass.
955
+ for item in points:
956
+ item.unjudged += 1
957
+ return
958
+ with watch.ours():
959
+ for item in points:
960
+ _judge(chain, item, watch)
961
+
962
+ return consult
963
+
964
+
965
+ def _judge(chain: Chain, item: Watched, watch: Watch) -> None:
966
+ """One point, against one consultation of the chain."""
967
+ consultation = _consulted(chain, item.point.capability)
968
+ if consultation.matched is not None:
969
+ item.events += 1
970
+ return
971
+ if not consultation.read_something:
972
+ problem = consultation.problem
973
+ watch.unreadable = problem
974
+ raise ChainUnreadable(
975
+ f"the chain could not be read, so this effect is not verified: "
976
+ f"{item.point.capability} ({item.point.audit_event}): "
977
+ f"{'no answer at all' if problem is None else problem.message}"
978
+ )
979
+ if consultation.problem is not None:
980
+ # Some reads were answered and the last was not. The verdict below is
981
+ # made of what WAS read, and the failure is said beside it rather than
982
+ # folded into it: a reader has to be able to see that the chain went
983
+ # away while this run was concluding.
984
+ sys.stderr.write(
985
+ f"the chain was read and then stopped answering: {consultation.problem.message}\n"
986
+ )
987
+ item.refused = True
988
+ raise RuntimeError(f"{UNGOVERNED} effect: {item.point.capability} ({item.point.audit_event})")
989
+
990
+
991
+ def _consulted(chain: Chain, capability: str) -> Consultation:
992
+ """Poll the chain for a record this effect has not already been answered by.
993
+
994
+ Polled, because article 10 writes the chain asynchronously: the record of
995
+ an effect the plane allowed may arrive after the effect was allowed to
996
+ happen.
997
+
998
+ Every read that was not answered is remembered, and so is whether ANY was.
999
+ A consultation that read nothing at all across the whole patience has
1000
+ established nothing about the program — not even an absence — which is why
1001
+ that is a third outcome here and not a `False`.
1002
+ """
1003
+ deadline = time.monotonic() + CHAIN_WAIT
1004
+ read_something = False
1005
+ problem: Problem | None = None
1006
+ with chain.lock:
1007
+ while True:
1008
+ found, answered, failure = _matching(chain, capability)
1009
+ read_something = read_something or answered
1010
+ if failure is not None:
1011
+ problem = failure
1012
+ if found is not None:
1013
+ chain.cursor = found
1014
+ return Consultation(found, True, None)
1015
+ if time.monotonic() >= deadline:
1016
+ return Consultation(None, read_something, problem)
1017
+ time.sleep(POLL_INTERVAL)
1018
+
1019
+
1020
+ def _matching(chain: Chain, capability: str) -> tuple[int | None, bool, Problem | None]:
1021
+ """One walk: the sequence matched, whether any page was answered, and the problem.
1022
+
1023
+ Nothing is inferred from a read that was not answered — not here and not in
1024
+ the poll above, which is what carries « no read succeeded » out to the
1025
+ caller instead of letting it look like « no record exists ».
1026
+ """
1027
+ answered = False
1028
+ from_sequence = chain.cursor + 1
1029
+ while True:
1030
+ result = reads.read(lambda at=from_sequence: _one_page(chain, at))
1031
+ if not isinstance(result, Answered):
1032
+ return None, answered, result.problem
1033
+ answered = True
1034
+ entries, _, next_from = result.value
1035
+ for entry in entries:
1036
+ sequence = entry.get("sequence")
1037
+ body = entry.get("body")
1038
+ if (
1039
+ isinstance(sequence, int)
1040
+ and sequence > chain.cursor
1041
+ and entry.get("kind") == EFFECT
1042
+ and isinstance(body, Mapping)
1043
+ and body.get("capability") == capability
1044
+ and body.get("outcome") == ALLOW
1045
+ ):
1046
+ return sequence, True, None
1047
+ if next_from is None:
1048
+ return None, answered, None
1049
+ from_sequence = next_from
1050
+
1051
+
1052
+ def _write_report(
1053
+ configured: Configuration,
1054
+ packs: Sequence[manifest.Pack],
1055
+ watched: Sequence[Watched],
1056
+ start_sequence: int,
1057
+ target_exit: int | None,
1058
+ connection: VerifiedConnection,
1059
+ ) -> None:
1060
+ """Write what this run observed, whatever ended it.
1061
+
1062
+ Written from a `finally`, because the interesting run is the one the target
1063
+ did not survive: an effect the hook aborted kills the program, and a proof
1064
+ that lost its own findings to the failure it caused would prove nothing.
1065
+
1066
+ `inspected` is read off the points that were actually watched for rather
1067
+ than off the command line. Article 9 asks the public gate to fail unless
1068
+ the verifier inspected every shipped pack, and a set copied from the
1069
+ invocation would satisfy that assertion without having watched anything.
1070
+ """
1071
+ inspected = sorted({item.pack for item in watched})
1072
+ document = {
1073
+ "packs": [pack.name for pack in packs],
1074
+ "inspected": inspected,
1075
+ "points": [item.to_document() for item in watched],
1076
+ "target_exit": target_exit,
1077
+ "start_sequence": start_sequence,
1078
+ "verification": render.verification_document(
1079
+ connection.server_credential.uid, connection.expected_uid, connection.verified
1080
+ ),
1081
+ }
1082
+ configured.report.write_text(
1083
+ json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8"
1084
+ )
1085
+
1086
+
1087
+ def _asked(forwarded: Sequence[str]) -> tuple[Configuration, list[str]]:
1088
+ """The configuration and the program, or the misuse that there is neither."""
1089
+ argv = list(forwarded)
1090
+ if not argv:
1091
+ raise HarnessMisuse(
1092
+ f"this harness is run as `<configuration> {SEPARATOR} <target…>`, and was given nothing"
1093
+ )
1094
+ if SEPARATOR not in argv[1:]:
1095
+ raise HarnessMisuse(
1096
+ f"this harness was given no `{SEPARATOR}`: the program to prove follows it"
1097
+ )
1098
+ at = argv.index(SEPARATOR, 1)
1099
+ return _configured(Path(argv[0])), argv[at + 1 :]
1100
+
1101
+
1102
+ def _configured(path: Path) -> Configuration:
1103
+ """Read the configuration, naming the member and the rule on every refusal."""
1104
+ try:
1105
+ document = json.loads(path.read_text(encoding="utf-8"))
1106
+ except (OSError, UnicodeDecodeError, ValueError, RecursionError) as unreadable:
1107
+ raise HarnessMisuse(
1108
+ f"{path} does not read as a configuration: {unreadable}"
1109
+ ) from unreadable
1110
+ if not isinstance(document, Mapping):
1111
+ raise HarnessMisuse(f"{path} is not a configuration: it declares no members")
1112
+ declared = {member: None for member in CONFIGURED} | dict(document)
1113
+ packs = declared["packs"]
1114
+ if not isinstance(packs, list) or not packs or not all(isinstance(it, str) for it in packs):
1115
+ raise HarnessMisuse(
1116
+ f"packs is {packs!r}: a verification names the pack directories it watches for, "
1117
+ f"as a non-empty list of paths — one that watched for nothing would be the "
1118
+ f"vacuous proof article 9 forbids"
1119
+ )
1120
+ for member in ("socket", "scope", "mode"):
1121
+ if not isinstance(declared[member], str) or not declared[member]:
1122
+ raise HarnessMisuse(f"{member} is {declared[member]!r}: it names a non-empty string")
1123
+ for member in ("daemon_user", "principal"):
1124
+ if declared[member] is not None and not isinstance(declared[member], str):
1125
+ raise HarnessMisuse(f"{member} is {declared[member]!r}: it is a string or absent")
1126
+ if not isinstance(declared["governed"], bool):
1127
+ raise HarnessMisuse(
1128
+ f"governed is {declared['governed']!r}: it says whether the program is handed over "
1129
+ f"with the boundary in front of it, as true or false and never as an absence"
1130
+ )
1131
+ if not isinstance(declared["report"], str) or not declared["report"]:
1132
+ raise HarnessMisuse(
1133
+ f"report is {declared['report']!r}: it names the path this run's findings are "
1134
+ f"written to, and a run that wrote them nowhere would prove nothing"
1135
+ )
1136
+ if not isinstance(declared[OUTCOME_FILE_MEMBER], str) or not declared[OUTCOME_FILE_MEMBER]:
1137
+ raise HarnessMisuse(
1138
+ f"{OUTCOME_FILE_MEMBER} is {declared[OUTCOME_FILE_MEMBER]!r}: it names the path "
1139
+ f"this run says its own ending on, and a run that said it nowhere would leave "
1140
+ f"the command reading the target's exit status for it"
1141
+ )
1142
+ try:
1143
+ profile = SocketProfile(
1144
+ declared["socket"],
1145
+ mode=declared["mode"],
1146
+ daemon_user=declared["daemon_user"],
1147
+ scope=declared["scope"],
1148
+ )
1149
+ except ProfileMisuse as invalid:
1150
+ raise HarnessMisuse(str(invalid)) from invalid
1151
+ return Configuration(
1152
+ packs=tuple(Path(it) for it in packs),
1153
+ profile=profile,
1154
+ principal=declared["principal"],
1155
+ governed=declared["governed"],
1156
+ report=Path(declared["report"]),
1157
+ outcome=Path(declared[OUTCOME_FILE_MEMBER]),
1158
+ )
1159
+
1160
+
1161
+ if __name__ == "__main__": # pragma: no cover - exercised as a subprocess
1162
+ raise SystemExit(main())