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.
Files changed (25) hide show
  1. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/NOTICE +1 -1
  2. {pgc_assembler-4.0.0/pgc_assembler.egg-info → pgc_assembler-6.0.0}/PKG-INFO +23 -18
  3. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/README.md +20 -17
  4. pgc_assembler-6.0.0/VERSION +1 -0
  5. pgc_assembler-6.0.0/assembler/VERSION +1 -0
  6. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/__init__.py +4 -5
  7. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/cli.py +67 -1
  8. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/core.py +80 -13
  9. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/indexes.py +6 -9
  10. pgc_assembler-6.0.0/assembler/signing.py +229 -0
  11. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0/pgc_assembler.egg-info}/PKG-INFO +23 -18
  12. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/SOURCES.txt +1 -0
  13. pgc_assembler-6.0.0/pgc_assembler.egg-info/requires.txt +4 -0
  14. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pyproject.toml +8 -1
  15. pgc_assembler-4.0.0/VERSION +0 -1
  16. pgc_assembler-4.0.0/assembler/VERSION +0 -1
  17. pgc_assembler-4.0.0/pgc_assembler.egg-info/requires.txt +0 -1
  18. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/LICENSE +0 -0
  19. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/MANIFEST.in +0 -0
  20. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/_build_hook.py +0 -0
  21. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/assembler/conformance.py +0 -0
  22. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/dependency_links.txt +0 -0
  23. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/entry_points.txt +0 -0
  24. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/pgc_assembler.egg-info/top_level.txt +0 -0
  25. {pgc_assembler-4.0.0 → pgc_assembler-6.0.0}/setup.cfg +0 -0
@@ -1,4 +1,4 @@
1
- PGS — Protocol-Governed Systems
1
+ PGC — Protocol-Governed Computing
2
2
  Copyright 2026 Bhash Ganti aka Bachi
3
3
 
4
4
  This project introduces a protocol-first execution model in which:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgc-assembler
3
- Version: 4.0.0
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 **seven domains**: `platform`, `workload`, `inspection`, `transformation`,
44
- and the business domains `ai_governance`, `blockchain` and `book_library_mgmt`.
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` | `transformation` | change request → protocol artifacts |
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. `PGC_SNAPSHOT_ROOT` is where compiled output is written, and each domain build needs
138
- its own: every layer's output consolidates into one root, and verification rejects any file in that
139
- root the current build did not declare. `PGC_SNAPSHOT_PROFILES` is the directory holding snapshot
140
- profiles, required by the assembler and the runtime alike.
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** — `PGC_SNAPSHOT_ROOT` is the
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 schemes, and the published version follows the second.
152
+ **Versioning.** Two numbers name two different things.
149
153
 
150
- - **Internal** — each repository's `VERSION` file, a monotonic composition ordinal. PGC versions the
151
- composition rather than each repo: they release together and the governance closure forces lockstep,
152
- so the ordinal names which composition a repo belongs to. Development happens on `dev/<N>` and each
153
- cycle is tagged `release-<N>`. This is not published.
154
- - **Public** — `PUBLIC_VERSION`, tagged on every component repository. The platform is at **`v3`**.
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
- **The published version is the public one: `v4` is `4.0.0`.** The standard the packages implement is a
157
- separate artifact on its own track and is not this number.
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 **seven domains**: `platform`, `workload`, `inspection`, `transformation`,
19
- and the business domains `ai_governance`, `blockchain` and `book_library_mgmt`.
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` | `transformation` | change request → protocol artifacts |
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. `PGC_SNAPSHOT_ROOT` is where compiled output is written, and each domain build needs
113
- its own: every layer's output consolidates into one root, and verification rejects any file in that
114
- root the current build did not declare. `PGC_SNAPSHOT_PROFILES` is the directory holding snapshot
115
- profiles, required by the assembler and the runtime alike.
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** — `PGC_SNAPSHOT_ROOT` is the
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 schemes, and the published version follows the second.
125
+ **Versioning.** Two numbers name two different things.
124
126
 
125
- - **Internal** — each repository's `VERSION` file, a monotonic composition ordinal. PGC versions the
126
- composition rather than each repo: they release together and the governance closure forces lockstep,
127
- so the ordinal names which composition a repo belongs to. Development happens on `dev/<N>` and each
128
- cycle is tagged `release-<N>`. This is not published.
129
- - **Public** — `PUBLIC_VERSION`, tagged on every component repository. The platform is at **`v3`**.
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
- **The published version is the public one: `v4` is `4.0.0`.** The standard the packages implement is a
132
- separate artifact on its own track and is not this number.
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 composition's release ordinal, read from the repo's single version declaration.
10
+ """The workspace's cycle ordinal, read from the repo's `VERSION` file.
11
11
 
12
- PGC versions the *composition*, not each repo independently: all repos are released together
13
- and the governance closure forces lockstep, so a monotonic integer names which composition a
14
- repo belongs to. `VERSION` is the sole declaration — pyproject derives it, and so does this.
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
- POST_SEAL = ("conformance/",)
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) -> 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
- Three inputs, and each is there for a stated reason:
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
- snapshot_id = compute_snapshot_identity(domains, constituents, profile)
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
- _DOMAIN_INSTANTIATED = frozenset({"WF", "CC", "CS", "CT", "RB", "AC", "IN", "EV", "TI", "TE"})
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
- This previously compared recorded values to recorded values — manifest against metadata.json
681
- against attestation — which is transitive from construction and establishes nothing to a party
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
- derived = compute_snapshot_identity(domains, recomputed, profile)
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 — the PGC successor to RI-0's cross-structure `build` aggregation):
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 is the authoring copy with the authored parts removed: `content_hash`,
47
- `frontmatter` and the IR are identical, while `content` and `references` are empty and
48
- `layer_code` names the consuming layer. So the copy that still carries authored content is the
49
- authoring copy, and it is the one an identity must resolve to — `si.artifact.show` promises "the
50
- published canonical artifact for an FQDN, as authored".
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: 4.0.0
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 **seven domains**: `platform`, `workload`, `inspection`, `transformation`,
44
- and the business domains `ai_governance`, `blockchain` and `book_library_mgmt`.
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` | `transformation` | change request → protocol artifacts |
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. `PGC_SNAPSHOT_ROOT` is where compiled output is written, and each domain build needs
138
- its own: every layer's output consolidates into one root, and verification rejects any file in that
139
- root the current build did not declare. `PGC_SNAPSHOT_PROFILES` is the directory holding snapshot
140
- profiles, required by the assembler and the runtime alike.
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** — `PGC_SNAPSHOT_ROOT` is the
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 schemes, and the published version follows the second.
152
+ **Versioning.** Two numbers name two different things.
149
153
 
150
- - **Internal** — each repository's `VERSION` file, a monotonic composition ordinal. PGC versions the
151
- composition rather than each repo: they release together and the governance closure forces lockstep,
152
- so the ordinal names which composition a repo belongs to. Development happens on `dev/<N>` and each
153
- cycle is tagged `release-<N>`. This is not published.
154
- - **Public** — `PUBLIC_VERSION`, tagged on every component repository. The platform is at **`v3`**.
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
- **The published version is the public one: `v4` is `4.0.0`.** The standard the packages implement is a
157
- separate artifact on its own track and is not this number.
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
@@ -11,6 +11,7 @@ assembler/cli.py
11
11
  assembler/conformance.py
12
12
  assembler/core.py
13
13
  assembler/indexes.py
14
+ assembler/signing.py
14
15
  pgc_assembler.egg-info/PKG-INFO
15
16
  pgc_assembler.egg-info/SOURCES.txt
16
17
  pgc_assembler.egg-info/dependency_links.txt
@@ -0,0 +1,4 @@
1
+ pyyaml
2
+
3
+ [signing]
4
+ cryptography>=42
@@ -5,7 +5,7 @@ backend-path = ["."]
5
5
 
6
6
  [project]
7
7
  name = "pgc-assembler"
8
- version = "4.0.0"
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
 
@@ -1 +0,0 @@
1
- 16
@@ -1 +0,0 @@
1
- 16
@@ -1 +0,0 @@
1
- pyyaml
File without changes
File without changes
File without changes