warrant-graph 2.0.0a1__tar.gz

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.
Files changed (38) hide show
  1. warrant_graph-2.0.0a1/LICENSE +21 -0
  2. warrant_graph-2.0.0a1/PKG-INFO +130 -0
  3. warrant_graph-2.0.0a1/README.md +112 -0
  4. warrant_graph-2.0.0a1/pyproject.toml +38 -0
  5. warrant_graph-2.0.0a1/setup.cfg +4 -0
  6. warrant_graph-2.0.0a1/warrant/__init__.py +11 -0
  7. warrant_graph-2.0.0a1/warrant/bindings.py +145 -0
  8. warrant_graph-2.0.0a1/warrant/cli.py +401 -0
  9. warrant_graph-2.0.0a1/warrant/compute.py +291 -0
  10. warrant_graph-2.0.0a1/warrant/engine.py +990 -0
  11. warrant_graph-2.0.0a1/warrant/jcs.py +140 -0
  12. warrant_graph-2.0.0a1/warrant/keys.py +80 -0
  13. warrant_graph-2.0.0a1/warrant/limits.py +39 -0
  14. warrant_graph-2.0.0a1/warrant/pilot.py +436 -0
  15. warrant_graph-2.0.0a1/warrant/records.py +318 -0
  16. warrant_graph-2.0.0a1/warrant/shapes.py +63 -0
  17. warrant_graph-2.0.0a1/warrant/store.py +469 -0
  18. warrant_graph-2.0.0a1/warrant/triggers.py +138 -0
  19. warrant_graph-2.0.0a1/warrant_agent/__init__.py +21 -0
  20. warrant_graph-2.0.0a1/warrant_agent/_assess_worker.py +60 -0
  21. warrant_graph-2.0.0a1/warrant_agent/api.py +486 -0
  22. warrant_graph-2.0.0a1/warrant_agent/cli.py +58 -0
  23. warrant_graph-2.0.0a1/warrant_agent/computeops.py +177 -0
  24. warrant_graph-2.0.0a1/warrant_agent/config.py +159 -0
  25. warrant_graph-2.0.0a1/warrant_agent/drafts.py +220 -0
  26. warrant_graph-2.0.0a1/warrant_agent/errors.py +24 -0
  27. warrant_graph-2.0.0a1/warrant_agent/glossary.py +102 -0
  28. warrant_graph-2.0.0a1/warrant_agent/handoff.py +283 -0
  29. warrant_graph-2.0.0a1/warrant_agent/init.py +312 -0
  30. warrant_graph-2.0.0a1/warrant_agent/mcp_server.py +238 -0
  31. warrant_graph-2.0.0a1/warrant_agent/objects.py +168 -0
  32. warrant_graph-2.0.0a1/warrant_agent/views.py +427 -0
  33. warrant_graph-2.0.0a1/warrant_graph.egg-info/PKG-INFO +130 -0
  34. warrant_graph-2.0.0a1/warrant_graph.egg-info/SOURCES.txt +36 -0
  35. warrant_graph-2.0.0a1/warrant_graph.egg-info/dependency_links.txt +1 -0
  36. warrant_graph-2.0.0a1/warrant_graph.egg-info/entry_points.txt +4 -0
  37. warrant_graph-2.0.0a1/warrant_graph.egg-info/requires.txt +7 -0
  38. warrant_graph-2.0.0a1/warrant_graph.egg-info/top_level.txt +2 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 s0fractal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: warrant-graph
3
+ Version: 2.0.0a1
4
+ Summary: Record transitions of work with their residual, close loops exactly, and hand the next agent a capsule instead of a transcript.
5
+ Author: s0fractal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/s0fractal/warrant
8
+ Project-URL: Specification, https://github.com/s0fractal/warrant/blob/master/SPEC.md
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: cryptography>=41
13
+ Provides-Extra: compute
14
+ Requires-Dist: sigma-glyph==0.8.0; extra == "compute"
15
+ Provides-Extra: mcp
16
+ Requires-Dist: mcp==2.3.0; extra == "mcp"
17
+ Dynamic: license-file
18
+
19
+ # Warrant
20
+
21
+ Warrant records transitions of work: what was done, on what basis, what it
22
+ established, which exact conditions it closed, which questions it opened, and
23
+ what it leaves unestablished. The next agent reads a short capsule instead of a
24
+ transcript, and cannot credit the work with more than its grounds support.
25
+
26
+ Review is one use. Ordinary decisions, running services, periodic obligations
27
+ and reproducible computations are others.
28
+
29
+ The core is small: six signed record types and one pure function, `assess`,
30
+ that reads one view of a store (its bytes, the reader's own trust file and
31
+ profile, a time) and answers what may be relied on now, what is still owed and
32
+ by whom. The package `warrant` holds the core and the `warrant` command, and
33
+ imports nothing but the Python standard library and `cryptography`;
34
+ `warrant_agent` is an interface for agents on top of it.
35
+
36
+ - **[Warrant in 15 minutes](https://github.com/s0fractal/warrant/blob/master/QUICKSTART.md)**:
37
+ one duty, its closure, an acceptance, a challenge; every command and output on
38
+ the page is run by CI.
39
+ - Contract: [SPEC.md](https://github.com/s0fractal/warrant/blob/master/SPEC.md)
40
+ - Handing work to another agent (Python API, CLI, stdio MCP): [AGENT.md](https://github.com/s0fractal/warrant/blob/master/AGENT.md)
41
+ - What the rebuild retired, and where the previous generation lives: [HISTORY.md](https://github.com/s0fractal/warrant/blob/master/HISTORY.md)
42
+ - The proof-space pilot (goal G9), its contract and resolution layer, outside SPEC.md: [PILOT.md](https://github.com/s0fractal/warrant/blob/master/PILOT.md)
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install --pre warrant-graph # Python ≥ 3.10; depends on `cryptography`
48
+ pip install --pre "warrant-graph[mcp]" # adds `warrant-agent-mcp` on the official MCP SDK
49
+ pip install --pre "warrant-graph[compute]" # adds the Sigma provider (sigma-glyph==0.8.0)
50
+ ```
51
+
52
+ This installs the commands `warrant` and `warrant-agent` and the packages
53
+ `warrant` and `warrant_agent`. The package published as `warrant-verify`
54
+ (≤ 0.12.0) is the retired 0.x generation.
55
+
56
+ Attest and observe need nothing else. `compute` conditions are replayed by the
57
+ Sigma provider, the `sigma_provider` module of the `sigma-glyph` distribution
58
+ (PyPI `0.8.0`, provider identity `0caff793…`; `warrant compute --store S
59
+ provider` prints it). A recipient accepts a provider by putting that identity
60
+ in its profile's `providers`; without an accepted, installed provider every
61
+ REPLAYED basis is UNVERIFIED.
62
+
63
+ ## The model in one table
64
+
65
+ | Record | Says |
66
+ | --- | --- |
67
+ | `duty` | a condition someone owes someone: attest, observe, or compute; optionally scheduled |
68
+ | `transition` | work done: basis, evidence, established, residual, opened questions, exact closures |
69
+ | `accept` | acceptance of one exact closure item (or of a transition's results) |
70
+ | `observation` | what an observer saw, when, with which probe |
71
+ | `grant` | permission to release named duties, attenuable |
72
+ | `challenge` | a counterexample against a closure or a result |
73
+
74
+ Assessment is a pure function of one view: the store's bytes read once, the
75
+ recipient's trust file and profile, a time, and the current revision. It
76
+ answers `reliance` (ELIGIBLE, STALE, CHALLENGED, SUPERSEDED, UNASSESSED,
77
+ NOT_CLOSED, ...), `frontier`, `questions` and `capsule`. A key authenticates; it
78
+ does not authorize. A computation is credited only by the recipient's own replay.
79
+
80
+ ## Status
81
+
82
+ **2.0.0a1 is an alpha**, the first publication of `warrant-graph`. A
83
+ pre-release may still change the record formats, the command line, the Python
84
+ API and the agent interface.
85
+
86
+ - **What is backed.** Every normative sentence of SPEC.md is a signed claim
87
+ record. Of 358 sentences, 281 are BACKED: each names the test cases that
88
+ witness it and the mutants those cases kill. 20 are UNBACKED, each with the
89
+ reason it is still open, and 57 are informative.
90
+ [SPEC-CLAIMS.md](https://github.com/s0fractal/warrant/blob/master/SPEC-CLAIMS.md)
91
+ lists every claim with its witnesses and mutants. A change to a rule is filed
92
+ as a new record that supersedes the old one and says why; the old one stays.
93
+ - **What is not.** Most of the code is exercised by tests but not tied to a
94
+ claim. At 7c6d80b, 304 of the 2317 statements of `warrant/` (13%), and 431 of
95
+ the 3980 statements of `warrant/` and `warrant_agent/` together (11%), are
96
+ changed by a mutant that a BACKED claim cites (`tools/claims.py why
97
+ --uncovered`). Raising that is the open goal G6. The other open goals,
98
+ among them an agent with no preparation using Warrant correctly (G2) and
99
+ admission of witnesses without an admin (G3), are in
100
+ [ROADMAP.md](https://github.com/s0fractal/warrant/blob/master/ROADMAP.md).
101
+ - **Review.** Pull requests are reviewed by a model reviewer through the
102
+ repository's review loop
103
+ ([REVIEWS.md](https://github.com/s0fractal/warrant/blob/master/REVIEWS.md));
104
+ no external security audit is recorded. There is one implementation, this
105
+ Python one.
106
+
107
+ ## Limits
108
+
109
+ Not in this version: threshold adoption, key rotation, P-256, policy
110
+ compilation, grants for acts other than release, goals as a distinct kind, and
111
+ any independent implementation besides this Python one. The process boundary
112
+ around the Sigma kernel isolates module names; it is not an OS sandbox. A view
113
+ cannot prove its own completeness, and every report says so. An assessment
114
+ has aggregate admission limits (records, bytes, `requires` edges, periods;
115
+ SPEC §9): beyond one it is refused, never answered in part.
116
+
117
+ ## The repository
118
+
119
+ The repository also builds and checks Warrant with Warrant: SPEC.md is
120
+ generated from the claim records, the roadmap and the pull request reviews are
121
+ records in the same store, and a gate merges a pull request on a recorded
122
+ acceptance. That machinery (`tools/`, `.warrant/`, `.github/`) is evidence
123
+ behind the package, not part of it: `tests/boundary_test.py` refuses a core
124
+ module that imports it and a wheel or sdist that carries it.
125
+ [docs/REPOSITORY.md](https://github.com/s0fractal/warrant/blob/master/docs/REPOSITORY.md)
126
+ describes it, with the checks to run from a checkout;
127
+ [CHANGELOG.md](https://github.com/s0fractal/warrant/blob/master/CHANGELOG.md)
128
+ records what changed.
129
+
130
+ Licensed MIT. See [TRADEMARK.md](https://github.com/s0fractal/warrant/blob/master/TRADEMARK.md) for the name.
@@ -0,0 +1,112 @@
1
+ # Warrant
2
+
3
+ Warrant records transitions of work: what was done, on what basis, what it
4
+ established, which exact conditions it closed, which questions it opened, and
5
+ what it leaves unestablished. The next agent reads a short capsule instead of a
6
+ transcript, and cannot credit the work with more than its grounds support.
7
+
8
+ Review is one use. Ordinary decisions, running services, periodic obligations
9
+ and reproducible computations are others.
10
+
11
+ The core is small: six signed record types and one pure function, `assess`,
12
+ that reads one view of a store (its bytes, the reader's own trust file and
13
+ profile, a time) and answers what may be relied on now, what is still owed and
14
+ by whom. The package `warrant` holds the core and the `warrant` command, and
15
+ imports nothing but the Python standard library and `cryptography`;
16
+ `warrant_agent` is an interface for agents on top of it.
17
+
18
+ - **[Warrant in 15 minutes](https://github.com/s0fractal/warrant/blob/master/QUICKSTART.md)**:
19
+ one duty, its closure, an acceptance, a challenge; every command and output on
20
+ the page is run by CI.
21
+ - Contract: [SPEC.md](https://github.com/s0fractal/warrant/blob/master/SPEC.md)
22
+ - Handing work to another agent (Python API, CLI, stdio MCP): [AGENT.md](https://github.com/s0fractal/warrant/blob/master/AGENT.md)
23
+ - What the rebuild retired, and where the previous generation lives: [HISTORY.md](https://github.com/s0fractal/warrant/blob/master/HISTORY.md)
24
+ - The proof-space pilot (goal G9), its contract and resolution layer, outside SPEC.md: [PILOT.md](https://github.com/s0fractal/warrant/blob/master/PILOT.md)
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install --pre warrant-graph # Python ≥ 3.10; depends on `cryptography`
30
+ pip install --pre "warrant-graph[mcp]" # adds `warrant-agent-mcp` on the official MCP SDK
31
+ pip install --pre "warrant-graph[compute]" # adds the Sigma provider (sigma-glyph==0.8.0)
32
+ ```
33
+
34
+ This installs the commands `warrant` and `warrant-agent` and the packages
35
+ `warrant` and `warrant_agent`. The package published as `warrant-verify`
36
+ (≤ 0.12.0) is the retired 0.x generation.
37
+
38
+ Attest and observe need nothing else. `compute` conditions are replayed by the
39
+ Sigma provider, the `sigma_provider` module of the `sigma-glyph` distribution
40
+ (PyPI `0.8.0`, provider identity `0caff793…`; `warrant compute --store S
41
+ provider` prints it). A recipient accepts a provider by putting that identity
42
+ in its profile's `providers`; without an accepted, installed provider every
43
+ REPLAYED basis is UNVERIFIED.
44
+
45
+ ## The model in one table
46
+
47
+ | Record | Says |
48
+ | --- | --- |
49
+ | `duty` | a condition someone owes someone: attest, observe, or compute; optionally scheduled |
50
+ | `transition` | work done: basis, evidence, established, residual, opened questions, exact closures |
51
+ | `accept` | acceptance of one exact closure item (or of a transition's results) |
52
+ | `observation` | what an observer saw, when, with which probe |
53
+ | `grant` | permission to release named duties, attenuable |
54
+ | `challenge` | a counterexample against a closure or a result |
55
+
56
+ Assessment is a pure function of one view: the store's bytes read once, the
57
+ recipient's trust file and profile, a time, and the current revision. It
58
+ answers `reliance` (ELIGIBLE, STALE, CHALLENGED, SUPERSEDED, UNASSESSED,
59
+ NOT_CLOSED, ...), `frontier`, `questions` and `capsule`. A key authenticates; it
60
+ does not authorize. A computation is credited only by the recipient's own replay.
61
+
62
+ ## Status
63
+
64
+ **2.0.0a1 is an alpha**, the first publication of `warrant-graph`. A
65
+ pre-release may still change the record formats, the command line, the Python
66
+ API and the agent interface.
67
+
68
+ - **What is backed.** Every normative sentence of SPEC.md is a signed claim
69
+ record. Of 358 sentences, 281 are BACKED: each names the test cases that
70
+ witness it and the mutants those cases kill. 20 are UNBACKED, each with the
71
+ reason it is still open, and 57 are informative.
72
+ [SPEC-CLAIMS.md](https://github.com/s0fractal/warrant/blob/master/SPEC-CLAIMS.md)
73
+ lists every claim with its witnesses and mutants. A change to a rule is filed
74
+ as a new record that supersedes the old one and says why; the old one stays.
75
+ - **What is not.** Most of the code is exercised by tests but not tied to a
76
+ claim. At 7c6d80b, 304 of the 2317 statements of `warrant/` (13%), and 431 of
77
+ the 3980 statements of `warrant/` and `warrant_agent/` together (11%), are
78
+ changed by a mutant that a BACKED claim cites (`tools/claims.py why
79
+ --uncovered`). Raising that is the open goal G6. The other open goals,
80
+ among them an agent with no preparation using Warrant correctly (G2) and
81
+ admission of witnesses without an admin (G3), are in
82
+ [ROADMAP.md](https://github.com/s0fractal/warrant/blob/master/ROADMAP.md).
83
+ - **Review.** Pull requests are reviewed by a model reviewer through the
84
+ repository's review loop
85
+ ([REVIEWS.md](https://github.com/s0fractal/warrant/blob/master/REVIEWS.md));
86
+ no external security audit is recorded. There is one implementation, this
87
+ Python one.
88
+
89
+ ## Limits
90
+
91
+ Not in this version: threshold adoption, key rotation, P-256, policy
92
+ compilation, grants for acts other than release, goals as a distinct kind, and
93
+ any independent implementation besides this Python one. The process boundary
94
+ around the Sigma kernel isolates module names; it is not an OS sandbox. A view
95
+ cannot prove its own completeness, and every report says so. An assessment
96
+ has aggregate admission limits (records, bytes, `requires` edges, periods;
97
+ SPEC §9): beyond one it is refused, never answered in part.
98
+
99
+ ## The repository
100
+
101
+ The repository also builds and checks Warrant with Warrant: SPEC.md is
102
+ generated from the claim records, the roadmap and the pull request reviews are
103
+ records in the same store, and a gate merges a pull request on a recorded
104
+ acceptance. That machinery (`tools/`, `.warrant/`, `.github/`) is evidence
105
+ behind the package, not part of it: `tests/boundary_test.py` refuses a core
106
+ module that imports it and a wheel or sdist that carries it.
107
+ [docs/REPOSITORY.md](https://github.com/s0fractal/warrant/blob/master/docs/REPOSITORY.md)
108
+ describes it, with the checks to run from a checkout;
109
+ [CHANGELOG.md](https://github.com/s0fractal/warrant/blob/master/CHANGELOG.md)
110
+ records what changed.
111
+
112
+ Licensed MIT. See [TRADEMARK.md](https://github.com/s0fractal/warrant/blob/master/TRADEMARK.md) for the name.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # Warrant 2 (SPEC.md). A new distribution name on purpose: `warrant-verify`
7
+ # (<= 0.12.0) is the retired 0.x generation and keeps its own meaning.
8
+ name = "warrant-graph"
9
+ version = "2.0.0a1"
10
+ description = "Record transitions of work with their residual, close loops exactly, and hand the next agent a capsule instead of a transcript."
11
+ readme = "README.md"
12
+ requires-python = ">=3.10"
13
+ license = { text = "MIT" }
14
+ authors = [{ name = "s0fractal" }]
15
+ dependencies = ["cryptography>=41"]
16
+
17
+ [project.optional-dependencies]
18
+ # `compute` conditions need the Sigma provider (sigma_provider in the sigma-glyph
19
+ # distribution, released on PyPI as 0.8.0). Its provider identity is
20
+ # 0caff793d00dd798fa0af89517674c2ca562e1d517e7c969cc40617778c776c5; CI checks the installed bytes against it, since a version
21
+ # pin alone does not name bytes. Without it, attest and observe work and every
22
+ # REPLAYED basis is UNVERIFIED.
23
+ compute = ["sigma-glyph==0.8.0"]
24
+ # The thin stdio MCP front end of warrant_agent (`warrant-agent-mcp`), on the
25
+ # official Python SDK at one exact version.
26
+ mcp = ["mcp==2.3.0"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/s0fractal/warrant"
30
+ Specification = "https://github.com/s0fractal/warrant/blob/master/SPEC.md"
31
+
32
+ [project.scripts]
33
+ warrant = "warrant.cli:main"
34
+ warrant-agent = "warrant_agent.cli:main"
35
+ warrant-agent-mcp = "warrant_agent.mcp_server:main"
36
+
37
+ [tool.setuptools]
38
+ packages = ["warrant", "warrant_agent"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"]
@@ -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))