crovia-tacet-operator 0.3.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 (26) hide show
  1. crovia_tacet_operator-0.3.0/PKG-INFO +81 -0
  2. crovia_tacet_operator-0.3.0/README.md +68 -0
  3. crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/PKG-INFO +81 -0
  4. crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/SOURCES.txt +24 -0
  5. crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/dependency_links.txt +1 -0
  6. crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/entry_points.txt +2 -0
  7. crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/requires.txt +5 -0
  8. crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/top_level.txt +1 -0
  9. crovia_tacet_operator-0.3.0/pyproject.toml +22 -0
  10. crovia_tacet_operator-0.3.0/setup.cfg +4 -0
  11. crovia_tacet_operator-0.3.0/tacet_operator/__init__.py +20 -0
  12. crovia_tacet_operator-0.3.0/tacet_operator/cli.py +134 -0
  13. crovia_tacet_operator-0.3.0/tacet_operator/config.py +113 -0
  14. crovia_tacet_operator-0.3.0/tacet_operator/drand.py +98 -0
  15. crovia_tacet_operator-0.3.0/tacet_operator/fetch.py +85 -0
  16. crovia_tacet_operator-0.3.0/tacet_operator/keys.py +32 -0
  17. crovia_tacet_operator-0.3.0/tacet_operator/ots.py +92 -0
  18. crovia_tacet_operator-0.3.0/tacet_operator/predicates/__init__.py +24 -0
  19. crovia_tacet_operator-0.3.0/tacet_operator/predicates/hf_card_training_data_v1.py +103 -0
  20. crovia_tacet_operator-0.3.0/tacet_operator/prove.py +131 -0
  21. crovia_tacet_operator-0.3.0/tacet_operator/publish.py +175 -0
  22. crovia_tacet_operator-0.3.0/tacet_operator/runner.py +260 -0
  23. crovia_tacet_operator-0.3.0/tacet_operator/state.py +127 -0
  24. crovia_tacet_operator-0.3.0/tacet_operator/targets.py +100 -0
  25. crovia_tacet_operator-0.3.0/tests/test_epoch_end_to_end.py +182 -0
  26. crovia_tacet_operator-0.3.0/tests/test_predicate.py +55 -0
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: crovia-tacet-operator
3
+ Version: 0.3.0
4
+ Summary: Production operator for TACET: hourly epochs, signed surface snapshots, OTS anchoring, silence proofs as crovia.seal.v1
5
+ Author-email: Crovia Trust <info@croviatrust.com>
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: crovia-tacet>=0.3.0
10
+ Requires-Dist: cryptography>=41.0.0
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest>=7; extra == "test"
13
+
14
+ # TACET operator
15
+
16
+ Runs the TACET log in production. The [reference](../reference/python) defines
17
+ the objects; this package fetches, signs, anchors and publishes them.
18
+
19
+ ```
20
+ every hour tacet-operator run-epoch drand round → observe surfaces → update map → sign sheet → OTS stamp
21
+ every 6 h tacet-operator refresh-anchors upgrade OTS proofs; close sheets confirmed in Bitcoin
22
+ daily tacet-operator publish --proofs rebuild featured silence proofs (crovia.seal.v1)
23
+ ```
24
+
25
+ ## What one epoch does
26
+
27
+ 1. Fetches the current **drand** round (chain `8990e7…`, pinned in `trust_root.json`).
28
+ Nothing in the epoch could have existed before that round: lower time bound.
29
+ 2. Fetches the surfaces of the featured targets and of a rotating slice of the
30
+ target list (default 120 per hour, 0.6 s apart, `crovia-tacet-observer` user
31
+ agent). Surface = the raw model card `…/raw/main/README.md`; for gated
32
+ repositories (401/403) the rendered model page.
33
+ 3. Runs the public predicate `crovia.pred.hf-card-training-data 1.0.0`
34
+ ([source](tacet_operator/predicates/hf_card_training_data_v1.py), hashed
35
+ byte-for-byte into every snapshot) on the bytes and signs a **snapshot**
36
+ with the observer key. Non-200 responses produce no snapshot: a fetch failure
37
+ is indeterminate, never absence.
38
+ 4. Applies forward-only slot transitions: a positive snapshot for an empty slot
39
+ inserts a `disclosure` value; two consecutive negatives for a disclosed slot
40
+ insert a `retraction`. Every value is asserted by a `crovia.seal.v1` signed
41
+ with the Seal-issuer key (`values/<key>.json`).
42
+ 5. Signs the **epoch sheet** (root, size, `snapshots_root`, `opened` beacon,
43
+ `prev_sheet_hash`) with the operator key and stamps `sheet_hash` with
44
+ OpenTimestamps. When the calendar reaches Bitcoin, `refresh-anchors` fills
45
+ `closed` (block height, block time, proof URL): upper time bound.
46
+
47
+ The operator is the only writer; all changes of an epoch are applied in one
48
+ batch, so the sheet's root is the root at `epoch_end`. Missed hours are
49
+ back-filled with empty sheets (historical drand round): the chain stays
50
+ contiguous and those hours contribute no silence.
51
+
52
+ ## Silence proofs
53
+
54
+ ```
55
+ tacet-operator prove mistralai/Mistral-7B-v0.1 --strength 2 -o proof.seal.json
56
+ tacet-operator verify proof.seal.json --operator-pubkey <key_hex from trust_root.json>
57
+ ```
58
+
59
+ A proof carries every sheet in the range, the delta-encoded non-inclusion
60
+ path, the negative snapshots with their Merkle inclusion in each epoch's
61
+ `snapshots_root`, and the `silence` block recomputed by the verifier:
62
+ `silence_seconds` = sum of the durations of anchored epochs that hold a negative
63
+ snapshot for the target. Unobserved, unanchored or unreachable hours count
64
+ zero. The bundle is wrapped in an unmodified `crovia.seal.v1`.
65
+
66
+ ## Keys
67
+
68
+ Three Ed25519 seeds in `state/keys/` (mode 0600), created on first run:
69
+ `operator` (signs sheets), `observer` (signs snapshots), `issuer`
70
+ (`urn:crovia:seal-issuer:tacet`, signs Seals: value assertions and proofs).
71
+ Public keys are published in `trust_root.json`.
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ pip install -e ../reference/python -e <crovia-seal>/reference/python -e .
77
+ TACET_STATE=/opt/crovia/tacet/state TACET_PUBLIC=/var/www/registry/data/tacet tacet-operator keys
78
+ ```
79
+
80
+ Tests: `pytest -q tests` (predicate vectors are real model cards; the epoch
81
+ test uses a fake network, fake drand and fake OTS).
@@ -0,0 +1,68 @@
1
+ # TACET operator
2
+
3
+ Runs the TACET log in production. The [reference](../reference/python) defines
4
+ the objects; this package fetches, signs, anchors and publishes them.
5
+
6
+ ```
7
+ every hour tacet-operator run-epoch drand round → observe surfaces → update map → sign sheet → OTS stamp
8
+ every 6 h tacet-operator refresh-anchors upgrade OTS proofs; close sheets confirmed in Bitcoin
9
+ daily tacet-operator publish --proofs rebuild featured silence proofs (crovia.seal.v1)
10
+ ```
11
+
12
+ ## What one epoch does
13
+
14
+ 1. Fetches the current **drand** round (chain `8990e7…`, pinned in `trust_root.json`).
15
+ Nothing in the epoch could have existed before that round: lower time bound.
16
+ 2. Fetches the surfaces of the featured targets and of a rotating slice of the
17
+ target list (default 120 per hour, 0.6 s apart, `crovia-tacet-observer` user
18
+ agent). Surface = the raw model card `…/raw/main/README.md`; for gated
19
+ repositories (401/403) the rendered model page.
20
+ 3. Runs the public predicate `crovia.pred.hf-card-training-data 1.0.0`
21
+ ([source](tacet_operator/predicates/hf_card_training_data_v1.py), hashed
22
+ byte-for-byte into every snapshot) on the bytes and signs a **snapshot**
23
+ with the observer key. Non-200 responses produce no snapshot: a fetch failure
24
+ is indeterminate, never absence.
25
+ 4. Applies forward-only slot transitions: a positive snapshot for an empty slot
26
+ inserts a `disclosure` value; two consecutive negatives for a disclosed slot
27
+ insert a `retraction`. Every value is asserted by a `crovia.seal.v1` signed
28
+ with the Seal-issuer key (`values/<key>.json`).
29
+ 5. Signs the **epoch sheet** (root, size, `snapshots_root`, `opened` beacon,
30
+ `prev_sheet_hash`) with the operator key and stamps `sheet_hash` with
31
+ OpenTimestamps. When the calendar reaches Bitcoin, `refresh-anchors` fills
32
+ `closed` (block height, block time, proof URL): upper time bound.
33
+
34
+ The operator is the only writer; all changes of an epoch are applied in one
35
+ batch, so the sheet's root is the root at `epoch_end`. Missed hours are
36
+ back-filled with empty sheets (historical drand round): the chain stays
37
+ contiguous and those hours contribute no silence.
38
+
39
+ ## Silence proofs
40
+
41
+ ```
42
+ tacet-operator prove mistralai/Mistral-7B-v0.1 --strength 2 -o proof.seal.json
43
+ tacet-operator verify proof.seal.json --operator-pubkey <key_hex from trust_root.json>
44
+ ```
45
+
46
+ A proof carries every sheet in the range, the delta-encoded non-inclusion
47
+ path, the negative snapshots with their Merkle inclusion in each epoch's
48
+ `snapshots_root`, and the `silence` block recomputed by the verifier:
49
+ `silence_seconds` = sum of the durations of anchored epochs that hold a negative
50
+ snapshot for the target. Unobserved, unanchored or unreachable hours count
51
+ zero. The bundle is wrapped in an unmodified `crovia.seal.v1`.
52
+
53
+ ## Keys
54
+
55
+ Three Ed25519 seeds in `state/keys/` (mode 0600), created on first run:
56
+ `operator` (signs sheets), `observer` (signs snapshots), `issuer`
57
+ (`urn:crovia:seal-issuer:tacet`, signs Seals: value assertions and proofs).
58
+ Public keys are published in `trust_root.json`.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ pip install -e ../reference/python -e <crovia-seal>/reference/python -e .
64
+ TACET_STATE=/opt/crovia/tacet/state TACET_PUBLIC=/var/www/registry/data/tacet tacet-operator keys
65
+ ```
66
+
67
+ Tests: `pytest -q tests` (predicate vectors are real model cards; the epoch
68
+ test uses a fake network, fake drand and fake OTS).
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: crovia-tacet-operator
3
+ Version: 0.3.0
4
+ Summary: Production operator for TACET: hourly epochs, signed surface snapshots, OTS anchoring, silence proofs as crovia.seal.v1
5
+ Author-email: Crovia Trust <info@croviatrust.com>
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: crovia-tacet>=0.3.0
10
+ Requires-Dist: cryptography>=41.0.0
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest>=7; extra == "test"
13
+
14
+ # TACET operator
15
+
16
+ Runs the TACET log in production. The [reference](../reference/python) defines
17
+ the objects; this package fetches, signs, anchors and publishes them.
18
+
19
+ ```
20
+ every hour tacet-operator run-epoch drand round → observe surfaces → update map → sign sheet → OTS stamp
21
+ every 6 h tacet-operator refresh-anchors upgrade OTS proofs; close sheets confirmed in Bitcoin
22
+ daily tacet-operator publish --proofs rebuild featured silence proofs (crovia.seal.v1)
23
+ ```
24
+
25
+ ## What one epoch does
26
+
27
+ 1. Fetches the current **drand** round (chain `8990e7…`, pinned in `trust_root.json`).
28
+ Nothing in the epoch could have existed before that round: lower time bound.
29
+ 2. Fetches the surfaces of the featured targets and of a rotating slice of the
30
+ target list (default 120 per hour, 0.6 s apart, `crovia-tacet-observer` user
31
+ agent). Surface = the raw model card `…/raw/main/README.md`; for gated
32
+ repositories (401/403) the rendered model page.
33
+ 3. Runs the public predicate `crovia.pred.hf-card-training-data 1.0.0`
34
+ ([source](tacet_operator/predicates/hf_card_training_data_v1.py), hashed
35
+ byte-for-byte into every snapshot) on the bytes and signs a **snapshot**
36
+ with the observer key. Non-200 responses produce no snapshot: a fetch failure
37
+ is indeterminate, never absence.
38
+ 4. Applies forward-only slot transitions: a positive snapshot for an empty slot
39
+ inserts a `disclosure` value; two consecutive negatives for a disclosed slot
40
+ insert a `retraction`. Every value is asserted by a `crovia.seal.v1` signed
41
+ with the Seal-issuer key (`values/<key>.json`).
42
+ 5. Signs the **epoch sheet** (root, size, `snapshots_root`, `opened` beacon,
43
+ `prev_sheet_hash`) with the operator key and stamps `sheet_hash` with
44
+ OpenTimestamps. When the calendar reaches Bitcoin, `refresh-anchors` fills
45
+ `closed` (block height, block time, proof URL): upper time bound.
46
+
47
+ The operator is the only writer; all changes of an epoch are applied in one
48
+ batch, so the sheet's root is the root at `epoch_end`. Missed hours are
49
+ back-filled with empty sheets (historical drand round): the chain stays
50
+ contiguous and those hours contribute no silence.
51
+
52
+ ## Silence proofs
53
+
54
+ ```
55
+ tacet-operator prove mistralai/Mistral-7B-v0.1 --strength 2 -o proof.seal.json
56
+ tacet-operator verify proof.seal.json --operator-pubkey <key_hex from trust_root.json>
57
+ ```
58
+
59
+ A proof carries every sheet in the range, the delta-encoded non-inclusion
60
+ path, the negative snapshots with their Merkle inclusion in each epoch's
61
+ `snapshots_root`, and the `silence` block recomputed by the verifier:
62
+ `silence_seconds` = sum of the durations of anchored epochs that hold a negative
63
+ snapshot for the target. Unobserved, unanchored or unreachable hours count
64
+ zero. The bundle is wrapped in an unmodified `crovia.seal.v1`.
65
+
66
+ ## Keys
67
+
68
+ Three Ed25519 seeds in `state/keys/` (mode 0600), created on first run:
69
+ `operator` (signs sheets), `observer` (signs snapshots), `issuer`
70
+ (`urn:crovia:seal-issuer:tacet`, signs Seals: value assertions and proofs).
71
+ Public keys are published in `trust_root.json`.
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ pip install -e ../reference/python -e <crovia-seal>/reference/python -e .
77
+ TACET_STATE=/opt/crovia/tacet/state TACET_PUBLIC=/var/www/registry/data/tacet tacet-operator keys
78
+ ```
79
+
80
+ Tests: `pytest -q tests` (predicate vectors are real model cards; the epoch
81
+ test uses a fake network, fake drand and fake OTS).
@@ -0,0 +1,24 @@
1
+ README.md
2
+ pyproject.toml
3
+ crovia_tacet_operator.egg-info/PKG-INFO
4
+ crovia_tacet_operator.egg-info/SOURCES.txt
5
+ crovia_tacet_operator.egg-info/dependency_links.txt
6
+ crovia_tacet_operator.egg-info/entry_points.txt
7
+ crovia_tacet_operator.egg-info/requires.txt
8
+ crovia_tacet_operator.egg-info/top_level.txt
9
+ tacet_operator/__init__.py
10
+ tacet_operator/cli.py
11
+ tacet_operator/config.py
12
+ tacet_operator/drand.py
13
+ tacet_operator/fetch.py
14
+ tacet_operator/keys.py
15
+ tacet_operator/ots.py
16
+ tacet_operator/prove.py
17
+ tacet_operator/publish.py
18
+ tacet_operator/runner.py
19
+ tacet_operator/state.py
20
+ tacet_operator/targets.py
21
+ tacet_operator/predicates/__init__.py
22
+ tacet_operator/predicates/hf_card_training_data_v1.py
23
+ tests/test_epoch_end_to_end.py
24
+ tests/test_predicate.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tacet-operator = tacet_operator.cli:main
@@ -0,0 +1,5 @@
1
+ crovia-tacet>=0.3.0
2
+ cryptography>=41.0.0
3
+
4
+ [test]
5
+ pytest>=7
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "crovia-tacet-operator"
7
+ version = "0.3.0"
8
+ description = "Production operator for TACET: hourly epochs, signed surface snapshots, OTS anchoring, silence proofs as crovia.seal.v1"
9
+ readme = "README.md"
10
+ license = { text = "Apache-2.0" }
11
+ authors = [{ name = "Crovia Trust", email = "info@croviatrust.com" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = ["crovia-tacet>=0.3.0", "cryptography>=41.0.0"]
14
+
15
+ [project.optional-dependencies]
16
+ test = ["pytest>=7"]
17
+
18
+ [project.scripts]
19
+ tacet-operator = "tacet_operator.cli:main"
20
+
21
+ [tool.setuptools]
22
+ packages = ["tacet_operator", "tacet_operator.predicates"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ """TACET operator: turns the reference primitives into a running log.
2
+
3
+ Layout (SPEC §12):
4
+
5
+ state/ private, on the operator host
6
+ keys/ 32-byte Ed25519 seeds (operator, observer, issuer)
7
+ map.json {key_hex: value_hash_hex} current map
8
+ values/<key_hex>.json slot value objects (kind disclosure/retraction)
9
+ cursor.json rotation cursor over the target list
10
+ public/ served at /registry/data/tacet/
11
+ trust_root.json map_id, genesis, keys, drand chain, predicates
12
+ sheets/<epoch>.json signed epoch sheets (closed once OTS confirms)
13
+ snapshots/<epoch>.jsonl signed surface snapshots taken in the epoch
14
+ changes/<epoch>.json [[key_hex, value_hash_hex], ...] applied in the epoch
15
+ ots/<epoch>.ots OpenTimestamps proof of the sheet hash
16
+ proofs/<slug>.seal.json featured silence proofs wrapped in crovia.seal.v1
17
+ latest.json, index.json discovery
18
+ """
19
+
20
+ __version__ = "0.1.0"
@@ -0,0 +1,134 @@
1
+ """tacet-operator: run-epoch | refresh-anchors | publish | prove | verify | build-targets | keys"""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import logging
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .config import Settings
11
+ from .keys import load_all
12
+
13
+
14
+ def _settings(args) -> Settings:
15
+ s = Settings.from_env()
16
+ if args.state:
17
+ s.paths.state = Path(args.state)
18
+ if args.public:
19
+ s.paths.public = Path(args.public)
20
+ if getattr(args, "targets", None):
21
+ s.targets_file = Path(args.targets)
22
+ if getattr(args, "budget", None):
23
+ s.per_epoch_budget = args.budget
24
+ s.paths.ensure()
25
+ return s
26
+
27
+
28
+ def main(argv=None) -> int:
29
+ p = argparse.ArgumentParser(prog="tacet-operator")
30
+ p.add_argument("--state", help="state directory (default $TACET_STATE or /opt/crovia/tacet/state)")
31
+ p.add_argument("--public", help="public directory (default $TACET_PUBLIC or /var/www/registry/data/tacet)")
32
+ p.add_argument("-v", "--verbose", action="store_true")
33
+ sub = p.add_subparsers(dest="cmd", required=True)
34
+
35
+ r = sub.add_parser("run-epoch", help="emit the current epoch (and back-fill missed ones)")
36
+ r.add_argument("--targets", help="newline-separated target ids")
37
+ r.add_argument("--budget", type=int)
38
+ r.add_argument("--no-ots", action="store_true", help="do not stamp (tests)")
39
+
40
+ sub.add_parser("refresh-anchors", help="stamp missing proofs; close sheets confirmed in Bitcoin")
41
+
42
+ pb = sub.add_parser("publish", help="rewrite trust_root/index/latest/targets (+ featured proofs)")
43
+ pb.add_argument("--proofs", action="store_true")
44
+
45
+ pr = sub.add_parser("prove", help="build a silence proof for one target, wrapped in crovia.seal.v1")
46
+ pr.add_argument("target")
47
+ pr.add_argument("--from-epoch", type=int, default=0)
48
+ pr.add_argument("--to-epoch", type=int)
49
+ pr.add_argument("--strength", type=int, default=2, choices=(1, 2))
50
+ pr.add_argument("-o", "--out")
51
+
52
+ vf = sub.add_parser("verify", help="verify a wrapped silence proof offline")
53
+ vf.add_argument("file")
54
+ vf.add_argument("--operator-pubkey", help="expected operator key_hex (from trust_root.json)")
55
+ vf.add_argument("--no-beacon", action="store_true")
56
+ vf.add_argument("--no-ots", action="store_true")
57
+
58
+ bt = sub.add_parser("build-targets", help="derive the target list from public registry files")
59
+ bt.add_argument("--candidates", required=True)
60
+ bt.add_argument("--silence")
61
+ bt.add_argument("-o", "--out", required=True)
62
+
63
+ sub.add_parser("keys", help="print public keys (creating seeds if absent)")
64
+
65
+ args = p.parse_args(argv)
66
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
67
+ format="%(asctime)s %(levelname)s %(message)s", stream=sys.stderr)
68
+
69
+ if args.cmd == "verify":
70
+ # Third parties run this on any machine: it must not touch operator state or keys.
71
+ from .prove import verify_file
72
+ res = verify_file(Path(args.file), expected_operator_pubkey_hex=args.operator_pubkey,
73
+ check_beacon=not args.no_beacon, check_ots=not args.no_ots)
74
+ print(json.dumps(res, indent=1))
75
+ return 0 if res["ok"] else 1
76
+
77
+ s = _settings(args)
78
+
79
+ if args.cmd == "keys":
80
+ keys = load_all(s.paths.keys)
81
+ print(json.dumps({r: {"id": k.id, "pubkey": k.pubkey_json()} for r, k in keys.items()}, indent=1))
82
+ return 0
83
+
84
+ if args.cmd == "run-epoch":
85
+ from .publish import publish_all
86
+ from .runner import EpochRunner
87
+ keys = load_all(s.paths.keys)
88
+ runner = EpochRunner(s, keys)
89
+ if args.no_ots:
90
+ runner.ots_stamp = None
91
+ sheet = runner.run()
92
+ publish_all(s, keys)
93
+ print(json.dumps({"emitted": sheet["epoch"] if sheet else None}))
94
+ return 0
95
+
96
+ if args.cmd == "refresh-anchors":
97
+ from .publish import publish_all
98
+ from .runner import refresh_anchors
99
+ counts = refresh_anchors(s)
100
+ # A newly closed sheet changes every silence figure; re-issue the featured proofs at once.
101
+ publish_all(s, load_all(s.paths.keys), with_proofs=counts.get("closed", 0) > 0)
102
+ print(json.dumps(counts))
103
+ return 0
104
+
105
+ if args.cmd == "publish":
106
+ from .publish import publish_all
107
+ latest = publish_all(s, load_all(s.paths.keys), with_proofs=args.proofs)
108
+ print(json.dumps(latest, indent=1))
109
+ return 0
110
+
111
+ if args.cmd == "prove":
112
+ from .prove import build
113
+ keys = load_all(s.paths.keys)
114
+ bundle = build(s, keys["issuer"], args.target, from_epoch=args.from_epoch, to_epoch=args.to_epoch, strength=args.strength)
115
+ text = json.dumps(bundle, indent=1, sort_keys=True)
116
+ if args.out:
117
+ Path(args.out).write_text(text + "\n")
118
+ print(json.dumps({"out": args.out, "silence": bundle["proof"]["silence"], "seal_id": bundle["seal"]["seal_id"]}))
119
+ else:
120
+ print(text)
121
+ return 0
122
+
123
+ if args.cmd == "build-targets":
124
+ from .targets import build_from_public
125
+ ids = build_from_public(Path(args.candidates), Path(args.silence) if args.silence else None)
126
+ Path(args.out).write_text("\n".join(ids) + "\n")
127
+ print(json.dumps({"targets": len(ids), "out": args.out}))
128
+ return 0
129
+
130
+ return 2
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ MAP_ID = "urn:crovia:tacet:map:disclosure"
9
+ OPERATOR_ID = "urn:crovia:tacet:operator:crovia-trust"
10
+ OBSERVER_ID = "urn:crovia:observer:hetzner-1"
11
+ ISSUER_ID = "urn:crovia:seal-issuer:tacet" # Seal issuer ids are constrained by the Seal spec
12
+
13
+ # Epoch 0 starts here; epoch e covers [GENESIS + e*EPOCH_SECONDS, GENESIS + (e+1)*EPOCH_SECONDS).
14
+ GENESIS = datetime(2026, 9, 19, 18, 0, 0, tzinfo=timezone.utc)
15
+ EPOCH_SECONDS = 3600
16
+
17
+ DRAND_CHAIN_HASH = "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce"
18
+ DRAND_URLS = (
19
+ "https://api.drand.sh",
20
+ "https://drand.cloudflare.com",
21
+ "https://api2.drand.sh",
22
+ )
23
+
24
+ PUBLIC_BASE_URL = "https://croviatrust.com/registry/data/tacet"
25
+ USER_AGENT = "crovia-tacet-observer/0.1 (+https://croviatrust.com/registry/tacet/)"
26
+
27
+
28
+ @dataclass
29
+ class Paths:
30
+ state: Path
31
+ public: Path
32
+
33
+ @property
34
+ def keys(self) -> Path:
35
+ return self.state / "keys"
36
+
37
+ @property
38
+ def map_file(self) -> Path:
39
+ return self.state / "map.json"
40
+
41
+ @property
42
+ def values(self) -> Path:
43
+ return self.state / "values"
44
+
45
+ @property
46
+ def cursor(self) -> Path:
47
+ return self.state / "cursor.json"
48
+
49
+ @property
50
+ def fetch_log(self) -> Path:
51
+ return self.state / "fetch_log"
52
+
53
+ @property
54
+ def sheets(self) -> Path:
55
+ return self.public / "sheets"
56
+
57
+ @property
58
+ def snapshots(self) -> Path:
59
+ return self.public / "snapshots"
60
+
61
+ @property
62
+ def changes(self) -> Path:
63
+ return self.public / "changes"
64
+
65
+ @property
66
+ def ots(self) -> Path:
67
+ return self.public / "ots"
68
+
69
+ @property
70
+ def proofs(self) -> Path:
71
+ return self.public / "proofs"
72
+
73
+ def ensure(self) -> None:
74
+ for d in (self.state, self.keys, self.values, self.fetch_log,
75
+ self.public, self.sheets, self.snapshots, self.changes, self.ots, self.proofs):
76
+ d.mkdir(parents=True, exist_ok=True)
77
+
78
+
79
+ @dataclass
80
+ class Settings:
81
+ paths: Paths
82
+ per_epoch_budget: int = 120 # surfaces fetched per epoch (HF-friendly: 2/min)
83
+ featured_every_epoch: int = 40 # featured targets observed in every epoch
84
+ request_delay_s: float = 0.6
85
+ request_timeout_s: float = 25.0
86
+ featured: list = field(default_factory=list)
87
+ targets_file: Path | None = None # newline-separated org/model ids
88
+
89
+ @classmethod
90
+ def from_env(cls) -> "Settings":
91
+ state = Path(os.environ.get("TACET_STATE", "/opt/crovia/tacet/state"))
92
+ public = Path(os.environ.get("TACET_PUBLIC", "/var/www/registry/data/tacet"))
93
+ s = cls(paths=Paths(state, public))
94
+ if os.environ.get("TACET_BUDGET"):
95
+ s.per_epoch_budget = int(os.environ["TACET_BUDGET"])
96
+ if os.environ.get("TACET_TARGETS"):
97
+ s.targets_file = Path(os.environ["TACET_TARGETS"])
98
+ return s
99
+
100
+
101
+ def epoch_of(ts: datetime) -> int:
102
+ return int((ts - GENESIS).total_seconds() // EPOCH_SECONDS)
103
+
104
+
105
+ def epoch_bounds(epoch: int) -> tuple[str, str]:
106
+ start = GENESIS.timestamp() + epoch * EPOCH_SECONDS
107
+ end = start + EPOCH_SECONDS
108
+ f = lambda t: datetime.fromtimestamp(t, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # noqa: E731
109
+ return f(start), f(end)
110
+
111
+
112
+ def now_iso() -> str:
113
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -0,0 +1,98 @@
1
+ """drand League of Entropy beacon: the epoch's lower time bound (SPEC §6).
2
+
3
+ The default chain (pedersen-bls-chained, 30 s period, genesis 1595431050) is
4
+ pinned by hash. `fetch_latest` tries the public relays in order and returns the
5
+ `opened` object for an epoch sheet. `round_time` lets verifiers check that the
6
+ round could not have existed before `epoch_start`.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import urllib.request
12
+ from typing import Any, Dict
13
+
14
+ from .config import DRAND_CHAIN_HASH, DRAND_URLS, USER_AGENT
15
+
16
+ PERIOD = 30
17
+ GENESIS_TIME = 1595431050
18
+
19
+
20
+ class DrandError(RuntimeError):
21
+ pass
22
+
23
+
24
+ def _get(url: str, timeout: float) -> Dict[str, Any]:
25
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "application/json"})
26
+ with urllib.request.urlopen(req, timeout=timeout) as r:
27
+ return json.loads(r.read().decode("utf-8"))
28
+
29
+
30
+ def fetch_latest(timeout: float = 10.0) -> Dict[str, Any]:
31
+ last = None
32
+ for base in DRAND_URLS:
33
+ try:
34
+ d = _get(f"{base}/{DRAND_CHAIN_HASH}/public/latest", timeout)
35
+ for k in ("round", "randomness", "signature"):
36
+ if k not in d:
37
+ raise DrandError(f"{base}: missing {k}")
38
+ return {
39
+ "kind": "drand",
40
+ "chain_hash": DRAND_CHAIN_HASH,
41
+ "round": int(d["round"]),
42
+ "randomness": d["randomness"],
43
+ "signature": d["signature"],
44
+ }
45
+ except Exception as e: # noqa: BLE001 - try next relay
46
+ last = e
47
+ raise DrandError(f"all drand relays failed: {last}")
48
+
49
+
50
+ def fetch_info(timeout: float = 10.0) -> Dict[str, Any]:
51
+ last = None
52
+ for base in DRAND_URLS:
53
+ try:
54
+ d = _get(f"{base}/{DRAND_CHAIN_HASH}/info", timeout)
55
+ if d.get("hash") != DRAND_CHAIN_HASH:
56
+ raise DrandError("chain hash mismatch")
57
+ return {"chain_hash": d["hash"], "public_key": d["public_key"], "period": d["period"],
58
+ "genesis_time": d["genesis_time"], "scheme": d.get("schemeID")}
59
+ except Exception as e: # noqa: BLE001
60
+ last = e
61
+ raise DrandError(f"all drand relays failed: {last}")
62
+
63
+
64
+ def fetch_round(round_no: int, timeout: float = 10.0) -> Dict[str, Any]:
65
+ """Historical round (used only to back-fill empty epochs after downtime)."""
66
+ last = None
67
+ for base in DRAND_URLS:
68
+ try:
69
+ d = _get(f"{base}/{DRAND_CHAIN_HASH}/public/{round_no}", timeout)
70
+ if int(d["round"]) != round_no:
71
+ raise DrandError("round mismatch")
72
+ return {"kind": "drand", "chain_hash": DRAND_CHAIN_HASH, "round": round_no,
73
+ "randomness": d["randomness"], "signature": d["signature"]}
74
+ except Exception as e: # noqa: BLE001
75
+ last = e
76
+ raise DrandError(f"all drand relays failed for round {round_no}: {last}")
77
+
78
+
79
+ def round_time(round_no: int) -> int:
80
+ """Unix time at which `round_no` became available."""
81
+ return GENESIS_TIME + (round_no - 1) * PERIOD
82
+
83
+
84
+ def round_at(unix_time: int) -> int:
85
+ """First round available at or after `unix_time`."""
86
+ if unix_time <= GENESIS_TIME:
87
+ return 1
88
+ return (unix_time - GENESIS_TIME + PERIOD - 1) // PERIOD + 1
89
+
90
+
91
+ def beacon_check(opened: Dict[str, Any], epoch_start: str, tolerance_s: int = 1800) -> bool:
92
+ """SPEC §8.5 step 2: the round must belong to the pinned chain and sit at the epoch start."""
93
+ from datetime import datetime, timezone
94
+ if opened.get("chain_hash") != DRAND_CHAIN_HASH:
95
+ return False
96
+ start = int(datetime.strptime(epoch_start, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).timestamp())
97
+ t = round_time(int(opened["round"]))
98
+ return start - PERIOD <= t <= start + tolerance_s