warrant-graph 2.0.0a1__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.
warrant/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Warrant 2: transitions of work, exact closures and what remains open (SPEC.md)."""
2
+ from .jcs import canon, loads
3
+ from .engine import assess, capsule, frontier, profile, questions, reliance
4
+ from .keys import Key
5
+ from .records import accept, challenge, duty, grant, item, observation, record, transition
6
+ from .store import Snapshot, Store, Trust
7
+
8
+ __version__ = "2.0.0a1"
9
+ __all__ = ["Key", "Snapshot", "Store", "Trust", "accept", "assess", "canon", "capsule", "challenge", "duty",
10
+ "frontier", "grant", "item", "loads", "observation", "profile", "questions", "record", "reliance",
11
+ "transition"]
warrant/bindings.py ADDED
@@ -0,0 +1,145 @@
1
+ """Bindings and roles of a repository's trust (the open circle of witnesses, part A, and
2
+ AMEND 1 of #170): the one rule tools/claims.py and a warrant-agent workspace admit keys by.
3
+
4
+ A binding certifies itself: its key signs canon({actor, public, purpose}). `admit_bindings`
5
+ admits one mechanically, in file order, under the roles .warrant/roles.json pins; nobody judges
6
+ who may join. A binding grants only "this key signs as this actor" (SPEC §6). This module reads
7
+ documents; it writes none and runs nothing (the claims tool writes them)."""
8
+ from . import jcs as C
9
+ from . import keys as K
10
+ from . import records as R
11
+ from . import shapes as S
12
+ from .store import Trust
13
+
14
+
15
+ # Open circle of witnesses: a binding certifies itself (its key signs it); nobody admits anyone.
16
+ BINDINGS = "warrant-claims/bindings@1" # .warrant/trust.json: {"bindings": BINDINGS, "keys": [...]}
17
+ BINDING = "warrant-claims/binding@1" # the purpose a proof signs: canon({actor, public, purpose})
18
+ BINDING_FIELDS = ("actor", "public", "proof")
19
+ ROLES = "warrant-claims/roles@1" # .warrant/roles.json: the actors the configuration gives a role
20
+ COLLECTIVE = "collective" # a role no key signs as (readers): never bindable
21
+
22
+
23
+ def binding_message(actor, public):
24
+ """The bytes a binding's proof signs: canon({actor, public, purpose}). It starts with `{`,
25
+ so it is never the message of a record signature ("warrant-sig-v1:" || id)."""
26
+ return C.canon({"actor": actor, "public": public, "purpose": BINDING})
27
+
28
+
29
+ def binding_proof(key, actor):
30
+ return key.sign(binding_message(actor, key.public))
31
+
32
+
33
+ def read_roles(doc):
34
+ """({actor: public hex | None | COLLECTIVE}, problems) of a roles document (.warrant/roles.json).
35
+
36
+ AMEND 1 of #170: a name the repository's own configuration gives a role (an acceptor, a
37
+ creditor, the claims' author) is not first-come. Its key is pinned here, as a receiver's
38
+ profile pins the keys it trusts: a hex64 public key (a binding of that actor is admitted
39
+ with exactly that key), null (reserved: not bindable until a pin names its key) or
40
+ "collective" (a role no key signs as, such as `readers`: never bindable). Every other
41
+ name stays first-come. Changing who a role is, is a diff of this reviewed file."""
42
+ if not (isinstance(doc, dict) and set(doc) == {"roles", "actors"} and doc["roles"] == ROLES
43
+ and isinstance(doc["actors"], dict)):
44
+ return {}, [("ROLES_SHAPE", f".warrant/roles.json is not {{\"roles\": {ROLES!r}, \"actors\": {{...}}}}")]
45
+ roles, problems = {}, []
46
+ for actor, v in doc["actors"].items():
47
+ try:
48
+ R.check_actor(actor)
49
+ except R.Invalid as e:
50
+ problems.append(("ROLES_SHAPE", f".warrant/roles.json: {actor[:60]!r} is not an actor id: {e}"))
51
+ continue
52
+ if not (v is None or v == COLLECTIVE or S.hex64(v)):
53
+ problems.append(("ROLES_SHAPE", f".warrant/roles.json: {actor}: a role is a hex64 public key, null or "
54
+ f"{COLLECTIVE!r}, not {str(v)[:70]!r}"))
55
+ continue
56
+ roles[actor] = v
57
+ return roles, problems
58
+
59
+
60
+ def _role_name(v):
61
+ """A role value for a message: 'key <id>' for a key, else what it is (never raises)."""
62
+ if S.hex64(v):
63
+ return f"key {K.key_id(v)[:12]}"
64
+ if v == COLLECTIVE:
65
+ return "collective"
66
+ return "reserved" if v is None else repr(v)[:40]
67
+
68
+
69
+ def admit_bindings(doc, legacy=False, roles=None):
70
+ """(admitted [(actor, public)], problems [(code, detail)]) of a bindings document.
71
+
72
+ A binding is admitted mechanically, in file order, iff its proof is the signature of its
73
+ own key over binding_message(actor, public), its actor id has SPEC-3-21's shape, its
74
+ actor's role (`roles`, read_roles) admits that key, its key is not bound already, and its
75
+ actor is not bound already to another key: the first binding of a name with no role
76
+ wins, and a second key for it would need a record signed by the first, which this tool
77
+ does not implement (rotation is out of scope), so it is refused. Nobody judges who may
78
+ join; what a binding grants is only "this key signs as this actor" (SPEC §6). `legacy`
79
+ reads a trust/2 document (actor, public, no proof) as admitted: only for a base revision
80
+ written before bindings carried proofs."""
81
+ if legacy and isinstance(doc, dict) and doc.get("warrant") == "trust/2":
82
+ try:
83
+ return [(a, p) for a, p in Trust(doc).keys.values()], []
84
+ except R.Invalid as e:
85
+ return [], [("BINDINGS_SHAPE", f".warrant/trust.json: {e}")]
86
+ if not (isinstance(doc, dict) and set(doc) == {"bindings", "keys"} and doc["bindings"] == BINDINGS
87
+ and isinstance(doc["keys"], list)):
88
+ return [], [("BINDINGS_SHAPE", f".warrant/trust.json is not {{\"bindings\": {BINDINGS!r}, \"keys\": [...]}} "
89
+ "(tools/claims.py bind writes it)")]
90
+ admitted, problems = [], []
91
+ publics = [k.get("public") for k in doc["keys"] if isinstance(k, dict)]
92
+ for i, k in enumerate(doc["keys"]):
93
+ code, why = _binding_problem(k, publics, admitted, roles or {})
94
+ if code:
95
+ problems.append((code, f".warrant/trust.json keys[{i}]: {why}"))
96
+ else:
97
+ admitted.append((k["actor"], k["public"]))
98
+ return admitted, problems
99
+
100
+
101
+ def _binding_problem(k, publics, admitted, roles):
102
+ """(code, why) when entry `k` is not admitted after `admitted`, else (None, None)."""
103
+ if not (isinstance(k, dict) and set(k) <= set(BINDING_FIELDS) and {"actor", "public"} <= set(k)
104
+ and S.hex64(k["public"])):
105
+ return "BINDING_SHAPE", f"an entry is {{actor, public: hex64, proof: hex128}}, not {str(k)[:120]}"
106
+ try:
107
+ R.check_actor(k["actor"])
108
+ except R.Invalid as e:
109
+ return "BINDING_ACTOR_SHAPE", f"{str(k['actor'])[:60]!r} is not an actor id (SPEC-3-21): {e}"
110
+ if "proof" not in k:
111
+ return "BINDING_UNPROVEN", f"{k['actor']}: no proof; the key's own signature over the binding is needed"
112
+ msg = binding_message(k["actor"], k["public"])
113
+ if not K.verify(k["public"], k["proof"], msg):
114
+ other = [p for p in publics if p != k["public"] and isinstance(p, str) and K.verify(p, k["proof"], msg)]
115
+ if other:
116
+ return "BINDING_PROOF_OTHER_KEY", (f"{k['actor']}: the proof is signed by key {K.key_id(other[0])[:12]}, "
117
+ f"not by the key it binds ({K.key_id(k['public'])[:12]})")
118
+ return "BINDING_PROOF_FORGED", f"{k['actor']}: the proof is not the bound key's signature over this binding"
119
+ role = roles.get(k["actor"], k["public"])
120
+ if role != k["public"]: # one rule; the code only says which kind of role refused it
121
+ if role == COLLECTIVE:
122
+ return "BINDING_ROLE_COLLECTIVE", f"{k['actor']} is a collective role in .warrant/roles.json: no key signs as it"
123
+ if role is None:
124
+ return "BINDING_ROLE_RESERVED", (f"{k['actor']} is reserved in .warrant/roles.json: it is bindable once a "
125
+ "reviewed pin names its key")
126
+ return "BINDING_ROLE_KEY", (f"{k['actor']}: .warrant/roles.json pins {_role_name(role)}, this binding is of "
127
+ f"key {K.key_id(k['public'])[:12]}")
128
+ if any(p == k["public"] for _, p in admitted):
129
+ return "BINDING_KEY_TWICE", f"key {K.key_id(k['public'])[:12]} is bound already"
130
+ first = next((p for a, p in admitted if a == k["actor"]), None)
131
+ if first is not None:
132
+ return "BINDING_ACTOR_TAKEN", (f"{k['actor']} is bound already to key {K.key_id(first)[:12]}; the first "
133
+ "binding wins, and rotation (a record signed by the first key) is not "
134
+ "implemented, so a second key for an actor is refused")
135
+ return None, None
136
+
137
+
138
+ def trust_of(bindings):
139
+ """A Trust of (actor, public) pairs, the first binding of each key kept."""
140
+ seen, pairs = set(), []
141
+ for a, p in bindings:
142
+ if p not in seen:
143
+ seen.add(p)
144
+ pairs.append((a, p))
145
+ return Trust(Trust.document(pairs))
warrant/cli.py ADDED
@@ -0,0 +1,401 @@
1
+ """`warrant` — the one command-line interface of Warrant 2 (SPEC.md).
2
+
3
+ Writing: init, key, trust, blob, file, accept, observe, manifest, compute
4
+ Reading: assess, reliance, frontier, questions, capsule
5
+ Every reading command assesses one view: --store, --trust, --profile, --as-of,
6
+ and optionally --current and --workspace. Output is JSON."""
7
+ import argparse
8
+ import base64
9
+ import json
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+
16
+ from . import jcs as C
17
+ from . import compute as P
18
+ from . import engine as E
19
+ from . import limits as L
20
+ from . import records as R
21
+ from . import triggers as TR
22
+ from .keys import Key
23
+ from .store import Store, Trust
24
+
25
+
26
+ def _out(doc):
27
+ sys.stdout.write(json.dumps(doc, indent=2, ensure_ascii=False, sort_keys=True) + "\n")
28
+
29
+
30
+ def _read_json(arg):
31
+ if arg == "-":
32
+ return json.loads(sys.stdin.read())
33
+ if arg.lstrip().startswith(("{", "[")):
34
+ return json.loads(arg)
35
+ return json.loads(Path(arg).read_text())
36
+
37
+
38
+ def _key(path):
39
+ return Key.from_seed_hex(Path(path).read_text())
40
+
41
+
42
+ def _now(v):
43
+ return int(time.time()) if v is None else v
44
+
45
+
46
+ # ---------------------------------------------------------------- writing
47
+
48
+ def cmd_init(a):
49
+ Store(a.store).init()
50
+ _out({"store": str(Path(a.store))})
51
+
52
+
53
+ def cmd_key(a):
54
+ p = Path(a.out)
55
+ if p.exists():
56
+ raise SystemExit(f"refusing to overwrite {p}")
57
+ k = Key.generate()
58
+ fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
59
+ with os.fdopen(fd, "w") as f:
60
+ f.write(k.seed_hex() + "\n")
61
+ _out({"key_file": str(p), "public": k.public, "key_id": k.id})
62
+
63
+
64
+ def cmd_trust(a):
65
+ p = Path(a.trust)
66
+ doc = json.loads(p.read_text()) if p.exists() else Trust.document([])
67
+ pub = _key(a.key).public if a.key else a.public
68
+ doc["keys"].append({"actor": a.actor, "public": pub})
69
+ Trust(doc) # refuse a malformed or doubly bound file
70
+ p.write_bytes(C.canon(doc))
71
+ _out({"trust": str(p), "actor": a.actor, "public": pub})
72
+
73
+
74
+ def cmd_blob(a):
75
+ s = Store(a.store).init()
76
+ if a.json is not None:
77
+ h = s.put_json(_read_json(a.json))
78
+ else:
79
+ h = s.put_blob(sys.stdin.buffer.read() if a.file == "-" else Path(a.file).read_bytes())
80
+ _out({"blob": h})
81
+
82
+
83
+ # Structural members `warrant file` fills when a body omits them. Only empty
84
+ # values: nothing here states a meaning. Semantic members (a duty's statement,
85
+ # holder, owed_to, accept_by, condition; a transition's basis, established,
86
+ # residual, closes) are never filled, so their absence is still refused.
87
+ STRUCTURAL = {
88
+ "duty": {"release_by": [], "schedule": None, "requires": [], "serves": [], "answers": None, "subject": None},
89
+ "transition": {"relied_on": [], "opened": [], "subject": None, "supersedes": [], "triggers": []},
90
+ }
91
+
92
+
93
+ def fill(rtype, body):
94
+ """`body` with omitted structural members filled; given members are kept as they are.
95
+ A transition that omits `evidence` carries exactly the evidence its items cite."""
96
+ if rtype not in STRUCTURAL or not isinstance(body, dict):
97
+ return body
98
+ out = {k: v for k, v in STRUCTURAL[rtype].items() if k not in body}
99
+ if rtype == "transition" and "evidence" not in body and isinstance(body.get("closes"), list):
100
+ out["evidence"] = sorted({h for m in body["closes"] if isinstance(m, dict)
101
+ for h in (m.get("evidence") if isinstance(m.get("evidence"), list) else [])})
102
+ out.update(body)
103
+ return out
104
+
105
+
106
+ def _store(a):
107
+ """The store a signer writes, with its own verdicts directory if it names one (F44)."""
108
+ return Store(a.store, verdicts=a.verdicts).init()
109
+
110
+
111
+ def _append(s, rtype, actor, at, body, key):
112
+ """File a record extending `actor`'s chain in store `s` (SPEC §3: `prev` is its head)."""
113
+ try:
114
+ return s.append(rtype, actor, at, body, key)
115
+ except R.Invalid as e:
116
+ raise SystemExit(f"invalid {rtype}: {e}")
117
+ except ValueError as e:
118
+ raise SystemExit(str(e))
119
+
120
+
121
+ def cmd_file(a):
122
+ s = _store(a)
123
+ body = fill(a.type, _read_json(a.body))
124
+ rid = _append(s, a.type, a.actor, _now(a.at), body, _key(a.key))
125
+ if a.id:
126
+ sys.stdout.write(rid + "\n")
127
+ else:
128
+ _out({"record": rid, "type": a.type})
129
+
130
+
131
+ def cmd_accept(a):
132
+ """Accept one exact item (or a transition's results) as it is filed."""
133
+ s = _store(a)
134
+ snap = s.snapshot()
135
+ env = C.loads(snap.records[a.transition])
136
+ t = env["record"]["body"]
137
+ ev = t["evidence"] if a.item is None else t["closes"][a.item]["evidence"]
138
+ rid = _append(s, "accept", a.actor, _now(a.at), R.accept(a.transition, a.item, ev, a.support or []), _key(a.key))
139
+ _out({"record": rid, "type": "accept"})
140
+
141
+
142
+ def cmd_observe(a):
143
+ """Run a probe once and file what was observed, when, and how."""
144
+ s = _store(a)
145
+ probe = _read_json(a.probe)
146
+ if not (isinstance(probe, dict) and isinstance(probe.get("argv"), list) and probe["argv"]):
147
+ raise SystemExit("a probe is {\"argv\": [...], ...}: the command this observer runs")
148
+ probe_h = s.put_json(probe)
149
+ t0 = int(time.time())
150
+ try:
151
+ p = subprocess.run(probe["argv"], capture_output=True, timeout=probe.get("timeout", 30), check=False)
152
+ result = p.stdout.decode(errors="replace").strip()[:R.MAX_TEXT]
153
+ raw = [s.put_blob(p.stdout), s.put_json({"exit": p.returncode, "stderr": p.stderr.decode(errors="replace")[-2000:]})]
154
+ except (OSError, subprocess.TimeoutExpired) as e:
155
+ result, raw = f"PROBE_FAILED: {type(e).__name__}", []
156
+ t1 = max(t0, int(time.time()))
157
+ body = R.observation(a.target, probe_h, [t0, t1], result, raw)
158
+ rid = _append(s, "observation", a.actor, _now(a.at) if a.at is not None else t1, body, _key(a.key))
159
+ _out({"record": rid, "type": "observation", "result": result, "window": [t0, t1]})
160
+
161
+
162
+ def cmd_manifest(a):
163
+ s = Store(a.store).init()
164
+ try:
165
+ doc = TR.manifest(a.repo, a.rev, sorted(a.paths))
166
+ except TR.Refusal as e:
167
+ raise SystemExit(f"manifest refused: {e}")
168
+ _out({"blob": s.put_json(doc), "commit": TR.resolve(a.repo, a.rev)})
169
+
170
+
171
+ # ---------------------------------------------------------------- compute (producer side)
172
+
173
+ def _env_blobs(s, env_h):
174
+ snap = s.snapshot()
175
+ doc = snap.json_blob(env_h)
176
+ if doc is None or P.environment_problem(doc):
177
+ raise SystemExit("not a sigma environment manifest in this store")
178
+ hashes = P.environment_hashes(doc)
179
+ return hashes, {h: snap.blobs[h] for h in hashes if h in snap.blobs}
180
+
181
+
182
+ def _call(op, args, blobs=None):
183
+ try:
184
+ out = P.Provider.capture().call(op, args, blobs)
185
+ except P.ProviderError as e:
186
+ raise SystemExit(f"provider: {e}")
187
+ if "refused" in out:
188
+ raise SystemExit(f"provider refused: {out['refused']}")
189
+ return out
190
+
191
+
192
+ def cmd_compute(a):
193
+ s = Store(a.store).init()
194
+ runtime = {"book1": P.BOOK1, "rich": P.RICH}[a.runtime]
195
+ if a.op == "provider":
196
+ try:
197
+ p = P.Provider.capture()
198
+ except P.ProviderError as e:
199
+ raise SystemExit(f"provider: {e}")
200
+ return _out({"provider": p.id, "where": p.where, "files": p.identity["files"]})
201
+ if a.op == "term":
202
+ out = _call("term", {"expr": a.expr})
203
+ nodes = [s.put_blob(base64.b64decode(b)) for b in out["nodes"].values()]
204
+ env_h = s.put_json(P.environment_doc(nodes))
205
+ return _out({"input": out["input"], "environment": env_h, "runtime": runtime, "nodes": len(nodes)})
206
+ hashes, blobs = _env_blobs(s, a.environment)
207
+ base = {"runtime": runtime, "input": a.input, "hashes": hashes}
208
+ if a.op == "start":
209
+ out = _call("start", base, blobs)
210
+ elif a.op == "advance":
211
+ snap = s.snapshot()
212
+ out = _call("advance", dict(base, checkpoint=base64.b64encode(snap.blobs[a.checkpoint]).decode(),
213
+ state=base64.b64encode(snap.blobs[a.state]).decode(), credit=a.credit), blobs)
214
+ elif a.op == "cycle":
215
+ out = _call("find_cycle", dict(base, max_steps=a.max_steps), blobs)
216
+ if out.get("witness") is None:
217
+ return _out({"cycle": None, "outcome": out.get("outcome")})
218
+ w = s.put_blob(base64.b64decode(out["witness"]))
219
+ doc = {"cycle": "sigma-resume", "target": a.target, "input": a.input, "environment": a.environment,
220
+ "witness": w, "lost": []}
221
+ return _out({"cycle": s.put_json(doc), "witness": w})
222
+ else:
223
+ raise SystemExit(f"unknown compute op {a.op}")
224
+ ck = s.put_blob(base64.b64decode(out["checkpoint"]))
225
+ st = s.put_blob(base64.b64decode(out["state"]))
226
+ res = {"outcome": out.get("outcome", "STARTED"), "checkpoint": ck, "state": st, "result": out.get("result")}
227
+ if out.get("outcome") == "NORMAL_FORM" and a.target:
228
+ res["receipt"] = s.put_json({"receipt": "sigma-resume", "target": a.target, "input": a.input,
229
+ "environment": a.environment, "checkpoint": ck, "state": st,
230
+ "claimed": out["result"], "lost": []})
231
+ _out(res)
232
+
233
+
234
+ # ---------------------------------------------------------------- reading
235
+
236
+ def _limits(a):
237
+ out = {}
238
+ for item in a.limit:
239
+ name, _, value = item.partition("=")
240
+ if not value.isdigit():
241
+ raise SystemExit(f"--limit {item!r}: NAME=N")
242
+ out[name] = int(value)
243
+ try:
244
+ return L.resolve(out)
245
+ except ValueError as e:
246
+ raise SystemExit(str(e)) from None
247
+
248
+
249
+ def _report(a):
250
+ s = Store(a.store)
251
+ lim = _limits(a)
252
+ snap = s.snapshot(limits=lim)
253
+ trust = Trust.load(a.trust)
254
+ prof = _read_json(a.profile) if a.profile else E.profile()
255
+ ws = None
256
+ if a.workspace:
257
+ repo, _, rev = a.workspace.rpartition("@")
258
+ ws = (repo, rev or "HEAD")
259
+ try:
260
+ provider = P.Provider.capture()
261
+ except P.ProviderError as e:
262
+ print(f"warning: provider unavailable, replays are UNVERIFIED: {e}", file=sys.stderr)
263
+ provider = None
264
+ return E.assess(snap, trust, _now(a.as_of), prof, current=a.current or [], workspace=ws, provider=provider,
265
+ replay_cache=a.replay_cache, limits=lim, verdicts=a.verdicts)
266
+
267
+
268
+ def cmd_assess(a):
269
+ _out(_report(a))
270
+
271
+
272
+ def cmd_reliance(a):
273
+ _out(E.reliance(_report(a), a.target, a.use))
274
+
275
+
276
+ def cmd_frontier(a):
277
+ rep = _report(a)
278
+ _out({"assessment_id": rep["assessment_id"], "actor": a.actor, "frontier": E.frontier(rep, a.actor)})
279
+
280
+
281
+ def cmd_questions(a):
282
+ rep = _report(a)
283
+ _out({"assessment_id": rep["assessment_id"], "questions": E.questions(rep, a.scope, owned=a.all)})
284
+
285
+
286
+ def cmd_capsule(a):
287
+ _out(E.capsule(_report(a), a.target, a.use))
288
+
289
+
290
+ def build_parser():
291
+ ap = argparse.ArgumentParser(prog="warrant", description=__doc__, allow_abbrev=False,
292
+ formatter_class=argparse.RawDescriptionHelpFormatter)
293
+ sub = ap.add_subparsers(dest="cmd", required=True)
294
+
295
+ def store(p):
296
+ p.add_argument("--store", required=True)
297
+
298
+ def signer(p):
299
+ store(p)
300
+ p.add_argument("--key", required=True, help="file holding the signer's Ed25519 seed (hex)")
301
+ p.add_argument("--actor", required=True)
302
+ p.add_argument("--at", type=int, help="record time (default: now)")
303
+ p.add_argument("--verdicts", help="directory of this signer's earlier authentication verdicts (cost only)")
304
+
305
+ def view(p):
306
+ store(p)
307
+ p.add_argument("--trust", required=True)
308
+ p.add_argument("--profile", help="the recipient's profile JSON (default: one use 'act', no acceptors)")
309
+ p.add_argument("--as-of", type=int, help="end of the view (default: now)")
310
+ p.add_argument("--current", action="append", help="a revision the recipient treats as current")
311
+ p.add_argument("--workspace", help="REPO@REV for subject_changed triggers")
312
+ p.add_argument("--replay-cache", help="directory of this recipient's earlier verified replays (cost only)")
313
+ p.add_argument("--verdicts", help="directory of this recipient's earlier authentication verdicts (cost only)")
314
+ p.add_argument("--limit", action="append", default=[], metavar="NAME=N",
315
+ help="override an admission limit (SPEC §9), e.g. duty_periods=50000")
316
+
317
+ p = sub.add_parser("init", help="create a store")
318
+ store(p)
319
+ p.set_defaults(fn=cmd_init)
320
+ p = sub.add_parser("key", help="generate an Ed25519 key file (0600, never overwritten)")
321
+ p.add_argument("--out", required=True)
322
+ p.set_defaults(fn=cmd_key)
323
+ p = sub.add_parser("trust", help="bind a key to an actor in a trust file")
324
+ p.add_argument("--trust", required=True)
325
+ p.add_argument("--actor", required=True)
326
+ g = p.add_mutually_exclusive_group(required=True)
327
+ g.add_argument("--key")
328
+ g.add_argument("--public")
329
+ p.set_defaults(fn=cmd_trust)
330
+ p = sub.add_parser("blob", help="add bytes (FILE or -) or a JSON document (--json) to the store")
331
+ store(p)
332
+ p.add_argument("file", nargs="?", default="-")
333
+ p.add_argument("--json")
334
+ p.set_defaults(fn=cmd_blob)
335
+ p = sub.add_parser("file", help="sign and file a record body (SPEC §3); a duty or transition body may omit "
336
+ "its structural members (empty lists, nulls; a transition's evidence is "
337
+ "then the evidence its items cite)")
338
+ signer(p)
339
+ p.add_argument("--type", required=True, choices=R.TYPES)
340
+ p.add_argument("--id", action="store_true", help="print only the new record id")
341
+ p.add_argument("body", help="JSON body, a file, or -")
342
+ p.set_defaults(fn=cmd_file)
343
+ p = sub.add_parser("accept", help="accept one exact item of a transition, or its results")
344
+ signer(p)
345
+ p.add_argument("transition")
346
+ p.add_argument("--item", type=int, help="item index; omit to accept the transition's results")
347
+ p.add_argument("--support", action="append")
348
+ p.set_defaults(fn=cmd_accept)
349
+ p = sub.add_parser("observe", help="run a probe and file the observation")
350
+ signer(p)
351
+ p.add_argument("--target", required=True)
352
+ p.add_argument("--probe", required=True, help='JSON {"argv": [...], "timeout": s}')
353
+ p.set_defaults(fn=cmd_observe)
354
+ p = sub.add_parser("manifest", help="store a subject_changed baseline for explicit paths at a revision")
355
+ store(p)
356
+ p.add_argument("--repo", required=True)
357
+ p.add_argument("--rev", required=True)
358
+ p.add_argument("paths", nargs="+")
359
+ p.set_defaults(fn=cmd_manifest)
360
+ p = sub.add_parser("compute", help="the installed Sigma provider: provider (its identity), term, start, advance, cycle")
361
+ store(p)
362
+ p.add_argument("op", choices=("provider", "term", "start", "advance", "cycle"))
363
+ p.add_argument("--runtime", choices=("book1", "rich"), default="book1")
364
+ p.add_argument("--expr")
365
+ p.add_argument("--input")
366
+ p.add_argument("--environment")
367
+ p.add_argument("--checkpoint")
368
+ p.add_argument("--state")
369
+ p.add_argument("--credit", type=int, default=0)
370
+ p.add_argument("--max-steps", type=int, default=10000)
371
+ p.add_argument("--target", help="crossing target named in the receipt or cycle")
372
+ p.set_defaults(fn=cmd_compute)
373
+ for name, fn, extra in (("assess", cmd_assess, None), ("reliance", cmd_reliance, "target"),
374
+ ("frontier", cmd_frontier, "actor"), ("questions", cmd_questions, None),
375
+ ("capsule", cmd_capsule, "target")):
376
+ p = sub.add_parser(name)
377
+ view(p)
378
+ if extra:
379
+ p.add_argument(extra)
380
+ if name in ("reliance", "capsule"):
381
+ p.add_argument("--use", default="act")
382
+ if name == "questions":
383
+ p.add_argument("--scope")
384
+ p.add_argument("--all", action="store_true", help="include questions a duty has taken")
385
+ p.set_defaults(fn=fn)
386
+ return ap
387
+
388
+
389
+ def main(argv=None):
390
+ a = build_parser().parse_args(argv)
391
+ try:
392
+ a.fn(a)
393
+ except L.LimitExceeded as e:
394
+ # An operational refusal (SPEC §9), never a report: nothing was assessed.
395
+ print(f"warrant: RESOURCE_LIMIT {e}", file=sys.stderr)
396
+ return 3
397
+ return 0
398
+
399
+
400
+ if __name__ == "__main__":
401
+ sys.exit(main())