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,88 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """One process exit code per thing that can come back, all of them distinct.
3
+
4
+ Article 1 names this file's guard: "The project's client renders 'denied' and
5
+ 'could not ask' as distinct results with distinct exit codes, and a test holds
6
+ them apart." Article 2 is why the list is longer than two: an answer, a refusal
7
+ of the question and an unanswerable question are three different facts, and a
8
+ shell that collapses them into "non-zero" has lost the one distinction the
9
+ constitution insists on.
10
+
11
+ Three of these codes are not this repository's to choose. `sayfirst whoami`,
12
+ which the contract distribution implements, already publishes `0`, `3`, `4` and
13
+ `64` for the same four situations; a second client of the same contract that
14
+ numbered them differently would make the same event read two ways depending on
15
+ which subcommand produced it. So they are taken as given; the codes for the
16
+ three outcomes and the local checks are added here.
17
+
18
+ Article 1 also keeps a local check apart from an answer. A broken chain or a
19
+ contradicted verdict is something this client found, not a denial the plane
20
+ gave. A check that cannot conclude is not a failure to ask the plane either:
21
+ the record may have arrived, while its version or coverage prevents a check.
22
+
23
+ `2` is absent on purpose: `argparse` exits with it on a usage error it rejects
24
+ itself, before any of this runs. Claiming it for an outcome would make a
25
+ mistyped flag indistinguishable from an answer.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import Final
31
+
32
+ #: The control plane answered `allow`. The only code that means "go ahead".
33
+ EXIT_ALLOW: Final[int] = 0
34
+
35
+ #: The control plane answered `deny`. An answer, not a failure to obtain one.
36
+ EXIT_DENY: Final[int] = 1
37
+
38
+ #: The request was refused — the question was received and rejected.
39
+ #: The value `sayfirst whoami` publishes for the same situation.
40
+ EXIT_REFUSED: Final[int] = 3
41
+
42
+ #: The control plane could not be asked, or answered something this generation
43
+ #: cannot read. Never rendered as a denial (articles 1 and 2). The value
44
+ #: `sayfirst whoami` publishes for the same situation.
45
+ EXIT_COULD_NOT_ASK: Final[int] = 4
46
+
47
+ #: The control plane answered `suspend`: the effect waits for a person.
48
+ EXIT_SUSPEND: Final[int] = 5
49
+
50
+ # A successful read exits 0 without another zero-valued name in CODES: a read's
51
+ # success is not an outcome; the record's own outcome is data, not permission.
52
+ #: A local verification found what the plane did not say: a broken chain, an
53
+ #: undeclared gap, a manifest that does not recompute, or a served verdict the
54
+ #: local check contradicts. A finding, never a denial (article 1).
55
+ EXIT_CHECK_FAILED: Final[int] = 6
56
+
57
+ #: The local check could not conclude: an unknown preimage or manifest version,
58
+ #: or insufficient coverage. An unknown check, not an unanswerable question.
59
+ EXIT_COULD_NOT_CHECK: Final[int] = 7
60
+
61
+ #: The invocation was wrong, or something it named cannot be read — a profile
62
+ #: that cannot say what it must verify, a pack manifest that does not parse, a
63
+ #: pack this distribution ships that will not read. The value `sayfirst whoami`
64
+ #: publishes for the same situation; `packs list` uses it for a broken shipped
65
+ #: pack rather than minting a code for a case no caller can act on differently,
66
+ #: because a distinct number would cost one in a table three commands share and
67
+ #: the paragraph above says why the numbers here are not this repository's alone
68
+ #: to choose. « The invocation was wrong » is a stretch for a pack the
69
+ #: DISTRIBUTION ships, and it is the honest half of the answer: the caller typed
70
+ #: nothing wrong, and something the command named cannot be read.
71
+ EXIT_MISUSE: Final[int] = 64
72
+
73
+ #: The code `argparse` uses for a usage error it rejects itself. Reserved
74
+ #: rather than assigned, so that nothing here can collide with it.
75
+ EXIT_PARSER_USAGE: Final[int] = 2
76
+
77
+ #: Every code this client can exit with, by the name of what it means.
78
+ CODES: Final[dict[str, int]] = {
79
+ "allow": EXIT_ALLOW,
80
+ "deny": EXIT_DENY,
81
+ "refused": EXIT_REFUSED,
82
+ "could_not_ask": EXIT_COULD_NOT_ASK,
83
+ "suspend": EXIT_SUSPEND,
84
+ "check_failed": EXIT_CHECK_FAILED,
85
+ "could_not_check": EXIT_COULD_NOT_CHECK,
86
+ "misuse": EXIT_MISUSE,
87
+ "parser_usage": EXIT_PARSER_USAGE,
88
+ }
@@ -0,0 +1,35 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Read a decision's reason, rule and policy version in the plane's own words."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ from collections.abc import Sequence
9
+ from typing import TextIO
10
+
11
+ from . import reads, render
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ parser = argparse.ArgumentParser(
16
+ prog="sayfirst explain", description="Read the record and reason of a decision."
17
+ )
18
+ reads.add_connection_arguments(parser)
19
+ parser.add_argument("--decision", required=True, help="the reference of the recorded decision")
20
+ return parser
21
+
22
+
23
+ def main(
24
+ argv: Sequence[str] | None = None, *, out: TextIO | None = None, err: TextIO | None = None
25
+ ) -> int:
26
+ stdout, stderr = out or sys.stdout, err or sys.stderr
27
+ arguments = build_parser().parse_args(argv)
28
+ connection = reads.open_connection(arguments, stderr)
29
+ if isinstance(connection, int):
30
+ return connection
31
+ try:
32
+ result = reads.read(lambda: connection.read_decision(arguments.scope, arguments.decision))
33
+ return reads.finish(result, arguments, connection, stdout, stderr, render.write_record)
34
+ finally:
35
+ connection.close()
@@ -0,0 +1,14 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The instrumentation chain's client side: the manifest, the engine, the verifier.
3
+
4
+ Article 9 says the open project ships the instrumentation engine, the
5
+ instrumentation verifier and the convenience packs. Two of the three live here.
6
+
7
+ Layer 1 is `manifest`, `engine` and `launch`: they read what a pack declares,
8
+ install it in front of the named operations, and hand the program over. Layer 3
9
+ is `verify` and `harness`: they run the program again under the interpreter's
10
+ own audit hook and ask the daemon's chain whether every effect of a named kind
11
+ was decided. The two do not meet — the verifier consumes audit events and
12
+ evidence and nothing the engine kept — because a proof that trusts the thing it
13
+ is proving is not a proof.
14
+ """
@@ -0,0 +1,191 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`sayfirst instrument`: run somebody's program with the boundary in front of it.
3
+
4
+ Three verbs, because one word cannot carry two meanings. Article 9 writes
5
+ « `sayfirst instrument apply` executes it » while also making runtime
6
+ interposition the primary mode and a committed code modification a later option
7
+ — and a verb that undoes itself when the process ends is not the same act as a
8
+ verb that edits somebody's repository. `apply` is the ordinary name for the
9
+ second, so it keeps the article's name for the article's later option and says,
10
+ in as many words, that it is not that option yet (the architecture reading of
11
+ 2026-09-14). Nothing is renamed into meaning its opposite, and a reader who
12
+ disagrees has an amendment to write rather than a redefinition to find in code.
13
+
14
+ The codes this command produces come from two places and never a third: the
15
+ invocation's own mistakes, which are this client's to report, and the governed
16
+ program's ending, which is the program's and is passed through untouched.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import sys
23
+ from collections.abc import Sequence
24
+ from pathlib import Path
25
+ from typing import Final, TextIO
26
+
27
+ from sayfirst_contract.client import Refused
28
+ from sayfirst_contract.generation import CONTRACT_GENERATION
29
+ from sayfirst_contract.transport.socket_client import (
30
+ PER_USER,
31
+ SYSTEM,
32
+ ProfileMisuse,
33
+ SocketClientProblem,
34
+ SocketProfile,
35
+ )
36
+
37
+ from .. import exit_codes, reads, render
38
+ from . import engine, launch, manifest, verify
39
+
40
+ #: What `apply` says instead of doing anything. The sentence names the mode that
41
+ #: does exist, because a refusal that leaves the reader with no next step is
42
+ #: only half of what article 2 asks of an absence.
43
+ APPLY_REFUSAL: Final[str] = (
44
+ "instrument apply is reserved for the committed code modification, which does not "
45
+ "exist yet; use `instrument run`, the reversible mode (architecture reading, 2026-09-14)"
46
+ )
47
+
48
+ #: The verbs that do nothing, and the sentence each says instead. They are
49
+ #: answered before the parser reads their arguments, because they refuse the
50
+ #: ACT: a usage error about an option would suggest that some other spelling of
51
+ #: the same act would be accepted, and none would. `verify` left this table
52
+ #: when the verifier arrived; `apply` stays until the committed code
53
+ #: modification does.
54
+ RESERVED: Final[dict[str, str]] = {"apply": APPLY_REFUSAL}
55
+
56
+
57
+ def build_parser() -> argparse.ArgumentParser:
58
+ """Every verb this command answers, and only the ones it answers."""
59
+ parser = argparse.ArgumentParser(
60
+ prog="sayfirst instrument",
61
+ description="Run a program with the sayfirst boundary in front of named effects.",
62
+ )
63
+ verbs = parser.add_subparsers(dest="verb", required=True)
64
+ running = verbs.add_parser("run", help="run a program with the designated packs installed")
65
+ running.add_argument(
66
+ "--pack",
67
+ action="append",
68
+ required=True,
69
+ metavar="DIR",
70
+ help="a pack directory; repeat the option for each pack",
71
+ )
72
+ running.add_argument("--scope", required=True, help="the scope the questions are asked in")
73
+ running.add_argument("--socket", required=True, help="the path of the daemon's socket")
74
+ running.add_argument("--mode", choices=(PER_USER, SYSTEM), default=PER_USER)
75
+ running.add_argument(
76
+ "--daemon-user",
77
+ default=None,
78
+ help="the account the daemon runs as; a system profile names it",
79
+ )
80
+ running.add_argument(
81
+ "--principal",
82
+ default=None,
83
+ metavar="REF",
84
+ help="the principal reference to hold grants against; this account by default",
85
+ )
86
+ running.add_argument(
87
+ "target",
88
+ nargs="*",
89
+ metavar="TARGET",
90
+ help="after `--`: either -m MODULE [args] or SCRIPT [args]",
91
+ )
92
+ verify.add_arguments(
93
+ verbs.add_parser("verify", help="prove every named effect of a program was decided")
94
+ )
95
+ # Declared so that `--help` names every verb this command answers, article 2's
96
+ # rule about a help text being a claim. `main` answers this one before the
97
+ # parser is asked to read anything after it.
98
+ verbs.add_parser("apply", help="the committed code modification (reserved; it refuses)")
99
+ return parser
100
+
101
+
102
+ def main(
103
+ argv: Sequence[str] | None = None, *, out: TextIO | None = None, err: TextIO | None = None
104
+ ) -> int:
105
+ """Run the verb and return the code the calling shell should see."""
106
+ stdout = out or sys.stdout
107
+ stderr = err or sys.stderr
108
+ forwarded = list(sys.argv[1:] if argv is None else argv)
109
+ if forwarded and forwarded[0] in RESERVED:
110
+ stderr.write(f"{RESERVED[forwarded[0]]}\n")
111
+ return exit_codes.EXIT_MISUSE
112
+ return _run(build_parser().parse_args(forwarded), stdout, stderr)
113
+
114
+
115
+ def _run(arguments: argparse.Namespace, stdout: TextIO, stderr: TextIO) -> int:
116
+ """Read the packs, open the profile, and hand the program over.
117
+
118
+ Everything that can be wrong with the invocation is found before a single
119
+ attribute is replaced — with the one exception the engine names: an
120
+ attribute a pack declares on a module the program only imports later
121
+ cannot be checked before that import, and is refused when it happens. A
122
+ program half instrumented would be running partly governed with nothing
123
+ saying which part, which is worse than not running.
124
+ """
125
+ if arguments.verb == "verify":
126
+ # Layer 3 owns its own codes: they are about the proof, never about the
127
+ # program's ending, which `run` below is the one that passes through.
128
+ return verify.run(arguments, stdout, stderr)
129
+ packs: list[manifest.Pack] = []
130
+ for named in arguments.pack:
131
+ try:
132
+ packs.append(manifest.read_pack(Path(named)))
133
+ except manifest.PackInvalid as invalid:
134
+ # The path as it was TYPED, not as it was resolved: with more than
135
+ # one `--pack` the sentence alone does not say which one to fix, and
136
+ # a resolved path is not what the reader has in their shell history.
137
+ stderr.write(f"{named}: {invalid}\n")
138
+ return exit_codes.EXIT_MISUSE
139
+ try:
140
+ profile = SocketProfile(
141
+ arguments.socket,
142
+ mode=arguments.mode,
143
+ daemon_user=arguments.daemon_user,
144
+ scope=arguments.scope,
145
+ )
146
+ except ProfileMisuse as invalid:
147
+ stderr.write(f"{invalid}\n")
148
+ return exit_codes.EXIT_MISUSE
149
+ try:
150
+ return launch.run(
151
+ packs,
152
+ profile,
153
+ arguments.target,
154
+ principal=arguments.principal,
155
+ out=stdout,
156
+ err=stderr,
157
+ )
158
+ except launch.LaunchMisuse as misuse:
159
+ # Everything the launcher refuses BEFORE the hand-off: a point the
160
+ # engine will not install, an execution module that will not load, a
161
+ # target that names no program. The program has not started, so this is
162
+ # the invocation's mistake and not an outcome about an effect.
163
+ stderr.write(f"{misuse}\n")
164
+ return exit_codes.EXIT_MISUSE
165
+ except engine.EngineMisuse as misuse:
166
+ # A point naming an attribute a module does not have, discovered inside
167
+ # the program's own import — the one check that cannot be made before
168
+ # the hand-off (the engine says why). The pack is the invocation's and
169
+ # the program is innocent: no question was ever put, so it is 64, the
170
+ # same answer this file already gives the byte-identical shape of a
171
+ # `-m` name that resolves to a package with no `__main__`. Uncaught, it
172
+ # reached a shell as a traceback and exit 1, this client's published
173
+ # code for « deny », over a mistake the plane was never asked about.
174
+ stderr.write(f"{misuse}\n")
175
+ return exit_codes.EXIT_MISUSE
176
+ except SocketClientProblem as failure:
177
+ # Raised while the connection was being arranged, so no question was
178
+ # ever put. It is reported with the contract's own classification and
179
+ # never as a denial (articles 1 and 2); a refusal the boundary raises
180
+ # later, inside the program, is the program's and is not caught here.
181
+ return _write_problem(failure, stderr)
182
+
183
+
184
+ def _write_problem(failure: SocketClientProblem, stderr: TextIO) -> int:
185
+ """A non-answer, said as the reads say it, with the code the reads use."""
186
+ result = reads.connection_problem(failure)
187
+ could_not_ask = not isinstance(result, Refused)
188
+ render.write_problem(
189
+ result.problem.to_document(CONTRACT_GENERATION), stderr, could_not_ask=could_not_ask
190
+ )
191
+ return exit_codes.EXIT_COULD_NOT_ASK if could_not_ask else exit_codes.EXIT_REFUSED