aics-cli 0.1.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.
- aics_cli/__init__.py +8 -0
- aics_cli/__main__.py +3 -0
- aics_cli/bundle_build.py +216 -0
- aics_cli/card.py +933 -0
- aics_cli/checkpoints.py +498 -0
- aics_cli/cli.py +298 -0
- aics_cli/commands.py +1656 -0
- aics_cli/consistency.py +280 -0
- aics_cli/dnstxt.py +319 -0
- aics_cli/encoding.py +125 -0
- aics_cli/errors.py +57 -0
- aics_cli/evidence.py +88 -0
- aics_cli/home.py +112 -0
- aics_cli/keystore.py +189 -0
- aics_cli/message.py +875 -0
- aics_cli/net.py +117 -0
- aics_cli/policy.py +618 -0
- aics_cli/published_keys.py +34 -0
- aics_cli/tiles_http.py +59 -0
- aics_cli/trust.py +243 -0
- aics_cli-0.1.0.dist-info/METADATA +420 -0
- aics_cli-0.1.0.dist-info/RECORD +25 -0
- aics_cli-0.1.0.dist-info/WHEEL +4 -0
- aics_cli-0.1.0.dist-info/entry_points.txt +3 -0
- aics_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
aics_cli/cli.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Command line entry point: ``aics``.
|
|
2
|
+
|
|
3
|
+
Every command accepts ``--json`` (one JSON object on stdout, whatever
|
|
4
|
+
happens) and exits 0 (verified), 1 (error, inconclusive, stale or
|
|
5
|
+
unproven), 2 (verification failed - the output carries the evidence) or 3
|
|
6
|
+
(needs a server feature that does not exist yet).
|
|
7
|
+
|
|
8
|
+
Text output passes through :func:`~aics_cli.encoding.printable_line`, so no
|
|
9
|
+
string from a server can put terminal escape sequences into it.
|
|
10
|
+
|
|
11
|
+
A value that starts with ``-`` (a base64url signature, a session id) must be
|
|
12
|
+
written with ``=``: ``--sig=-AbC...``, ``--session=-x1``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any, Sequence, TextIO
|
|
22
|
+
|
|
23
|
+
from . import __version__
|
|
24
|
+
from . import bundle_build as BB
|
|
25
|
+
from . import commands as C
|
|
26
|
+
from .checkpoints import DEFAULT_MAX_SKEW
|
|
27
|
+
from .encoding import printable_line
|
|
28
|
+
from .errors import EXIT_ERROR, AicsError, UsageError, VerificationFailed
|
|
29
|
+
from .net import HttpFetcher
|
|
30
|
+
|
|
31
|
+
DASH_HINT = ("; a value that starts with '-' must be written with '=', e.g. --sig=-AbC... or --session=-x1 "
|
|
32
|
+
"(or pass the signature with --sig-file FILE, '-' for stdin)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _Parser(argparse.ArgumentParser):
|
|
36
|
+
def error(self, message: str) -> None: # argparse would exit 2 ("evidence" here)
|
|
37
|
+
if "expected one argument" in message:
|
|
38
|
+
message += DASH_HINT
|
|
39
|
+
raise UsageError(f"{self.prog}: {message}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _common() -> argparse.ArgumentParser:
|
|
43
|
+
p = argparse.ArgumentParser(add_help=False)
|
|
44
|
+
p.add_argument("--json", action="store_true", default=argparse.SUPPRESS,
|
|
45
|
+
help="print one JSON object instead of text")
|
|
46
|
+
p.add_argument("--home", default=argparse.SUPPRESS, metavar="DIR",
|
|
47
|
+
help="state directory (default: $AICS_HOME or ~/.aics)")
|
|
48
|
+
return p
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_parser() -> _Parser:
|
|
52
|
+
common = _common()
|
|
53
|
+
parser = _Parser(
|
|
54
|
+
prog="aics",
|
|
55
|
+
description="Verify the AICS log, assurance card and keys yourself. Keys are pinned out of band "
|
|
56
|
+
"(DNS TXT, the PyPI package, the README), never taken from the server being checked.",
|
|
57
|
+
epilog="exit codes: 0 verified, 1 error/inconclusive/stale/unproven, 2 verification failed (evidence "
|
|
58
|
+
"printed), 3 not available until the server ships it",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument("--version", action="version", version=f"aics {__version__}")
|
|
61
|
+
parser.add_argument("--json", action="store_true", default=False, help="print one JSON object")
|
|
62
|
+
parser.add_argument("--home", default=None, metavar="DIR", help="state directory (default ~/.aics)")
|
|
63
|
+
parser.add_argument("--timeout", type=float, default=15.0, help="network timeout in seconds")
|
|
64
|
+
sub = parser.add_subparsers(dest="command", metavar="COMMAND", parser_class=_Parser)
|
|
65
|
+
sub.required = True
|
|
66
|
+
|
|
67
|
+
# keys -------------------------------------------------------------
|
|
68
|
+
keys = sub.add_parser("keys", parents=[common], help="make or show your own Ed25519 + X25519 keys")
|
|
69
|
+
ksub = keys.add_subparsers(dest="keys_cmd", metavar="new|show", parser_class=_Parser)
|
|
70
|
+
ksub.required = True
|
|
71
|
+
for name, fn, hlp in (("new", C.cmd_keys_new, "generate keys (files 0600) and print the public forms"),
|
|
72
|
+
("show", C.cmd_keys_show, "print the public forms of your keys")):
|
|
73
|
+
kp = ksub.add_parser(name, parents=[common], help=hlp)
|
|
74
|
+
kp.add_argument("--dir", default=None, help="key directory (default: the state directory)")
|
|
75
|
+
kp.set_defaults(func=fn, cmd_name=f"keys {name}")
|
|
76
|
+
|
|
77
|
+
# line signatures ---------------------------------------------------
|
|
78
|
+
sl = sub.add_parser("sign-line", parents=[common],
|
|
79
|
+
help='sign a gate command line: prints "sig <base64>" over "<session>\\n<line>"')
|
|
80
|
+
sl.add_argument("--session", required=True, help="session id the gate printed (write --session=ID if it "
|
|
81
|
+
"starts with '-')")
|
|
82
|
+
sl.add_argument("--line", required=True, help="the exact command line to sign (--line=... if it starts with '-')")
|
|
83
|
+
sl.add_argument("--dir", default=None, help="key directory (default: the state directory)")
|
|
84
|
+
sl.add_argument("--key", default=None, help="private key file (PKCS#8 PEM from openssl, or OpenSSH)")
|
|
85
|
+
sl.set_defaults(func=C.cmd_sign_line, cmd_name="sign-line")
|
|
86
|
+
|
|
87
|
+
vl = sub.add_parser("verify-line", parents=[common], help="verify a gate line signature")
|
|
88
|
+
vl.add_argument("--session", required=True, help="session id (write --session=ID if it starts with '-')")
|
|
89
|
+
vl.add_argument("--line", required=True, help="the signed line (--line=... if it starts with '-')")
|
|
90
|
+
vl.add_argument("--sig", default=None, help='"sig <base64>", "sig: <base64>" or the bare base64; write '
|
|
91
|
+
"--sig=SIG when it starts with '-' (base64url can)")
|
|
92
|
+
vl.add_argument("--sig-file", default=None, metavar="FILE",
|
|
93
|
+
help="read the signature (or an SSHSIG block) from FILE, '-' for stdin")
|
|
94
|
+
vl.add_argument("--pub", default=None, help='"pub <base64>", base64, ssh-ed25519 line or did:key (default: yours)')
|
|
95
|
+
vl.add_argument("--dir", default=None)
|
|
96
|
+
vl.add_argument("--key", default=None)
|
|
97
|
+
strictness = vl.add_mutually_exclusive_group()
|
|
98
|
+
strictness.add_argument("--leeway", action="store_true",
|
|
99
|
+
help="also accept a signature over the bytes plus a final newline, or over the line "
|
|
100
|
+
"stripped of spaces, tabs and CR (files written outside the protocol; never for "
|
|
101
|
+
"a protocol write, AICS-1 §11.3)")
|
|
102
|
+
strictness.add_argument("--strict", action="store_true",
|
|
103
|
+
help="accept only the exact signed bytes: the default, kept for scripts that pass it")
|
|
104
|
+
vl.set_defaults(func=C.cmd_verify_line, cmd_name="verify-line")
|
|
105
|
+
|
|
106
|
+
# policy ------------------------------------------------------------
|
|
107
|
+
pol = sub.add_parser("policy", parents=[common], help="pin, show or cross-check the out-of-band keys")
|
|
108
|
+
psub = pol.add_subparsers(dest="policy_cmd", metavar="init|show|diff", parser_class=_Parser)
|
|
109
|
+
psub.required = True
|
|
110
|
+
pi = psub.add_parser("init", parents=[common],
|
|
111
|
+
help="pin the trust bundle from DNS TXT _aics.<domain> (never from the API)")
|
|
112
|
+
pi.add_argument("--from-dns", metavar="DOMAIN", default=None, help="read the trust bundle at TXT _aics.DOMAIN")
|
|
113
|
+
pi.add_argument("--nameserver", default=os.environ.get("AICS_NAMESERVER"), help="ip[:port] of a DNS resolver")
|
|
114
|
+
pi.add_argument("--bundle", default=None,
|
|
115
|
+
help="the trust bundle from the public README: aics1=<base64url>, its JSON, or a file")
|
|
116
|
+
pi.add_argument("--from-api", metavar="URL", default=None,
|
|
117
|
+
help="also compare with what the API host claims (never pinned from it alone)")
|
|
118
|
+
pi.add_argument("--domain", default=None, help="domain to record when no --from-dns is given")
|
|
119
|
+
pi.add_argument("--origin", default=None, help="log origin, typed in by hand")
|
|
120
|
+
pi.add_argument("--log-vkey", default=None, help="log verifier key name+hexid+base64, typed in by hand")
|
|
121
|
+
pi.add_argument("--policy-kid", default=None, help="policy key did:key, typed in by hand")
|
|
122
|
+
pi.add_argument("--owner-kid", default=None, help="owner key did:key, typed in by hand")
|
|
123
|
+
pi.add_argument("--card-url", default=None, help="assurance card URL (default https://DOMAIN/.well-known/aics-card.json)")
|
|
124
|
+
pi.add_argument("--no-card", action="store_true",
|
|
125
|
+
help="do not read the witness policy from the card (when the bundle pins a bootstrap card, "
|
|
126
|
+
"give --quorum, and --witness/--max-age for a positive quorum)")
|
|
127
|
+
pi.add_argument("--accept-signed-card", action="store_true",
|
|
128
|
+
help="take the witness policy from a card that is signed but not the pinned bootstrap card")
|
|
129
|
+
pi.add_argument("--witness", action="append", default=None, help="witness cosignature vkey (repeatable)")
|
|
130
|
+
pi.add_argument("--quorum", type=int, default=None)
|
|
131
|
+
pi.add_argument("--max-age", type=int, default=None, help="max checkpoint age in seconds (at least 60)")
|
|
132
|
+
pi.add_argument("--independent", action="append", default=None, metavar="NAME",
|
|
133
|
+
help="you assessed witness NAME as independent of the operator (repeatable); the card's own "
|
|
134
|
+
"'independent' flag is only the operator's claim (AICS-1 §6.3)")
|
|
135
|
+
pi.add_argument("--allow-weaker", action="store_true",
|
|
136
|
+
help="allow replacing the pinned witness policy with a weaker one (refused by default)")
|
|
137
|
+
pi.add_argument("--reset-history", action="store_true",
|
|
138
|
+
help="start the saved checkpoint history again (needed when the origin changes; recorded, "
|
|
139
|
+
"nothing is deleted)")
|
|
140
|
+
pi.add_argument("--checkpoint-url", default=None)
|
|
141
|
+
pi.add_argument("--tiles-url", default=None, help="C2SP tlog-tiles prefix (tiles at <prefix>/tile/...)")
|
|
142
|
+
pi.add_argument("--consistency-url", default=None, help="proof endpoint template with {old} and {new}")
|
|
143
|
+
pi.add_argument("--mirror", action="append", default=None, help="mirror tile prefix (repeatable)")
|
|
144
|
+
pi.add_argument("--require-sources", type=int, default=None, metavar="N",
|
|
145
|
+
help="independent kinds of out-of-band source that must give the log key (default 2; "
|
|
146
|
+
"--require-sources 1 accepts a single source, e.g. DNS alone)")
|
|
147
|
+
pi.add_argument("--force", action="store_true", help="replace an existing policy (the old one is archived)")
|
|
148
|
+
pi.set_defaults(func=C.cmd_policy_init, cmd_name="policy init")
|
|
149
|
+
ps = psub.add_parser("show", parents=[common], help="print the pinned policy and its TXT record form")
|
|
150
|
+
ps.set_defaults(func=C.cmd_policy_show, cmd_name="policy show")
|
|
151
|
+
pd = psub.add_parser("diff", parents=[common], help="compare DNS, the PyPI constant and the policy file")
|
|
152
|
+
pd.add_argument("--domain", default=None)
|
|
153
|
+
pd.add_argument("--nameserver", default=os.environ.get("AICS_NAMESERVER"))
|
|
154
|
+
pd.set_defaults(func=C.cmd_policy_diff, cmd_name="policy diff")
|
|
155
|
+
|
|
156
|
+
# trust bundle (operator) ----------------------------------------------
|
|
157
|
+
tr = sub.add_parser("trust", parents=[common], help="operator: build the AICS-1 §6.4 trust bundle to publish")
|
|
158
|
+
trsub = tr.add_subparsers(dest="trust_cmd", metavar="build", parser_class=_Parser)
|
|
159
|
+
trsub.required = True
|
|
160
|
+
tb = trsub.add_parser("build", parents=[common],
|
|
161
|
+
help="print the JCS trust bundle, its aics1= TXT value, its SHA-256 and the README block, "
|
|
162
|
+
"after checking the logged card and the keys of pubkeys.txt")
|
|
163
|
+
tb.add_argument("--pubkeys", required=True, metavar="FILE",
|
|
164
|
+
help="the key file aics-server writes (/data/pubkeys.txt): origin, log, policy, owner")
|
|
165
|
+
tb.add_argument("--card", required=True, metavar="FILE",
|
|
166
|
+
help="the logged bootstrap card, exactly the bytes the server serves (J(card))")
|
|
167
|
+
tb.add_argument("--policies-url", default=None, metavar="URL",
|
|
168
|
+
help="the /v2/policies listing to find the card's policy leaf in (default: the card's "
|
|
169
|
+
"api_base + /v2/policies)")
|
|
170
|
+
tb.add_argument("--policies-file", default=None, metavar="FILE",
|
|
171
|
+
help="read the /v2/policies listing from FILE instead of fetching it")
|
|
172
|
+
tb.set_defaults(func=BB.cmd_trust_build, cmd_name="trust build")
|
|
173
|
+
|
|
174
|
+
# verify ------------------------------------------------------------
|
|
175
|
+
ver = sub.add_parser("verify", parents=[common], help="verify checkpoint, consistency or card")
|
|
176
|
+
vsub = ver.add_subparsers(dest="verify_cmd", metavar="checkpoint|consistency|card|message|receipt|tombstones",
|
|
177
|
+
parser_class=_Parser)
|
|
178
|
+
vsub.required = True
|
|
179
|
+
vc = vsub.add_parser("checkpoint", parents=[common], help="fetch and verify the signed checkpoint, then save it")
|
|
180
|
+
vc.add_argument("--url", default=None, help="checkpoint URL (default: pinned checkpoint_url)")
|
|
181
|
+
vc.add_argument("--note", default=None, help="verify a local note file instead ('-' for stdin)")
|
|
182
|
+
vc.add_argument("--no-save", action="store_true", help="verify only; save nothing")
|
|
183
|
+
vc.add_argument("--max-skew", type=int, default=DEFAULT_MAX_SKEW, help="allowed clock skew, seconds")
|
|
184
|
+
vc.add_argument("--reset-history", action="store_true",
|
|
185
|
+
help="if the saved head cannot be authenticated, drop the unverifiable saved checkpoints from "
|
|
186
|
+
"continuity checks and go on (recorded; nothing is deleted)")
|
|
187
|
+
vc.set_defaults(func=C.cmd_verify_checkpoint, cmd_name="verify checkpoint")
|
|
188
|
+
vco = vsub.add_parser("consistency", parents=[common],
|
|
189
|
+
help="prove every saved checkpoint is a prefix of today's log")
|
|
190
|
+
vco.add_argument("--saved", action="store_true", default=True, help="check the saved checkpoints (default)")
|
|
191
|
+
vco.add_argument("--url", default=None, help="checkpoint URL (default: pinned checkpoint_url)")
|
|
192
|
+
vco.add_argument("--max-skew", type=int, default=DEFAULT_MAX_SKEW)
|
|
193
|
+
vco.add_argument("--reset-history", action="store_true",
|
|
194
|
+
help="drop saved checkpoints that cannot be authenticated from continuity checks "
|
|
195
|
+
"(recorded; nothing is deleted)")
|
|
196
|
+
vco.set_defaults(func=C.cmd_verify_consistency, cmd_name="verify consistency")
|
|
197
|
+
vca = vsub.add_parser("card", parents=[common], help="verify the signed assurance card")
|
|
198
|
+
vca.add_argument("url", nargs="?", default=None, help="site or card URL (default: pinned card_url)")
|
|
199
|
+
vca.add_argument("--file", default=None, help="verify a local card file (compared with the cards saved for "
|
|
200
|
+
"the pinned policy key)")
|
|
201
|
+
vca.set_defaults(func=C.cmd_verify_card, cmd_name="verify card")
|
|
202
|
+
vm = vsub.add_parser("message", parents=[common],
|
|
203
|
+
help="verify one log entry (AICS-1 §14.3): checkpoint, inclusion, envelope, and the name, key "
|
|
204
|
+
"and sequence history through its index")
|
|
205
|
+
vm.add_argument("--bundle", default=None, metavar="FILE",
|
|
206
|
+
help="an AICS-1 §14.3 wire bundle (the API serves one at GET /v2/entries/<index>) or a "
|
|
207
|
+
"verification package {bundle, trust_bundle, policy_cards, entry_prefix, checkpoint_notes}; "
|
|
208
|
+
"'-' reads stdin")
|
|
209
|
+
vm.add_argument("--offline", action="store_true",
|
|
210
|
+
help="read nothing from the network: the history comes from the package or --tiles DIR, and "
|
|
211
|
+
"continuity with a larger saved head stays unproven")
|
|
212
|
+
vm.add_argument("--tiles", default=None, metavar="DIR|URL",
|
|
213
|
+
help="read the entry prefix from a local tile directory (a mirror copy or a backup) or a tile "
|
|
214
|
+
"prefix URL (default: the pinned tiles_url, unless --offline)")
|
|
215
|
+
vm.set_defaults(func=C.cmd_verify_message, cmd_name="verify message")
|
|
216
|
+
for name in ("receipt", "tombstones"):
|
|
217
|
+
vx = vsub.add_parser(name, parents=[common], help="(not available until the server ships it)")
|
|
218
|
+
vx.set_defaults(func=C.cmd_unavailable, cmd_name=f"verify {name}", stub=True)
|
|
219
|
+
|
|
220
|
+
# gossip / status / inbox ---------------------------------------------
|
|
221
|
+
gc = sub.add_parser("gossip-check", parents=[common],
|
|
222
|
+
help="judge two checkpoint notes by the G5 rule; prints split-view evidence")
|
|
223
|
+
gc.add_argument("note1", help="note file, '-' or URL")
|
|
224
|
+
gc.add_argument("note2", help="note file, '-' or URL")
|
|
225
|
+
gc.add_argument("--proof", default=None, help="consistency proof file (one hash per line)")
|
|
226
|
+
gc.add_argument("--offline", action="store_true", help="do not fetch tiles or proofs")
|
|
227
|
+
gc.add_argument("--out", default=None, help="also write the evidence bundle here (never overwritten)")
|
|
228
|
+
gc.set_defaults(func=C.cmd_gossip_check, cmd_name="gossip-check")
|
|
229
|
+
|
|
230
|
+
st = sub.add_parser("status", parents=[common],
|
|
231
|
+
help="verify origin (and mirror) checkpoints; report pairs not consistency-provable")
|
|
232
|
+
st.add_argument("--mirrors", action="store_true", help="include every mirror in the policy")
|
|
233
|
+
st.add_argument("--max-skew", type=int, default=DEFAULT_MAX_SKEW)
|
|
234
|
+
st.set_defaults(func=C.cmd_status, cmd_name="status")
|
|
235
|
+
|
|
236
|
+
ib = sub.add_parser("inbox", parents=[common], help="(not available until the server ships it)")
|
|
237
|
+
ib.set_defaults(func=C.cmd_unavailable, cmd_name="inbox", stub=True)
|
|
238
|
+
return parser
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _emit(out: TextIO, as_json: bool, data: dict[str, Any], text: list[str]) -> None:
|
|
242
|
+
if as_json:
|
|
243
|
+
out.write(json.dumps(data, default=str) + "\n")
|
|
244
|
+
else:
|
|
245
|
+
lines = [printable_line(part) for chunk in text for part in str(chunk).split("\n")]
|
|
246
|
+
out.write("\n".join(lines) + "\n")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def run(argv: Sequence[str] | None = None, env: C.Env | None = None, out: TextIO | None = None) -> int:
|
|
250
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
251
|
+
out = out or sys.stdout
|
|
252
|
+
as_json = "--json" in argv
|
|
253
|
+
parser = build_parser()
|
|
254
|
+
cmd_name = "aics"
|
|
255
|
+
try:
|
|
256
|
+
try:
|
|
257
|
+
args, unknown = parser.parse_known_args(argv)
|
|
258
|
+
except SystemExit as exc: # --help / --version
|
|
259
|
+
return int(exc.code or 0)
|
|
260
|
+
cmd_name = getattr(args, "cmd_name", "aics")
|
|
261
|
+
as_json = bool(getattr(args, "json", False)) or as_json
|
|
262
|
+
if unknown and not getattr(args, "stub", False):
|
|
263
|
+
raise UsageError(f"unrecognized arguments: {' '.join(unknown)}")
|
|
264
|
+
if env is None:
|
|
265
|
+
env = C.Env(fetcher=HttpFetcher(timeout=args.timeout))
|
|
266
|
+
result = args.func(args, env)
|
|
267
|
+
data = {"command": cmd_name, "exit": result.code, **result.data}
|
|
268
|
+
_emit(out, as_json, data, result.text)
|
|
269
|
+
return result.code
|
|
270
|
+
except VerificationFailed as exc:
|
|
271
|
+
details = dict(exc.details)
|
|
272
|
+
details.pop("text_lines", None)
|
|
273
|
+
data = {"ok": False, "command": cmd_name, "exit": exc.exit_code, "error": exc.message,
|
|
274
|
+
**details, "evidence": exc.evidence}
|
|
275
|
+
text = [f"VERIFICATION FAILED: {exc.message}", "evidence:", json.dumps(exc.evidence, indent=2, default=str)]
|
|
276
|
+
_emit(out, as_json, data, text)
|
|
277
|
+
return exc.exit_code
|
|
278
|
+
except AicsError as exc:
|
|
279
|
+
details = dict(exc.details)
|
|
280
|
+
lines = details.pop("text_lines", None) or []
|
|
281
|
+
data = {"ok": False, "command": cmd_name, "exit": exc.exit_code, "error": exc.message, **details}
|
|
282
|
+
_emit(out, as_json, data, [*lines, f"error: {exc.message}"])
|
|
283
|
+
return exc.exit_code
|
|
284
|
+
except KeyboardInterrupt:
|
|
285
|
+
return EXIT_ERROR
|
|
286
|
+
except Exception as exc: # last resort: --json still prints one object, and the exit code is 1, never 2
|
|
287
|
+
message = f"internal error: {type(exc).__name__}: {exc}"
|
|
288
|
+
data = {"ok": False, "command": cmd_name, "exit": EXIT_ERROR, "error": message, "internal": True}
|
|
289
|
+
_emit(out, as_json, data, [f"error: {message}"])
|
|
290
|
+
return EXIT_ERROR
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def main() -> None:
|
|
294
|
+
sys.exit(run())
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__": # pragma: no cover
|
|
298
|
+
main()
|