pgc-assembler 4.0.0__tar.gz → 6.0.0__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.
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/NOTICE +1 -1
- {pgc_assembler-4.0.0/pgc_assembler.egg-info → pgc_assembler-6.0.0}/PKG-INFO +23 -18
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/README.md +20 -17
- pgc_assembler-6.0.0/VERSION +1 -0
- pgc_assembler-6.0.0/assembler/VERSION +1 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/__init__.py +4 -5
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/cli.py +67 -1
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/core.py +80 -13
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/indexes.py +6 -9
- pgc_assembler-6.0.0/assembler/signing.py +229 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0/pgc_assembler.egg-info}/PKG-INFO +23 -18
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/SOURCES.txt +1 -0
- pgc_assembler-6.0.0/pgc_assembler.egg-info/requires.txt +4 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pyproject.toml +8 -1
- pgc_assembler-4.0.0/VERSION +0 -1
- pgc_assembler-4.0.0/assembler/VERSION +0 -1
- pgc_assembler-4.0.0/pgc_assembler.egg-info/requires.txt +0 -1
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/LICENSE +0 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/MANIFEST.in +0 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/_build_hook.py +0 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/conformance.py +0 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/dependency_links.txt +0 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/entry_points.txt +0 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/top_level.txt +0 -0
- {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgc-assembler
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0.0
|
|
4
4
|
Summary: PGC snapshot assembler — composes compiled projections into a manifest-pinned executable snapshot (import package: assembler)
|
|
5
5
|
Author-email: Bhash Ganti <bachipeachy@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,6 +21,8 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
License-File: NOTICE
|
|
23
23
|
Requires-Dist: pyyaml
|
|
24
|
+
Provides-Extra: signing
|
|
25
|
+
Requires-Dist: cryptography>=42; extra == "signing"
|
|
24
26
|
Dynamic: license-file
|
|
25
27
|
|
|
26
28
|
# snapshot_assembler
|
|
@@ -40,8 +42,10 @@ repos → protocol_compiler → each repo's compiled/ projections
|
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
The platform is an ordinary member of the composition — no singleton branch. The composition
|
|
43
|
-
currently assembles **
|
|
44
|
-
and the business domains `ai_governance`, `blockchain` and
|
|
45
|
+
currently assembles **eight domains**: `platform`, `workload`, `inspection`, `transformation`,
|
|
46
|
+
and the business domains `ai_governance`, `blockchain`, `book_library_mgmt` and
|
|
47
|
+
`causal_language_model`. A deliberately narrower composition is assembled by naming its roots in
|
|
48
|
+
`PGC_SOURCE_ROOTS`.
|
|
45
49
|
|
|
46
50
|
## Install
|
|
47
51
|
|
|
@@ -114,7 +118,7 @@ Apache-2.0.
|
|
|
114
118
|
| `pgc-assembler` | `snapshot_assembler` | projections → sealed snapshot |
|
|
115
119
|
| `pgc-runtime` | `protocol_runtime` | snapshot → governed execution |
|
|
116
120
|
| `pgc-inspector` | `snapshot_inspector` | snapshot → read-only inspection |
|
|
117
|
-
| `pgc-transformation` | `
|
|
121
|
+
| `pgc-transformation` | `transformation_compilers` | change request → protocol artifacts |
|
|
118
122
|
| `pgc-governance` | `software_governance` | the governance surface and its capability implementations |
|
|
119
123
|
| `pgc-workloads` | `conformance_workloads` | the workloads that make conformance observable |
|
|
120
124
|
| `pgc-domains` | `business_domains` | the business domain implementations the composed snapshot binds |
|
|
@@ -134,26 +138,27 @@ pgc # reports what is installed and whether the anchor resolves
|
|
|
134
138
|
|
|
135
139
|
`PGC_DOMAIN_ROOTS` names an additional domain contributing its own `registry/structures` — the
|
|
136
140
|
directory that *directly contains* it, not the repository above it; pointing one level too high is a
|
|
137
|
-
silent no-op.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
silent no-op. Where compiled output is written is declared by each build configuration
|
|
142
|
+
(`output_configuration.root`), not by the environment. `PGC_SNAPSHOT_PROFILES` is the directory
|
|
143
|
+
holding snapshot profiles, required by the assembler and the runtime alike. A snapshot's identity
|
|
144
|
+
covers the content of the profile it claims, so the profiles a node reads must be the ones it was
|
|
145
|
+
sealed against.
|
|
141
146
|
|
|
142
|
-
`PGC_BUILD_ROOT` is accepted and reported and **nothing reads it
|
|
143
|
-
anchor that controls output.
|
|
147
|
+
`PGC_BUILD_ROOT` is accepted and reported and **nothing reads it**.
|
|
144
148
|
|
|
145
149
|
The full sequence, with the repositories it needs, is in
|
|
146
150
|
[`pgc_install`](https://github.com/protocol-governed-computing/pgc_install).
|
|
147
151
|
|
|
148
|
-
**Versioning.** Two
|
|
152
|
+
**Versioning.** Two numbers name two different things.
|
|
149
153
|
|
|
150
|
-
- **
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
- **Published** — this distribution's `version` in `pyproject.toml`. It moves on its own. A release
|
|
155
|
+
that changes this repository bumps it, and a release that leaves the repository unchanged keeps it.
|
|
156
|
+
- **Composition** — the public identity (`v6`, …) names one composition of the whole family.
|
|
157
|
+
`pgc_release` records which version of each distribution that composition holds, and `pgc_install`
|
|
158
|
+
pins exactly those versions.
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
Each repository's `VERSION` file holds the workspace's cycle ordinal, which names the `dev/<N>`
|
|
161
|
+
branch. It is development accounting, and it is never published. The standard the packages
|
|
162
|
+
implement is a separate artifact on its own track, and none of these numbers is its revision.
|
|
158
163
|
|
|
159
164
|
The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150616
|
|
@@ -15,8 +15,10 @@ repos → protocol_compiler → each repo's compiled/ projections
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
The platform is an ordinary member of the composition — no singleton branch. The composition
|
|
18
|
-
currently assembles **
|
|
19
|
-
and the business domains `ai_governance`, `blockchain` and
|
|
18
|
+
currently assembles **eight domains**: `platform`, `workload`, `inspection`, `transformation`,
|
|
19
|
+
and the business domains `ai_governance`, `blockchain`, `book_library_mgmt` and
|
|
20
|
+
`causal_language_model`. A deliberately narrower composition is assembled by naming its roots in
|
|
21
|
+
`PGC_SOURCE_ROOTS`.
|
|
20
22
|
|
|
21
23
|
## Install
|
|
22
24
|
|
|
@@ -89,7 +91,7 @@ Apache-2.0.
|
|
|
89
91
|
| `pgc-assembler` | `snapshot_assembler` | projections → sealed snapshot |
|
|
90
92
|
| `pgc-runtime` | `protocol_runtime` | snapshot → governed execution |
|
|
91
93
|
| `pgc-inspector` | `snapshot_inspector` | snapshot → read-only inspection |
|
|
92
|
-
| `pgc-transformation` | `
|
|
94
|
+
| `pgc-transformation` | `transformation_compilers` | change request → protocol artifacts |
|
|
93
95
|
| `pgc-governance` | `software_governance` | the governance surface and its capability implementations |
|
|
94
96
|
| `pgc-workloads` | `conformance_workloads` | the workloads that make conformance observable |
|
|
95
97
|
| `pgc-domains` | `business_domains` | the business domain implementations the composed snapshot binds |
|
|
@@ -109,26 +111,27 @@ pgc # reports what is installed and whether the anchor resolves
|
|
|
109
111
|
|
|
110
112
|
`PGC_DOMAIN_ROOTS` names an additional domain contributing its own `registry/structures` — the
|
|
111
113
|
directory that *directly contains* it, not the repository above it; pointing one level too high is a
|
|
112
|
-
silent no-op.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
silent no-op. Where compiled output is written is declared by each build configuration
|
|
115
|
+
(`output_configuration.root`), not by the environment. `PGC_SNAPSHOT_PROFILES` is the directory
|
|
116
|
+
holding snapshot profiles, required by the assembler and the runtime alike. A snapshot's identity
|
|
117
|
+
covers the content of the profile it claims, so the profiles a node reads must be the ones it was
|
|
118
|
+
sealed against.
|
|
116
119
|
|
|
117
|
-
`PGC_BUILD_ROOT` is accepted and reported and **nothing reads it
|
|
118
|
-
anchor that controls output.
|
|
120
|
+
`PGC_BUILD_ROOT` is accepted and reported and **nothing reads it**.
|
|
119
121
|
|
|
120
122
|
The full sequence, with the repositories it needs, is in
|
|
121
123
|
[`pgc_install`](https://github.com/protocol-governed-computing/pgc_install).
|
|
122
124
|
|
|
123
|
-
**Versioning.** Two
|
|
125
|
+
**Versioning.** Two numbers name two different things.
|
|
124
126
|
|
|
125
|
-
- **
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
- **Published** — this distribution's `version` in `pyproject.toml`. It moves on its own. A release
|
|
128
|
+
that changes this repository bumps it, and a release that leaves the repository unchanged keeps it.
|
|
129
|
+
- **Composition** — the public identity (`v6`, …) names one composition of the whole family.
|
|
130
|
+
`pgc_release` records which version of each distribution that composition holds, and `pgc_install`
|
|
131
|
+
pins exactly those versions.
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
Each repository's `VERSION` file holds the workspace's cycle ordinal, which names the `dev/<N>`
|
|
134
|
+
branch. It is development accounting, and it is never published. The standard the packages
|
|
135
|
+
implement is a separate artifact on its own track, and none of these numbers is its revision.
|
|
133
136
|
|
|
134
137
|
The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150616
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
18
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
18
|
|
@@ -7,12 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _release() -> str:
|
|
10
|
-
"""The
|
|
10
|
+
"""The workspace's cycle ordinal, read from the repo's `VERSION` file.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Never restate it as a literal; two statements of one fact is how they drift apart.
|
|
12
|
+
The ordinal names the development cycle a snapshot was assembled in. It is not the published
|
|
13
|
+
version, which `pyproject.toml` declares on its own. Never restate it as a literal; two
|
|
14
|
+
statements of one fact is how they drift apart.
|
|
16
15
|
"""
|
|
17
16
|
# Source tree and editable installs: the repo-root VERSION is authoritative.
|
|
18
17
|
declared = Path(__file__).resolve().parent.parent / "VERSION"
|
|
@@ -22,7 +22,7 @@ import os
|
|
|
22
22
|
import sys
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
|
-
from assembler import conformance, core
|
|
25
|
+
from assembler import conformance, core, signing
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def _build_parser() -> argparse.ArgumentParser:
|
|
@@ -44,9 +44,19 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
44
44
|
"none cannot have clause 4 of 3b §7 evaluated about it.",
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
+
a.add_argument(
|
|
48
|
+
"--signing-key", metavar="PEM", default=None,
|
|
49
|
+
help="Private key authenticating the sealed snapshot's identity. The build machine "
|
|
50
|
+
"holds this and no node that executes a workflow does: a node able to sign can "
|
|
51
|
+
"mint a snapshot its peers would accept.")
|
|
52
|
+
|
|
47
53
|
v = subs.add_parser("verify", help="Verify an assembled snapshot against its manifest")
|
|
48
54
|
v.add_argument("--out", required=True, metavar="SNAPSHOT_DIR", help="Assembled snapshot dir to verify")
|
|
49
55
|
|
|
56
|
+
k = subs.add_parser("keygen", help="Mint a signing keypair for a snapshot trust root")
|
|
57
|
+
k.add_argument("--private", required=True, metavar="PEM", help="Where to write the private half")
|
|
58
|
+
k.add_argument("--public", required=True, metavar="PEM", help="Where to write the public half")
|
|
59
|
+
|
|
50
60
|
c = subs.add_parser("conform", help="Run Composition Conformance against an assembled snapshot")
|
|
51
61
|
c.add_argument("--out", required=True, metavar="SNAPSHOT_DIR", help="Assembled snapshot dir to check")
|
|
52
62
|
|
|
@@ -58,6 +68,37 @@ def _fatal(msg: str) -> None:
|
|
|
58
68
|
sys.exit(1)
|
|
59
69
|
|
|
60
70
|
|
|
71
|
+
def _report_authentication(out_root: Path, snapshot_id: str) -> None:
|
|
72
|
+
"""Report whether the snapshot is authenticated, and refuse where this party requires it.
|
|
73
|
+
|
|
74
|
+
A party holding a trust root is a party that requires one. Absent `PGC_TRUST_ROOT_PUBKEY`
|
|
75
|
+
this reports what it finds and refuses nothing: an unsigned snapshot is well-formed, and a
|
|
76
|
+
platform whose profile does not select signing must still verify. Holding an anchor is the
|
|
77
|
+
opt-in, and it lives outside the artifact being authenticated — which is also why the key
|
|
78
|
+
is read from here and never from the signature record.
|
|
79
|
+
"""
|
|
80
|
+
anchor = os.environ.get("PGC_TRUST_ROOT_PUBKEY")
|
|
81
|
+
try:
|
|
82
|
+
record = signing.read_signature(out_root)
|
|
83
|
+
except signing.SignatureMalformed as exc:
|
|
84
|
+
_fatal(f"snapshot authentication: {exc}")
|
|
85
|
+
|
|
86
|
+
if not anchor:
|
|
87
|
+
state = (f"present, unchecked (signed under key {record.get('key_id', '?')})"
|
|
88
|
+
if record else "absent")
|
|
89
|
+
print(f"[signing] {state} — set PGC_TRUST_ROOT_PUBKEY to require and check one")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if record is None:
|
|
93
|
+
_fatal("snapshot authentication: this party anchors a trust root and the snapshot "
|
|
94
|
+
"carries no signature. An unsigned snapshot is not one this party accepts.")
|
|
95
|
+
try:
|
|
96
|
+
key = signing.verify_identity(snapshot_id, record, Path(anchor))
|
|
97
|
+
except (signing.SignatureError, signing.SigningUnavailable, OSError) as exc:
|
|
98
|
+
_fatal(f"snapshot authentication: {type(exc).__name__}: {exc}")
|
|
99
|
+
print(f"[signing] AUTHENTICATED under trust root {key}")
|
|
100
|
+
|
|
101
|
+
|
|
61
102
|
def main() -> None:
|
|
62
103
|
args = _build_parser().parse_args()
|
|
63
104
|
|
|
@@ -88,6 +129,17 @@ def main() -> None:
|
|
|
88
129
|
print(f"[conformance] composition: {ev['status']} "
|
|
89
130
|
f"({ev['rules_evaluated']} rule(s) over {ev['artifacts_examined']} artifacts)")
|
|
90
131
|
|
|
132
|
+
# Signing follows sealing and never precedes it: what is authenticated is the identity
|
|
133
|
+
# the seal produced. A snapshot is sealed whether or not anyone signs it; a signature
|
|
134
|
+
# says who vouches for the one that was sealed.
|
|
135
|
+
if args.signing_key:
|
|
136
|
+
try:
|
|
137
|
+
record = signing.sign_identity(manifest["snapshot_id"], Path(args.signing_key))
|
|
138
|
+
path = signing.write_signature(out_root, record)
|
|
139
|
+
except (signing.SigningUnavailable, OSError, ValueError) as exc:
|
|
140
|
+
_fatal(f"signing failed: {exc}")
|
|
141
|
+
print(f"[signing] signed under key {record['key_id']} -> {path}")
|
|
142
|
+
|
|
91
143
|
elif args.command == "verify":
|
|
92
144
|
out_root = Path(args.out).resolve()
|
|
93
145
|
try:
|
|
@@ -96,6 +148,20 @@ def main() -> None:
|
|
|
96
148
|
_fatal(str(exc))
|
|
97
149
|
print(f"[assembler] VERIFIED snapshot_id={manifest['snapshot_id']}")
|
|
98
150
|
print(f"[assembler] domains: {json.dumps([d['domain'] for d in manifest['domains']])}")
|
|
151
|
+
_report_authentication(out_root, manifest["snapshot_id"])
|
|
152
|
+
|
|
153
|
+
elif args.command == "keygen":
|
|
154
|
+
try:
|
|
155
|
+
private_pem, public_pem = signing.generate_keypair()
|
|
156
|
+
except signing.SigningUnavailable as exc:
|
|
157
|
+
_fatal(str(exc))
|
|
158
|
+
priv, pub = Path(args.private), Path(args.public)
|
|
159
|
+
priv.write_bytes(private_pem)
|
|
160
|
+
priv.chmod(0o600)
|
|
161
|
+
pub.write_bytes(public_pem)
|
|
162
|
+
print(f"[keygen] key_id: {signing.key_id_from_public_pem(public_pem)}")
|
|
163
|
+
print(f"[keygen] private: {priv} (build machine only — never on an executing node)")
|
|
164
|
+
print(f"[keygen] public: {pub} (each node holds this, from outside the snapshot)")
|
|
99
165
|
|
|
100
166
|
elif args.command == "conform":
|
|
101
167
|
out_root = Path(args.out).resolve()
|
|
@@ -18,7 +18,9 @@ Invariants enforced here:
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import hashlib
|
|
21
|
+
import importlib.metadata
|
|
21
22
|
import json
|
|
23
|
+
import re
|
|
22
24
|
import shutil
|
|
23
25
|
import subprocess
|
|
24
26
|
from dataclasses import dataclass
|
|
@@ -182,7 +184,13 @@ def _identity_view(domains: list[dict]) -> list[dict]:
|
|
|
182
184
|
# it is a determination ABOUT the snapshot, in the sense `3b` §7 means, and evidence rather than
|
|
183
185
|
# content. That it is written inside the tree is a placement defect recorded in Task D; excluding it
|
|
184
186
|
# here is what makes the tree honest about what it sealed, not a carve-out to make a check pass.
|
|
185
|
-
|
|
187
|
+
# A signature is the same shape as a conformance result and joins this set for the same reason:
|
|
188
|
+
# it is over `snapshot_id`, so it cannot exist before the identity does, and it records who
|
|
189
|
+
# vouches for what was sealed rather than forming part of it. The placement defect noted above
|
|
190
|
+
# applies to it equally — it is written inside the tree and is not of it. That is an existing
|
|
191
|
+
# defect extended to a second file, not a new class of one, and not a carve-out invented to
|
|
192
|
+
# admit signing: a signature that were a constituent would change the identity it signs.
|
|
193
|
+
POST_SEAL = ("conformance/", "signature.json")
|
|
186
194
|
|
|
187
195
|
|
|
188
196
|
def _is_post_seal(rel: str) -> bool:
|
|
@@ -242,16 +250,20 @@ def enumerate_constituents(out_root: Path) -> list[dict[str, str]]:
|
|
|
242
250
|
return out
|
|
243
251
|
|
|
244
252
|
|
|
245
|
-
def compute_snapshot_identity(domains: list[dict], constituents: list[dict], profile: str
|
|
253
|
+
def compute_snapshot_identity(domains: list[dict], constituents: list[dict], profile: str,
|
|
254
|
+
profile_sha256: str) -> str:
|
|
246
255
|
"""The snapshot's identity, derived from content and covering every constituent (SN-2, SN-6).
|
|
247
256
|
|
|
248
|
-
|
|
257
|
+
Four inputs, and each is there for a stated reason:
|
|
249
258
|
|
|
250
259
|
* the domains identity view — the semantic identity of what was compiled;
|
|
251
260
|
* every constituent, by path and by a hash OF ITS BYTES — so that changing any carried file
|
|
252
261
|
changes the identity, which is SN-2's totality clause;
|
|
253
262
|
* the claimed profile — a self-description that claims a profile and does not cover the claim
|
|
254
|
-
would let the claim change without the identity changing (SN-5, SN-6)
|
|
263
|
+
would let the claim change without the identity changing (SN-5, SN-6);
|
|
264
|
+
* the claimed profile's content — the name alone let a profile be weakened, requirements
|
|
265
|
+
removed from it, without the identity of any snapshot claiming it changing. The digest is
|
|
266
|
+
taken over the profile's declaration (`profile_sha256`).
|
|
255
267
|
|
|
256
268
|
Provenance is excluded and that exclusion is declared in the manifest's `identity_covers`.
|
|
257
269
|
It is observational content (`3e` §5): `assembled_at` differs between two assemblies of identical
|
|
@@ -259,7 +271,8 @@ def compute_snapshot_identity(domains: list[dict], constituents: list[dict], pro
|
|
|
259
271
|
SN-6. What SN-6 requires covered is the description's determinative content, which this is.
|
|
260
272
|
"""
|
|
261
273
|
payload = json.dumps(
|
|
262
|
-
{"domains": _identity_view(domains), "constituents": constituents, "profile": profile
|
|
274
|
+
{"domains": _identity_view(domains), "constituents": constituents, "profile": profile,
|
|
275
|
+
"profile_sha256": profile_sha256},
|
|
263
276
|
sort_keys=True, separators=(",", ":"),
|
|
264
277
|
)
|
|
265
278
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
@@ -326,6 +339,19 @@ def _profile_declaration(identity: str) -> dict[str, Any]:
|
|
|
326
339
|
f"{root} — a claim nobody can read is not a claim (3b SN-7)")
|
|
327
340
|
|
|
328
341
|
|
|
342
|
+
def profile_digest(profile: str) -> str:
|
|
343
|
+
"""The digest of the claimed profile's declaration, as the identity covers it.
|
|
344
|
+
|
|
345
|
+
Taken over the `snapshot_profile` block in canonical form rather than over the file: what the
|
|
346
|
+
profile requires is its declaration, and a change to the prose around it changes nothing a
|
|
347
|
+
snapshot is checked against.
|
|
348
|
+
"""
|
|
349
|
+
declared = _profile_declaration(profile)
|
|
350
|
+
return hashlib.sha256(
|
|
351
|
+
json.dumps(declared, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
|
|
352
|
+
).hexdigest()
|
|
353
|
+
|
|
354
|
+
|
|
329
355
|
def verify_profile(out_root: Path, profile: str) -> list[str]:
|
|
330
356
|
"""What the claimed profile requires and the composition does not carry.
|
|
331
357
|
|
|
@@ -426,16 +452,37 @@ def _git_commit(repo_root: Path | None) -> str:
|
|
|
426
452
|
return "unknown"
|
|
427
453
|
|
|
428
454
|
|
|
455
|
+
def _distribution(repo_root: Path | None) -> tuple[str, str] | None:
|
|
456
|
+
"""The distribution a repo publishes, as its pyproject declares it: `(name, version)`.
|
|
457
|
+
|
|
458
|
+
Read by line rather than parsed as TOML, because `tomllib` is not in every Python this package
|
|
459
|
+
supports. A repo that publishes nothing has no pyproject and records nothing.
|
|
460
|
+
"""
|
|
461
|
+
if repo_root is None or not (repo_root / "pyproject.toml").is_file():
|
|
462
|
+
return None
|
|
463
|
+
text = (repo_root / "pyproject.toml").read_text(encoding="utf-8")
|
|
464
|
+
name = re.search(r'^name\s*=\s*"([^"]+)"', text, re.M)
|
|
465
|
+
version = re.search(r'^version\s*=\s*"([^"]+)"', text, re.M)
|
|
466
|
+
return (name.group(1), version.group(1)) if name and version else None
|
|
467
|
+
|
|
468
|
+
|
|
429
469
|
def _build_provenance(inputs: list[DomainInput], domains: list[dict]) -> dict[str, Any]:
|
|
430
470
|
source_commits: dict[str, str] = {}
|
|
471
|
+
component_versions: dict[str, str] = {}
|
|
431
472
|
for inp in inputs:
|
|
432
473
|
if inp.repo_root is not None:
|
|
433
474
|
source_commits[inp.repo_root.name] = _git_commit(inp.repo_root)
|
|
475
|
+
published = _distribution(inp.repo_root)
|
|
476
|
+
if published:
|
|
477
|
+
component_versions[published[0]] = published[1]
|
|
478
|
+
# The assembler names its own distribution: it is the one component certainly at work here.
|
|
479
|
+
component_versions["pgc-assembler"] = importlib.metadata.version("pgc-assembler")
|
|
434
480
|
compiler_versions = {d["domain"]: d["compiler_version"] for d in domains}
|
|
435
481
|
return {
|
|
436
482
|
"assembler_version": ASSEMBLER_VERSION,
|
|
437
483
|
"assembled_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
438
484
|
"source_commits": source_commits,
|
|
485
|
+
"component_versions": dict(sorted(component_versions.items())),
|
|
439
486
|
"compiler_versions": compiler_versions,
|
|
440
487
|
}
|
|
441
488
|
|
|
@@ -515,7 +562,8 @@ def assemble(source_roots: list[Path], out_root: Path, profile: str) -> dict[str
|
|
|
515
562
|
# `3b` §6 requires all five, with the enumeration total. It was four per domain; six top-level
|
|
516
563
|
# constituents were carried and enumerated nowhere, which under §6 is undeclared content.
|
|
517
564
|
constituents = enumerate_constituents(out_root)
|
|
518
|
-
|
|
565
|
+
sealed_profile = profile_digest(profile)
|
|
566
|
+
snapshot_id = compute_snapshot_identity(domains, constituents, profile, sealed_profile)
|
|
519
567
|
manifest = {
|
|
520
568
|
"manifest_version": MANIFEST_VERSION,
|
|
521
569
|
"snapshot_id": snapshot_id,
|
|
@@ -523,11 +571,14 @@ def assemble(source_roots: list[Path], out_root: Path, profile: str) -> dict[str
|
|
|
523
571
|
# What the identity is taken over, declared so a party who did not build this snapshot can
|
|
524
572
|
# recompute it without reading the assembler (AI-16). Provenance is excluded as observational
|
|
525
573
|
# content (`3e` §5) — see compute_snapshot_identity.
|
|
526
|
-
"identity_covers": ["domains", "constituents", "profile"],
|
|
574
|
+
"identity_covers": ["domains", "constituents", "profile", "profile_sha256"],
|
|
527
575
|
# Written after sealing and therefore outside what was sealed. Declared rather than silently
|
|
528
576
|
# skipped, so a party recomputing the identity knows exactly what to exclude (AI-16).
|
|
529
577
|
"post_seal": list(POST_SEAL),
|
|
530
578
|
"profile": profile,
|
|
579
|
+
# What the claimed profile declared when this snapshot was sealed. Acceptance recomputes it
|
|
580
|
+
# from the profile as it now reads and refuses a difference.
|
|
581
|
+
"profile_sha256": sealed_profile,
|
|
531
582
|
"domains": domains,
|
|
532
583
|
"constituents": constituents,
|
|
533
584
|
"provenance": _build_provenance(inputs, domains),
|
|
@@ -547,7 +598,9 @@ def assemble(source_roots: list[Path], out_root: Path, profile: str) -> dict[str
|
|
|
547
598
|
# Kinds a domain build instantiates. MUST match the compiler's _inject_imported_governance
|
|
548
599
|
# filter (compiler/stages/s1_extract.py): a governance invariant is imported into a domain iff
|
|
549
600
|
# its applies_to_kinds intersects this set and it declares no layer/surface scope.
|
|
550
|
-
|
|
601
|
+
# TEST_DATA joined when transform vectors moved into the supplying domain's build
|
|
602
|
+
# (conformance::CONSTITUTION_TEST_DATA_V0).
|
|
603
|
+
_DOMAIN_INSTANTIATED = frozenset({"WF", "CC", "CS", "CT", "RB", "AC", "IN", "EV", "TI", "TE", "TEST_DATA"})
|
|
551
604
|
|
|
552
605
|
|
|
553
606
|
def _recompute_governance_closure(out_root: Path, source_domain: str) -> tuple[str, int]:
|
|
@@ -675,11 +728,12 @@ def verify_snapshot(out_root: Path) -> dict[str, Any]:
|
|
|
675
728
|
3. identity — recomputed from the RECOMPUTED constituent hashes, compared to the identity
|
|
676
729
|
borne. Recomputing over recorded hashes detects a tampered manifest; only
|
|
677
730
|
recomputing over recomputed hashes detects a tampered constituent
|
|
678
|
-
4. profile — the snapshot claims one
|
|
731
|
+
4. profile — the snapshot claims one, and the profile it claims is the one it was sealed
|
|
732
|
+
against: its declaration digests to the value the identity covers
|
|
679
733
|
|
|
680
|
-
|
|
681
|
-
against attestation —
|
|
682
|
-
who did not build the snapshot (AI-16).
|
|
734
|
+
Every check recomputes from content. Comparing recorded values to recorded values — manifest
|
|
735
|
+
against metadata.json against attestation — is transitive from construction and establishes
|
|
736
|
+
nothing to a party who did not build the snapshot (AI-16).
|
|
683
737
|
|
|
684
738
|
On any failure the snapshot is refused whole (SN-9). There is no partial acceptance.
|
|
685
739
|
"""
|
|
@@ -741,7 +795,20 @@ def verify_snapshot(out_root: Path) -> dict[str, Any]:
|
|
|
741
795
|
+ "; ".join(unmet[:5])
|
|
742
796
|
+ (f"; and {len(unmet) - 5} more" if len(unmet) > 5 else ""))
|
|
743
797
|
|
|
744
|
-
|
|
798
|
+
# The profile read now, not the digest the manifest carries: recomputing over the recorded value
|
|
799
|
+
# would accept a profile weakened after sealing, which is what covering its content is for.
|
|
800
|
+
sealed_profile = manifest.get("profile_sha256")
|
|
801
|
+
if not sealed_profile:
|
|
802
|
+
raise AssemblyError(
|
|
803
|
+
"manifest carries no profile_sha256 — the profile's content is not in the identity, so "
|
|
804
|
+
"a weakened profile could not be told from the one this snapshot was sealed against.")
|
|
805
|
+
current_profile = profile_digest(profile)
|
|
806
|
+
if current_profile != sealed_profile:
|
|
807
|
+
raise AssemblyError(
|
|
808
|
+
f"profile failure: {profile} now digests to {current_profile[:16]}…, and this snapshot "
|
|
809
|
+
f"was sealed against {sealed_profile[:16]}…. The profile it claims has changed since.")
|
|
810
|
+
|
|
811
|
+
derived = compute_snapshot_identity(domains, recomputed, profile, current_profile)
|
|
745
812
|
borne = manifest.get("snapshot_id")
|
|
746
813
|
if derived != borne:
|
|
747
814
|
raise AssemblyError(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
indexes.py — cross-domain query indexes over the assembled snapshot.
|
|
3
3
|
|
|
4
4
|
Three inspection indexes, built AFTER composition (the assembler is the only point with the full
|
|
5
|
-
federated view of all domains
|
|
5
|
+
federated view of all domains):
|
|
6
6
|
|
|
7
7
|
* artifact_index/index.json — FQDN → domain / kind / owner_subdomain / canonical_path /
|
|
8
8
|
evidence_paths / per-domain addresses. Consumed by `si`.
|
|
@@ -43,14 +43,11 @@ def _authoring_rank(doc: dict) -> int:
|
|
|
43
43
|
capability is executable there, and the authoring stays platform-owned. Both copies are
|
|
44
44
|
canonical and both are legitimate; they are not interchangeable.
|
|
45
45
|
|
|
46
|
-
The binding copy
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
A binding copy also carries the `content_hash` of content it does not hold, which is the sharper
|
|
53
|
-
tell but not one to select on: a hash mismatch is a defect to report, not a discriminator.
|
|
46
|
+
The binding copy records exactly what the authoring copy records, so the two are the same
|
|
47
|
+
document, and `si snapshot validate`'s `republished_copies_agree` refuses any pair that is not.
|
|
48
|
+
An identity resolves to a copy that carries its authored content — `si.artifact.show` promises
|
|
49
|
+
"the published canonical artifact for an FQDN, as authored" — so a copy without content never
|
|
50
|
+
wins over one with it.
|
|
54
51
|
"""
|
|
55
52
|
return 1 if (doc.get("content") or "").strip() else 0
|
|
56
53
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Snapshot authentication — a signature over a sealed snapshot's identity.
|
|
2
|
+
|
|
3
|
+
Three things are kept apart here, and conflating any two of them defeats the purpose:
|
|
4
|
+
|
|
5
|
+
snapshot identity `snapshot_id`, derived from what was sealed
|
|
6
|
+
snapshot authentication a signature over that identity
|
|
7
|
+
trust anchor a public key the verifying party holds, from outside the snapshot
|
|
8
|
+
|
|
9
|
+
Verification is `Verify(K, I, sigma)` where **K comes from the node and never from the
|
|
10
|
+
artifact**. A snapshot carrying the key that authenticates it is self-authenticating, which
|
|
11
|
+
establishes nothing: whoever replaced the snapshot would replace the key with it. The
|
|
12
|
+
`key_id` in a signature record exists only so a refusal can say *which* failure occurred —
|
|
13
|
+
signed by a key this node does not anchor, versus signed badly. It MUST NOT be used to
|
|
14
|
+
select a key.
|
|
15
|
+
|
|
16
|
+
**What is signed is the identity, not the manifest.** The self-description sits outside the
|
|
17
|
+
identity it declares, so signing the file would bind the signature to bytes the identity does
|
|
18
|
+
not cover. And the record is a sidecar rather than a manifest field, because a sealed
|
|
19
|
+
snapshot's manifest is written once and never rewritten.
|
|
20
|
+
|
|
21
|
+
**Asymmetric by requirement, not by preference.** A profile requiring that a node able to
|
|
22
|
+
verify is unable to sign rules out every shared-secret construction, whatever its strength.
|
|
23
|
+
|
|
24
|
+
The dependency is an extra (`snapshot_assembler[signing]`) rather than a core requirement.
|
|
25
|
+
Signing is something a profile selects; a platform whose profile does not select it should
|
|
26
|
+
not acquire a cryptography dependency because the capability exists elsewhere. Absent the
|
|
27
|
+
extra, this module refuses — it never degrades to an unsigned success.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import base64
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
SIGNATURE_FILENAME = "signature.json"
|
|
40
|
+
SIGNATURE_VERSION = "V0"
|
|
41
|
+
ALGORITHM = "ed25519"
|
|
42
|
+
|
|
43
|
+
# Fields recorded beside the signature that the signature does not cover. `signed_at` says
|
|
44
|
+
# when signing happened; it is an accompanying fact, and covering it would make an identical
|
|
45
|
+
# snapshot produce a different record on every run.
|
|
46
|
+
ACCOMPANYING = ("signed_at",)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SigningUnavailable(RuntimeError):
|
|
50
|
+
"""The signing extra is not installed. Never a reason to proceed unsigned."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SignatureError(RuntimeError):
|
|
54
|
+
"""Base for every refusal this module issues."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SignatureAbsent(SignatureError):
|
|
58
|
+
"""The snapshot carries no signature and the verifying party requires one."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SignatureMalformed(SignatureError):
|
|
62
|
+
"""A signature record exists and cannot be read as one."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TrustRootMismatch(SignatureError):
|
|
66
|
+
"""Signed by a key this party does not anchor. Distinct from a bad signature."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SignatureInvalid(SignatureError):
|
|
70
|
+
"""Anchored by the right key and the signature does not verify."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class IdentityMismatch(SignatureError):
|
|
74
|
+
"""The signature authenticates an identity the snapshot does not claim."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ed25519():
|
|
78
|
+
"""The primitives, or a refusal naming what to install.
|
|
79
|
+
|
|
80
|
+
Imported here rather than at module scope so that reading this module, and importing the
|
|
81
|
+
assembler at all, costs nothing when signing is not in use.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
from cryptography.hazmat.primitives import serialization
|
|
85
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
86
|
+
except ImportError as exc: # pragma: no cover - exercised by absence, not by tests
|
|
87
|
+
raise SigningUnavailable(
|
|
88
|
+
"snapshot signing requires the 'signing' extra: "
|
|
89
|
+
"pip install 'snapshot-assembler[signing]'"
|
|
90
|
+
) from exc
|
|
91
|
+
return serialization, ed25519
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def key_id(public_key_bytes: bytes) -> str:
|
|
95
|
+
"""A short, stable name for a public key.
|
|
96
|
+
|
|
97
|
+
Identifies which anchor a record was produced under so that a refusal can distinguish a
|
|
98
|
+
key this party does not hold from a signature that does not verify. It is a label, never
|
|
99
|
+
a credential.
|
|
100
|
+
"""
|
|
101
|
+
return hashlib.sha256(public_key_bytes).hexdigest()[:16]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def key_id_from_public_pem(public_pem: bytes) -> str:
|
|
105
|
+
"""The `key_id` of a PEM-encoded public key, for reporting at mint time."""
|
|
106
|
+
serialization, _ = _ed25519()
|
|
107
|
+
return key_id(_raw_public(serialization.load_pem_public_key(public_pem)))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def generate_keypair() -> tuple[bytes, bytes]:
|
|
111
|
+
"""A new Ed25519 pair as (private_pem, public_pem).
|
|
112
|
+
|
|
113
|
+
The private half belongs on the machine that seals snapshots and on no machine that
|
|
114
|
+
executes them: a node able to sign can mint a snapshot its peers would accept.
|
|
115
|
+
"""
|
|
116
|
+
serialization, ed25519 = _ed25519()
|
|
117
|
+
private = ed25519.Ed25519PrivateKey.generate()
|
|
118
|
+
private_pem = private.private_bytes(
|
|
119
|
+
encoding=serialization.Encoding.PEM,
|
|
120
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
121
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
122
|
+
)
|
|
123
|
+
public_pem = private.public_key().public_bytes(
|
|
124
|
+
encoding=serialization.Encoding.PEM,
|
|
125
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
126
|
+
)
|
|
127
|
+
return private_pem, public_pem
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _raw_public(public_key) -> bytes:
|
|
131
|
+
serialization, _ = _ed25519()
|
|
132
|
+
return public_key.public_bytes(
|
|
133
|
+
encoding=serialization.Encoding.Raw,
|
|
134
|
+
format=serialization.PublicFormat.Raw,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def sign_identity(snapshot_id: str, private_key_path: Path) -> dict[str, Any]:
|
|
139
|
+
"""Produce the signature record for a sealed snapshot's identity."""
|
|
140
|
+
serialization, _ = _ed25519()
|
|
141
|
+
private = serialization.load_pem_private_key(
|
|
142
|
+
Path(private_key_path).read_bytes(), password=None
|
|
143
|
+
)
|
|
144
|
+
signature = private.sign(snapshot_id.encode("utf-8"))
|
|
145
|
+
return {
|
|
146
|
+
"signature_version": SIGNATURE_VERSION,
|
|
147
|
+
"snapshot_id": snapshot_id,
|
|
148
|
+
"algorithm": ALGORITHM,
|
|
149
|
+
"key_id": key_id(_raw_public(private.public_key())),
|
|
150
|
+
"signature": base64.b64encode(signature).decode("ascii"),
|
|
151
|
+
"signed_at": datetime.now(timezone.utc).isoformat(),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def write_signature(out_root: Path, record: dict[str, Any]) -> Path:
|
|
156
|
+
"""Write the sidecar beside the manifest, leaving the manifest untouched."""
|
|
157
|
+
path = Path(out_root) / SIGNATURE_FILENAME
|
|
158
|
+
path.write_text(json.dumps(record, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
159
|
+
return path
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def read_signature(out_root: Path) -> dict[str, Any] | None:
|
|
163
|
+
"""The signature record, or None where the snapshot carries none."""
|
|
164
|
+
path = Path(out_root) / SIGNATURE_FILENAME
|
|
165
|
+
if not path.exists():
|
|
166
|
+
return None
|
|
167
|
+
try:
|
|
168
|
+
record = json.loads(path.read_text(encoding="utf-8"))
|
|
169
|
+
except json.JSONDecodeError as exc:
|
|
170
|
+
raise SignatureMalformed(f"{path} is not readable as JSON: {exc}") from exc
|
|
171
|
+
if not isinstance(record, dict):
|
|
172
|
+
raise SignatureMalformed(f"{path} does not carry a signature record")
|
|
173
|
+
return record
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def verify_identity(snapshot_id: str, record: dict[str, Any], public_key_path: Path) -> str:
|
|
177
|
+
"""Verify a signature over a claimed identity against a party-supplied anchor.
|
|
178
|
+
|
|
179
|
+
Returns the `key_id` of the anchor on success. Raises one of this module's refusals
|
|
180
|
+
otherwise, each naming which of the four failures occurred: the record authenticates a
|
|
181
|
+
different identity, it was produced under a key this party does not anchor, the record is
|
|
182
|
+
unreadable, or it does not verify.
|
|
183
|
+
|
|
184
|
+
The anchor is the caller's. `record["key_id"]` is read only to report a mismatch.
|
|
185
|
+
"""
|
|
186
|
+
serialization, ed25519 = _ed25519()
|
|
187
|
+
|
|
188
|
+
for field in ("snapshot_id", "algorithm", "signature"):
|
|
189
|
+
if field not in record:
|
|
190
|
+
raise SignatureMalformed(f"signature record carries no {field!r}")
|
|
191
|
+
|
|
192
|
+
if record["algorithm"] != ALGORITHM:
|
|
193
|
+
raise SignatureMalformed(
|
|
194
|
+
f"signature algorithm is {record['algorithm']!r}; this verifier knows {ALGORITHM!r}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Checked before the cryptography: a record that verifies over a different identity is
|
|
198
|
+
# valid and irrelevant, and reporting it as a bad signature would name the wrong fault.
|
|
199
|
+
if record["snapshot_id"] != snapshot_id:
|
|
200
|
+
raise IdentityMismatch(
|
|
201
|
+
f"signature authenticates {record['snapshot_id'][:16]}… and this snapshot claims "
|
|
202
|
+
f"{snapshot_id[:16]}…"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
public = serialization.load_pem_public_key(Path(public_key_path).read_bytes())
|
|
206
|
+
if not isinstance(public, ed25519.Ed25519PublicKey):
|
|
207
|
+
raise SignatureMalformed(f"{public_key_path} is not an Ed25519 public key")
|
|
208
|
+
anchor = key_id(_raw_public(public))
|
|
209
|
+
|
|
210
|
+
borne = record.get("key_id")
|
|
211
|
+
if borne and borne != anchor:
|
|
212
|
+
raise TrustRootMismatch(
|
|
213
|
+
f"signed under key {borne} and this party anchors {anchor} — the signature may be "
|
|
214
|
+
f"sound and is not one this party accepts"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
signature = base64.b64decode(record["signature"], validate=True)
|
|
219
|
+
except (ValueError, TypeError) as exc:
|
|
220
|
+
raise SignatureMalformed(f"signature is not valid base64: {exc}") from exc
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
public.verify(signature, snapshot_id.encode("utf-8"))
|
|
224
|
+
except Exception as exc:
|
|
225
|
+
raise SignatureInvalid(
|
|
226
|
+
f"signature over {snapshot_id[:16]}… does not verify under key {anchor}"
|
|
227
|
+
) from exc
|
|
228
|
+
|
|
229
|
+
return anchor
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgc-assembler
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0.0
|
|
4
4
|
Summary: PGC snapshot assembler — composes compiled projections into a manifest-pinned executable snapshot (import package: assembler)
|
|
5
5
|
Author-email: Bhash Ganti <bachipeachy@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,6 +21,8 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
License-File: NOTICE
|
|
23
23
|
Requires-Dist: pyyaml
|
|
24
|
+
Provides-Extra: signing
|
|
25
|
+
Requires-Dist: cryptography>=42; extra == "signing"
|
|
24
26
|
Dynamic: license-file
|
|
25
27
|
|
|
26
28
|
# snapshot_assembler
|
|
@@ -40,8 +42,10 @@ repos → protocol_compiler → each repo's compiled/ projections
|
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
The platform is an ordinary member of the composition — no singleton branch. The composition
|
|
43
|
-
currently assembles **
|
|
44
|
-
and the business domains `ai_governance`, `blockchain` and
|
|
45
|
+
currently assembles **eight domains**: `platform`, `workload`, `inspection`, `transformation`,
|
|
46
|
+
and the business domains `ai_governance`, `blockchain`, `book_library_mgmt` and
|
|
47
|
+
`causal_language_model`. A deliberately narrower composition is assembled by naming its roots in
|
|
48
|
+
`PGC_SOURCE_ROOTS`.
|
|
45
49
|
|
|
46
50
|
## Install
|
|
47
51
|
|
|
@@ -114,7 +118,7 @@ Apache-2.0.
|
|
|
114
118
|
| `pgc-assembler` | `snapshot_assembler` | projections → sealed snapshot |
|
|
115
119
|
| `pgc-runtime` | `protocol_runtime` | snapshot → governed execution |
|
|
116
120
|
| `pgc-inspector` | `snapshot_inspector` | snapshot → read-only inspection |
|
|
117
|
-
| `pgc-transformation` | `
|
|
121
|
+
| `pgc-transformation` | `transformation_compilers` | change request → protocol artifacts |
|
|
118
122
|
| `pgc-governance` | `software_governance` | the governance surface and its capability implementations |
|
|
119
123
|
| `pgc-workloads` | `conformance_workloads` | the workloads that make conformance observable |
|
|
120
124
|
| `pgc-domains` | `business_domains` | the business domain implementations the composed snapshot binds |
|
|
@@ -134,26 +138,27 @@ pgc # reports what is installed and whether the anchor resolves
|
|
|
134
138
|
|
|
135
139
|
`PGC_DOMAIN_ROOTS` names an additional domain contributing its own `registry/structures` — the
|
|
136
140
|
directory that *directly contains* it, not the repository above it; pointing one level too high is a
|
|
137
|
-
silent no-op.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
silent no-op. Where compiled output is written is declared by each build configuration
|
|
142
|
+
(`output_configuration.root`), not by the environment. `PGC_SNAPSHOT_PROFILES` is the directory
|
|
143
|
+
holding snapshot profiles, required by the assembler and the runtime alike. A snapshot's identity
|
|
144
|
+
covers the content of the profile it claims, so the profiles a node reads must be the ones it was
|
|
145
|
+
sealed against.
|
|
141
146
|
|
|
142
|
-
`PGC_BUILD_ROOT` is accepted and reported and **nothing reads it
|
|
143
|
-
anchor that controls output.
|
|
147
|
+
`PGC_BUILD_ROOT` is accepted and reported and **nothing reads it**.
|
|
144
148
|
|
|
145
149
|
The full sequence, with the repositories it needs, is in
|
|
146
150
|
[`pgc_install`](https://github.com/protocol-governed-computing/pgc_install).
|
|
147
151
|
|
|
148
|
-
**Versioning.** Two
|
|
152
|
+
**Versioning.** Two numbers name two different things.
|
|
149
153
|
|
|
150
|
-
- **
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
- **Published** — this distribution's `version` in `pyproject.toml`. It moves on its own. A release
|
|
155
|
+
that changes this repository bumps it, and a release that leaves the repository unchanged keeps it.
|
|
156
|
+
- **Composition** — the public identity (`v6`, …) names one composition of the whole family.
|
|
157
|
+
`pgc_release` records which version of each distribution that composition holds, and `pgc_install`
|
|
158
|
+
pins exactly those versions.
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
Each repository's `VERSION` file holds the workspace's cycle ordinal, which names the `dev/<N>`
|
|
161
|
+
branch. It is development accounting, and it is never published. The standard the packages
|
|
162
|
+
implement is a separate artifact on its own track, and none of these numbers is its revision.
|
|
158
163
|
|
|
159
164
|
The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150616
|
|
@@ -5,7 +5,7 @@ backend-path = ["."]
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "pgc-assembler"
|
|
8
|
-
version = "
|
|
8
|
+
version = "6.0.0"
|
|
9
9
|
description = "PGC snapshot assembler — composes compiled projections into a manifest-pinned executable snapshot (import package: assembler)"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -30,6 +30,13 @@ classifiers = [
|
|
|
30
30
|
# install without this raises ModuleNotFoundError there instead of refusing.
|
|
31
31
|
dependencies = ["pyyaml"]
|
|
32
32
|
|
|
33
|
+
# Snapshot signing is a profile's selection, not a property of the core. A platform whose
|
|
34
|
+
# profile requires authenticated snapshots installs this; one that does not should not
|
|
35
|
+
# acquire a cryptography dependency because the capability exists elsewhere. Absent it,
|
|
36
|
+
# assembler.signing refuses rather than degrading to an unsigned success.
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
signing = ["cryptography>=42"]
|
|
39
|
+
|
|
33
40
|
[project.scripts]
|
|
34
41
|
snapshot_assembler = "assembler.cli:main"
|
|
35
42
|
|
pgc_assembler-4.0.0/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
16
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
16
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pyyaml
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|