trustchain-verify 0.1.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.
- trustchain_verify-0.1.0/PKG-INFO +76 -0
- trustchain_verify-0.1.0/README.md +51 -0
- trustchain_verify-0.1.0/pyproject.toml +41 -0
- trustchain_verify-0.1.0/setup.cfg +4 -0
- trustchain_verify-0.1.0/src/trustchain_verify/__init__.py +0 -0
- trustchain_verify-0.1.0/src/trustchain_verify/cli.py +210 -0
- trustchain_verify-0.1.0/src/trustchain_verify.egg-info/PKG-INFO +76 -0
- trustchain_verify-0.1.0/src/trustchain_verify.egg-info/SOURCES.txt +11 -0
- trustchain_verify-0.1.0/src/trustchain_verify.egg-info/dependency_links.txt +1 -0
- trustchain_verify-0.1.0/src/trustchain_verify.egg-info/entry_points.txt +2 -0
- trustchain_verify-0.1.0/src/trustchain_verify.egg-info/requires.txt +3 -0
- trustchain_verify-0.1.0/src/trustchain_verify.egg-info/top_level.txt +1 -0
- trustchain_verify-0.1.0/tests/test_cli.py +377 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustchain-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-source CLI verifier for Arkava Trust Chain witness manifests
|
|
5
|
+
Author: Arkava Ltd
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Arkava-Ai/arkava-trustchain
|
|
8
|
+
Project-URL: Source, https://github.com/Arkava-Ai/arkava-trustchain
|
|
9
|
+
Project-URL: Documentation, https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md
|
|
10
|
+
Keywords: arkava,trust-chain,audit,sovereign-ai,verifier
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Classifier: Topic :: System :: Systems Administration
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: click>=8.1
|
|
23
|
+
Requires-Dist: trustchain-core
|
|
24
|
+
Requires-Dist: requests>=2.31
|
|
25
|
+
|
|
26
|
+
# trustchain-verify
|
|
27
|
+
|
|
28
|
+
Open-source CLI verifier for Arkava® Trust Chain witness manifests.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install trustchain-verify
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Python 3.11+.
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Verify a local manifest file
|
|
42
|
+
trustchain-verify verify-manifest manifest.json
|
|
43
|
+
|
|
44
|
+
# Verify a remote manifest
|
|
45
|
+
trustchain-verify verify-manifest https://raw.githubusercontent.com/Arkava-Ai/arkava-trustchain/main/witness-history/2026/06/26/manifest_2026-06-26.json
|
|
46
|
+
|
|
47
|
+
# Batch-verify multiple manifests
|
|
48
|
+
trustchain-verify verify-batch manifest_1.json manifest_2.json
|
|
49
|
+
|
|
50
|
+
# Validate schema only (no signature check)
|
|
51
|
+
trustchain-verify validate-schema manifest.json
|
|
52
|
+
|
|
53
|
+
# List published operator public keys
|
|
54
|
+
trustchain-verify list-keys
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Exit Codes
|
|
58
|
+
|
|
59
|
+
| Code | Meaning |
|
|
60
|
+
|---|---|
|
|
61
|
+
| 0 | Verification succeeded |
|
|
62
|
+
| 1 | Schema validation error |
|
|
63
|
+
| 2 | Signature verification error |
|
|
64
|
+
| 3 | Usage error |
|
|
65
|
+
|
|
66
|
+
## Documentation
|
|
67
|
+
|
|
68
|
+
- CLI reference: [`docs/TRUSTCHAIN_VERIFY_CLI.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md)
|
|
69
|
+
- Architecture: [`docs/Arkava_TrustChain_Technical_Architecture_v0.2.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/Arkava_TrustChain_Technical_Architecture_v0.2.md) §2.9
|
|
70
|
+
- ADR: [TC-ADR-0011](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/adr/TC-ADR-0011-api-and-verification.md)
|
|
71
|
+
|
|
72
|
+
## Licence
|
|
73
|
+
|
|
74
|
+
Apache-2.0. Arkava® Trust Chain is a registered trademark of Arkava Ltd.
|
|
75
|
+
|
|
76
|
+
© 2026 Arkava® / All rights reserved.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# trustchain-verify
|
|
2
|
+
|
|
3
|
+
Open-source CLI verifier for Arkava® Trust Chain witness manifests.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install trustchain-verify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.11+.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Verify a local manifest file
|
|
17
|
+
trustchain-verify verify-manifest manifest.json
|
|
18
|
+
|
|
19
|
+
# Verify a remote manifest
|
|
20
|
+
trustchain-verify verify-manifest https://raw.githubusercontent.com/Arkava-Ai/arkava-trustchain/main/witness-history/2026/06/26/manifest_2026-06-26.json
|
|
21
|
+
|
|
22
|
+
# Batch-verify multiple manifests
|
|
23
|
+
trustchain-verify verify-batch manifest_1.json manifest_2.json
|
|
24
|
+
|
|
25
|
+
# Validate schema only (no signature check)
|
|
26
|
+
trustchain-verify validate-schema manifest.json
|
|
27
|
+
|
|
28
|
+
# List published operator public keys
|
|
29
|
+
trustchain-verify list-keys
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Exit Codes
|
|
33
|
+
|
|
34
|
+
| Code | Meaning |
|
|
35
|
+
|---|---|
|
|
36
|
+
| 0 | Verification succeeded |
|
|
37
|
+
| 1 | Schema validation error |
|
|
38
|
+
| 2 | Signature verification error |
|
|
39
|
+
| 3 | Usage error |
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
- CLI reference: [`docs/TRUSTCHAIN_VERIFY_CLI.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md)
|
|
44
|
+
- Architecture: [`docs/Arkava_TrustChain_Technical_Architecture_v0.2.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/Arkava_TrustChain_Technical_Architecture_v0.2.md) §2.9
|
|
45
|
+
- ADR: [TC-ADR-0011](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/adr/TC-ADR-0011-api-and-verification.md)
|
|
46
|
+
|
|
47
|
+
## Licence
|
|
48
|
+
|
|
49
|
+
Apache-2.0. Arkava® Trust Chain is a registered trademark of Arkava Ltd.
|
|
50
|
+
|
|
51
|
+
© 2026 Arkava® / All rights reserved.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "trustchain-verify"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Open-source CLI verifier for Arkava Trust Chain witness manifests"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
authors = [{ name = "Arkava Ltd" }]
|
|
9
|
+
keywords = ["arkava", "trust-chain", "audit", "sovereign-ai", "verifier"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: Information Technology",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Security :: Cryptography",
|
|
19
|
+
"Topic :: System :: Systems Administration",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"click>=8.1",
|
|
23
|
+
"trustchain-core",
|
|
24
|
+
"requests>=2.31",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/Arkava-Ai/arkava-trustchain"
|
|
29
|
+
Source = "https://github.com/Arkava-Ai/arkava-trustchain"
|
|
30
|
+
Documentation = "https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
trustchain-verify = "trustchain_verify.cli:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["setuptools>=69", "wheel"]
|
|
37
|
+
build-backend = "setuptools.build_meta"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
include = ["trustchain_verify*"]
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""trustchain-verify CLI — open-source manifest verifier (ARK-3560, TC-ADR-0011).
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
verify-manifest <path-or-url> Verify a single manifest (schema + Ed25519 signature)
|
|
5
|
+
verify-batch <path1> <path2>… Batch-verify multiple manifests
|
|
6
|
+
validate-schema <path> Validate manifest against JSON Schema only
|
|
7
|
+
list-keys List published operator public keys
|
|
8
|
+
list-manifests <dir> List manifest files in a witness-history directory
|
|
9
|
+
|
|
10
|
+
Exit codes:
|
|
11
|
+
0 — success (all verifications passed)
|
|
12
|
+
1 — schema validation error
|
|
13
|
+
2 — signature verification error
|
|
14
|
+
3 — usage error (bad arguments, file not found, network error)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
from trustchain_core.witness import (
|
|
28
|
+
PublicKeyStore,
|
|
29
|
+
VerificationResult,
|
|
30
|
+
verify_manifest_file,
|
|
31
|
+
verify_manifest_schema,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fetch_remote(url: str) -> str:
|
|
36
|
+
"""Download a manifest from a URL to a temp file and return its path."""
|
|
37
|
+
import requests
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
resp = requests.get(url, timeout=30, allow_redirects=True)
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
except requests.RequestException as exc:
|
|
43
|
+
click.echo(json.dumps({"error": f"Failed to fetch {url}: {exc}", "valid": False}))
|
|
44
|
+
sys.exit(3)
|
|
45
|
+
|
|
46
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
|
|
47
|
+
tmp.write(resp.text)
|
|
48
|
+
tmp.close()
|
|
49
|
+
return tmp.name
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _result_to_json(result: VerificationResult, source: str) -> dict[str, Any]:
|
|
53
|
+
"""Convert a VerificationResult to a JSON-serialisable dict."""
|
|
54
|
+
output: dict[str, Any] = {
|
|
55
|
+
"source": source,
|
|
56
|
+
"valid": result.valid,
|
|
57
|
+
"schema_valid": result.schema_valid,
|
|
58
|
+
"signature_valid": result.signature_valid,
|
|
59
|
+
"errors": result.errors,
|
|
60
|
+
}
|
|
61
|
+
if result.manifest is not None:
|
|
62
|
+
output["manifest"] = result.manifest.to_dict()
|
|
63
|
+
return output
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.group()
|
|
67
|
+
@click.version_option(version="0.1.0")
|
|
68
|
+
def cli() -> None:
|
|
69
|
+
"""Arkava Trust Chain — open-source manifest verifier."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@cli.command("verify-manifest")
|
|
73
|
+
@click.argument("source")
|
|
74
|
+
@click.option("--keys", default=None, help="Path to operator public keys JSON file")
|
|
75
|
+
def verify_manifest(source: str, keys: str | None) -> None:
|
|
76
|
+
"""Verify a single manifest (schema + Ed25519 signature).
|
|
77
|
+
|
|
78
|
+
SOURCE is a local file path or a URL (http/https).
|
|
79
|
+
"""
|
|
80
|
+
key_store = PublicKeyStore.from_file(keys) if keys else PublicKeyStore.from_default()
|
|
81
|
+
|
|
82
|
+
if source.startswith("http://") or source.startswith("https://"):
|
|
83
|
+
local_path = _fetch_remote(source)
|
|
84
|
+
else:
|
|
85
|
+
local_path = source
|
|
86
|
+
|
|
87
|
+
if not Path(local_path).exists():
|
|
88
|
+
click.echo(json.dumps({"error": f"File not found: {source}", "valid": False}))
|
|
89
|
+
sys.exit(3)
|
|
90
|
+
|
|
91
|
+
result = verify_manifest_file(local_path, key_store)
|
|
92
|
+
output = _result_to_json(result, source)
|
|
93
|
+
click.echo(json.dumps(output, indent=2, sort_keys=True))
|
|
94
|
+
|
|
95
|
+
if not result.schema_valid:
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
if result.signature_valid is False:
|
|
98
|
+
sys.exit(2)
|
|
99
|
+
if not result.valid:
|
|
100
|
+
sys.exit(3)
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cli.command("verify-batch")
|
|
105
|
+
@click.argument("sources", nargs=-1, required=True)
|
|
106
|
+
@click.option("--keys", default=None, help="Path to operator public keys JSON file")
|
|
107
|
+
def verify_batch(sources: tuple[str, ...], keys: str | None) -> None:
|
|
108
|
+
"""Batch-verify multiple manifests.
|
|
109
|
+
|
|
110
|
+
Each SOURCE is a local file path or URL. Exits 0 only if ALL pass.
|
|
111
|
+
"""
|
|
112
|
+
key_store = PublicKeyStore.from_file(keys) if keys else PublicKeyStore.from_default()
|
|
113
|
+
|
|
114
|
+
results: list[dict[str, Any]] = []
|
|
115
|
+
all_valid = True
|
|
116
|
+
has_schema_error = False
|
|
117
|
+
has_sig_error = False
|
|
118
|
+
|
|
119
|
+
for source in sources:
|
|
120
|
+
if source.startswith("http://") or source.startswith("https://"):
|
|
121
|
+
local_path = _fetch_remote(source)
|
|
122
|
+
else:
|
|
123
|
+
local_path = source
|
|
124
|
+
|
|
125
|
+
result = verify_manifest_file(local_path, key_store)
|
|
126
|
+
entry = _result_to_json(result, source)
|
|
127
|
+
results.append(entry)
|
|
128
|
+
|
|
129
|
+
if not result.valid:
|
|
130
|
+
all_valid = False
|
|
131
|
+
if not result.schema_valid:
|
|
132
|
+
has_schema_error = True
|
|
133
|
+
if result.signature_valid is False:
|
|
134
|
+
has_sig_error = True
|
|
135
|
+
|
|
136
|
+
batch_output: dict[str, Any] = {
|
|
137
|
+
"total": len(sources),
|
|
138
|
+
"valid": sum(1 for r in results if r["valid"]),
|
|
139
|
+
"invalid": sum(1 for r in results if not r["valid"]),
|
|
140
|
+
"results": results,
|
|
141
|
+
}
|
|
142
|
+
click.echo(json.dumps(batch_output, indent=2, sort_keys=True))
|
|
143
|
+
|
|
144
|
+
if has_schema_error:
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
if has_sig_error:
|
|
147
|
+
sys.exit(2)
|
|
148
|
+
if not all_valid:
|
|
149
|
+
sys.exit(3)
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@cli.command("validate-schema")
|
|
154
|
+
@click.argument("path")
|
|
155
|
+
def validate_schema(path: str) -> None:
|
|
156
|
+
"""Validate a manifest file against the v1 JSON Schema only (no signature check)."""
|
|
157
|
+
file_path = Path(path)
|
|
158
|
+
if not file_path.exists():
|
|
159
|
+
click.echo(json.dumps({"error": f"File not found: {path}", "valid": False}))
|
|
160
|
+
sys.exit(3)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
164
|
+
data = json.load(f)
|
|
165
|
+
except json.JSONDecodeError as exc:
|
|
166
|
+
click.echo(json.dumps({"error": f"JSON parse error: {exc}", "valid": False}))
|
|
167
|
+
sys.exit(3)
|
|
168
|
+
|
|
169
|
+
valid = verify_manifest_schema(data)
|
|
170
|
+
click.echo(json.dumps({"path": path, "schema_valid": valid}, indent=2, sort_keys=True))
|
|
171
|
+
sys.exit(0 if valid else 1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@cli.command("list-keys")
|
|
175
|
+
@click.option("--keys", default=None, help="Path to operator public keys JSON file")
|
|
176
|
+
def list_keys(keys: str | None) -> None:
|
|
177
|
+
"""List published operator public keys."""
|
|
178
|
+
key_store = PublicKeyStore.from_file(keys) if keys else PublicKeyStore.from_default()
|
|
179
|
+
dids = key_store.list_dids()
|
|
180
|
+
output: list[dict[str, str]] = []
|
|
181
|
+
for did in dids:
|
|
182
|
+
pk = key_store.lookup(did)
|
|
183
|
+
if pk:
|
|
184
|
+
output.append({"participant_did": did, "public_key_hex": pk})
|
|
185
|
+
click.echo(json.dumps({"keys": output}, indent=2, sort_keys=True))
|
|
186
|
+
sys.exit(0)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@cli.command("list-manifests")
|
|
190
|
+
@click.argument("directory")
|
|
191
|
+
def list_manifests(directory: str) -> None:
|
|
192
|
+
"""List manifest files in a witness-history directory."""
|
|
193
|
+
dir_path = Path(directory)
|
|
194
|
+
if not dir_path.exists():
|
|
195
|
+
click.echo(json.dumps({"error": f"Directory not found: {directory}"}))
|
|
196
|
+
sys.exit(3)
|
|
197
|
+
|
|
198
|
+
manifests = sorted(dir_path.rglob("manifest_*.json"))
|
|
199
|
+
output = [{"path": str(m.relative_to(dir_path))} for m in manifests]
|
|
200
|
+
click.echo(json.dumps({"directory": directory, "manifests": output}, indent=2, sort_keys=True))
|
|
201
|
+
sys.exit(0)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main() -> None:
|
|
205
|
+
"""Entry point for the trustchain-verify CLI."""
|
|
206
|
+
cli()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustchain-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-source CLI verifier for Arkava Trust Chain witness manifests
|
|
5
|
+
Author: Arkava Ltd
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Arkava-Ai/arkava-trustchain
|
|
8
|
+
Project-URL: Source, https://github.com/Arkava-Ai/arkava-trustchain
|
|
9
|
+
Project-URL: Documentation, https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md
|
|
10
|
+
Keywords: arkava,trust-chain,audit,sovereign-ai,verifier
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Classifier: Topic :: System :: Systems Administration
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: click>=8.1
|
|
23
|
+
Requires-Dist: trustchain-core
|
|
24
|
+
Requires-Dist: requests>=2.31
|
|
25
|
+
|
|
26
|
+
# trustchain-verify
|
|
27
|
+
|
|
28
|
+
Open-source CLI verifier for Arkava® Trust Chain witness manifests.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install trustchain-verify
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Python 3.11+.
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Verify a local manifest file
|
|
42
|
+
trustchain-verify verify-manifest manifest.json
|
|
43
|
+
|
|
44
|
+
# Verify a remote manifest
|
|
45
|
+
trustchain-verify verify-manifest https://raw.githubusercontent.com/Arkava-Ai/arkava-trustchain/main/witness-history/2026/06/26/manifest_2026-06-26.json
|
|
46
|
+
|
|
47
|
+
# Batch-verify multiple manifests
|
|
48
|
+
trustchain-verify verify-batch manifest_1.json manifest_2.json
|
|
49
|
+
|
|
50
|
+
# Validate schema only (no signature check)
|
|
51
|
+
trustchain-verify validate-schema manifest.json
|
|
52
|
+
|
|
53
|
+
# List published operator public keys
|
|
54
|
+
trustchain-verify list-keys
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Exit Codes
|
|
58
|
+
|
|
59
|
+
| Code | Meaning |
|
|
60
|
+
|---|---|
|
|
61
|
+
| 0 | Verification succeeded |
|
|
62
|
+
| 1 | Schema validation error |
|
|
63
|
+
| 2 | Signature verification error |
|
|
64
|
+
| 3 | Usage error |
|
|
65
|
+
|
|
66
|
+
## Documentation
|
|
67
|
+
|
|
68
|
+
- CLI reference: [`docs/TRUSTCHAIN_VERIFY_CLI.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md)
|
|
69
|
+
- Architecture: [`docs/Arkava_TrustChain_Technical_Architecture_v0.2.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/Arkava_TrustChain_Technical_Architecture_v0.2.md) §2.9
|
|
70
|
+
- ADR: [TC-ADR-0011](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/adr/TC-ADR-0011-api-and-verification.md)
|
|
71
|
+
|
|
72
|
+
## Licence
|
|
73
|
+
|
|
74
|
+
Apache-2.0. Arkava® Trust Chain is a registered trademark of Arkava Ltd.
|
|
75
|
+
|
|
76
|
+
© 2026 Arkava® / All rights reserved.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/trustchain_verify/__init__.py
|
|
4
|
+
src/trustchain_verify/cli.py
|
|
5
|
+
src/trustchain_verify.egg-info/PKG-INFO
|
|
6
|
+
src/trustchain_verify.egg-info/SOURCES.txt
|
|
7
|
+
src/trustchain_verify.egg-info/dependency_links.txt
|
|
8
|
+
src/trustchain_verify.egg-info/entry_points.txt
|
|
9
|
+
src/trustchain_verify.egg-info/requires.txt
|
|
10
|
+
src/trustchain_verify.egg-info/top_level.txt
|
|
11
|
+
tests/test_cli.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trustchain_verify
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""trustchain-verify CLI tests (ARK-3560 AC 7-10).
|
|
2
|
+
|
|
3
|
+
Acceptance criteria:
|
|
4
|
+
AC7: CLI verify-manifest <path-or-url> — verifies local + remote
|
|
5
|
+
AC8: CLI exit codes: 0=success, 1=schema error, 2=signature error, 3=usage error
|
|
6
|
+
AC9: CLI batch verification: verify-batch <path1> <path2> ...
|
|
7
|
+
AC10: CLI help text comprehensive with examples
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from click.testing import CliRunner
|
|
18
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
19
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
20
|
+
Encoding,
|
|
21
|
+
NoEncryption,
|
|
22
|
+
PrivateFormat,
|
|
23
|
+
PublicFormat,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from trustchain_core.witness import Manifest
|
|
27
|
+
from trustchain_core.witness.canonical_encoding import ALGORITHM_ID_SHA3_512
|
|
28
|
+
from trustchain_core.witness.verifier import _reconstruct_signed_data
|
|
29
|
+
from trustchain_verify.cli import cli
|
|
30
|
+
|
|
31
|
+
_TEST_DID = "did:arkava:log-test"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_json(output: str) -> Any:
|
|
35
|
+
lines = output.strip().split("\n")
|
|
36
|
+
for i in range(len(lines) - 1, -1, -1):
|
|
37
|
+
if lines[i].strip().startswith("{"):
|
|
38
|
+
candidate = "\n".join(lines[i:])
|
|
39
|
+
try:
|
|
40
|
+
return json.loads(candidate)
|
|
41
|
+
except json.JSONDecodeError:
|
|
42
|
+
continue
|
|
43
|
+
return json.loads(lines[-1])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _make_keypair() -> tuple[bytes, bytes]:
|
|
47
|
+
private_key = Ed25519PrivateKey.generate()
|
|
48
|
+
seed = private_key.private_bytes(
|
|
49
|
+
encoding=Encoding.Raw,
|
|
50
|
+
format=PrivateFormat.Raw,
|
|
51
|
+
encryption_algorithm=NoEncryption(),
|
|
52
|
+
)
|
|
53
|
+
public = private_key.public_key().public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
54
|
+
return seed, public
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sha3_512_root(seed: str = "test") -> str:
|
|
58
|
+
return hashlib.sha3_512(seed.encode()).hexdigest()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _sign_manifest(manifest: Manifest, tree_height: int, seed: bytes) -> bytes:
|
|
62
|
+
root_hash_bytes = bytes.fromhex(manifest.merkle_root)
|
|
63
|
+
signed_data = _reconstruct_signed_data(
|
|
64
|
+
tree_height,
|
|
65
|
+
manifest.participant_did,
|
|
66
|
+
manifest.timestamp,
|
|
67
|
+
root_hash_bytes,
|
|
68
|
+
)
|
|
69
|
+
key = Ed25519PrivateKey.from_private_bytes(seed)
|
|
70
|
+
return key.sign(signed_data)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _make_signed_manifest_dict(
|
|
74
|
+
public_key_hex: str, seed: bytes, tree_height: int = 2
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
root = _sha3_512_root("cli-test-root")
|
|
77
|
+
timestamp = "2026-06-26T12:00:00Z"
|
|
78
|
+
manifest = Manifest(
|
|
79
|
+
timestamp=timestamp,
|
|
80
|
+
merkle_root=root,
|
|
81
|
+
participant_did=_TEST_DID,
|
|
82
|
+
batch_count=5,
|
|
83
|
+
stump_proof_path=[],
|
|
84
|
+
algorithm_id=ALGORITHM_ID_SHA3_512,
|
|
85
|
+
key_version="v1",
|
|
86
|
+
tree_height=tree_height,
|
|
87
|
+
)
|
|
88
|
+
signature = _sign_manifest(manifest, tree_height, seed)
|
|
89
|
+
return {
|
|
90
|
+
"manifest_version": "v1",
|
|
91
|
+
"timestamp": timestamp,
|
|
92
|
+
"merkle_root": root,
|
|
93
|
+
"participant_did": _TEST_DID,
|
|
94
|
+
"batch_count": 5,
|
|
95
|
+
"stump_proof_path": [],
|
|
96
|
+
"algorithm_id": ALGORITHM_ID_SHA3_512,
|
|
97
|
+
"key_version": "v1",
|
|
98
|
+
"signature_hex": signature.hex(),
|
|
99
|
+
"tree_height": tree_height,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _make_key_file(tmp_path: Path, public_key_hex: str, did: str = _TEST_DID) -> Path:
|
|
104
|
+
key_file = tmp_path / "keys.json"
|
|
105
|
+
key_file.write_text(
|
|
106
|
+
json.dumps(
|
|
107
|
+
{
|
|
108
|
+
"keys": [
|
|
109
|
+
{"participant_did": did, "key_version": "v1", "public_key_hex": public_key_hex}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
),
|
|
113
|
+
encoding="utf-8",
|
|
114
|
+
)
|
|
115
|
+
return key_file
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _write_manifest(
|
|
119
|
+
tmp_path: Path, manifest_dict: dict[str, Any], name: str = "manifest.json"
|
|
120
|
+
) -> Path:
|
|
121
|
+
p = tmp_path / name
|
|
122
|
+
p.write_text(json.dumps(manifest_dict, sort_keys=True), encoding="utf-8")
|
|
123
|
+
return p
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestAC7VerifyManifest:
|
|
127
|
+
"""AC7: CLI verify-manifest <path-or-url> — verifies local + remote."""
|
|
128
|
+
|
|
129
|
+
def test_verify_valid_local_manifest_exit_0(self, tmp_path: Path) -> None:
|
|
130
|
+
seed, public = _make_keypair()
|
|
131
|
+
manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
|
|
132
|
+
manifest_path = _write_manifest(tmp_path, manifest_dict)
|
|
133
|
+
key_file = _make_key_file(tmp_path, public.hex())
|
|
134
|
+
|
|
135
|
+
runner = CliRunner()
|
|
136
|
+
result = runner.invoke(
|
|
137
|
+
cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert result.exit_code == 0
|
|
141
|
+
output = _extract_json(result.output)
|
|
142
|
+
assert output["valid"] is True
|
|
143
|
+
assert output["schema_valid"] is True
|
|
144
|
+
assert output["signature_valid"] is True
|
|
145
|
+
|
|
146
|
+
def test_verify_schema_invalid_manifest_exit_1(self, tmp_path: Path) -> None:
|
|
147
|
+
bad_data: dict[str, Any] = {
|
|
148
|
+
"manifest_version": "v1",
|
|
149
|
+
"timestamp": "2026-06-26T12:00:00Z",
|
|
150
|
+
"merkle_root": _sha3_512_root(),
|
|
151
|
+
"participant_did": "",
|
|
152
|
+
"batch_count": 1,
|
|
153
|
+
"stump_proof_path": [],
|
|
154
|
+
"algorithm_id": ALGORITHM_ID_SHA3_512,
|
|
155
|
+
"key_version": "v1",
|
|
156
|
+
}
|
|
157
|
+
manifest_path = _write_manifest(tmp_path, bad_data, "bad.json")
|
|
158
|
+
key_file = _make_key_file(tmp_path, "a" * 64)
|
|
159
|
+
|
|
160
|
+
runner = CliRunner()
|
|
161
|
+
result = runner.invoke(
|
|
162
|
+
cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
assert result.exit_code == 1
|
|
166
|
+
|
|
167
|
+
def test_verify_tampered_signature_exit_2(self, tmp_path: Path) -> None:
|
|
168
|
+
seed, public = _make_keypair()
|
|
169
|
+
manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
|
|
170
|
+
manifest_dict["merkle_root"] = _sha3_512_root("tampered")
|
|
171
|
+
manifest_path = _write_manifest(tmp_path, manifest_dict, "tampered.json")
|
|
172
|
+
key_file = _make_key_file(tmp_path, public.hex())
|
|
173
|
+
|
|
174
|
+
runner = CliRunner()
|
|
175
|
+
result = runner.invoke(
|
|
176
|
+
cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
assert result.exit_code == 2
|
|
180
|
+
|
|
181
|
+
def test_verify_nonexistent_file_exit_3(self, tmp_path: Path) -> None:
|
|
182
|
+
key_file = _make_key_file(tmp_path, "a" * 64)
|
|
183
|
+
|
|
184
|
+
runner = CliRunner()
|
|
185
|
+
result = runner.invoke(
|
|
186
|
+
cli, ["verify-manifest", str(tmp_path / "nope.json"), "--keys", str(key_file)]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
assert result.exit_code == 3
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestAC8ExitCodes:
|
|
193
|
+
"""AC8: CLI exit codes: 0=success, 1=schema error, 2=signature error, 3=usage error."""
|
|
194
|
+
|
|
195
|
+
def test_exit_0_on_valid_manifest(self, tmp_path: Path) -> None:
|
|
196
|
+
seed, public = _make_keypair()
|
|
197
|
+
manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
|
|
198
|
+
manifest_path = _write_manifest(tmp_path, manifest_dict)
|
|
199
|
+
key_file = _make_key_file(tmp_path, public.hex())
|
|
200
|
+
|
|
201
|
+
runner = CliRunner()
|
|
202
|
+
result = runner.invoke(
|
|
203
|
+
cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
|
|
204
|
+
)
|
|
205
|
+
assert result.exit_code == 0
|
|
206
|
+
|
|
207
|
+
def test_exit_1_on_schema_error(self, tmp_path: Path) -> None:
|
|
208
|
+
bad_data: dict[str, Any] = {"not": "valid"}
|
|
209
|
+
manifest_path = _write_manifest(tmp_path, bad_data, "bad.json")
|
|
210
|
+
key_file = _make_key_file(tmp_path, "a" * 64)
|
|
211
|
+
|
|
212
|
+
runner = CliRunner()
|
|
213
|
+
result = runner.invoke(
|
|
214
|
+
cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
|
|
215
|
+
)
|
|
216
|
+
assert result.exit_code == 1
|
|
217
|
+
|
|
218
|
+
def test_exit_2_on_signature_error(self, tmp_path: Path) -> None:
|
|
219
|
+
seed, public = _make_keypair()
|
|
220
|
+
manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
|
|
221
|
+
manifest_dict["signature_hex"] = "00" * 64
|
|
222
|
+
manifest_path = _write_manifest(tmp_path, manifest_dict, "badsig.json")
|
|
223
|
+
key_file = _make_key_file(tmp_path, public.hex())
|
|
224
|
+
|
|
225
|
+
runner = CliRunner()
|
|
226
|
+
result = runner.invoke(
|
|
227
|
+
cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
|
|
228
|
+
)
|
|
229
|
+
assert result.exit_code == 2
|
|
230
|
+
|
|
231
|
+
def test_exit_3_on_missing_file(self, tmp_path: Path) -> None:
|
|
232
|
+
key_file = _make_key_file(tmp_path, "a" * 64)
|
|
233
|
+
|
|
234
|
+
runner = CliRunner()
|
|
235
|
+
result = runner.invoke(
|
|
236
|
+
cli, ["verify-manifest", "/nonexistent/path.json", "--keys", str(key_file)]
|
|
237
|
+
)
|
|
238
|
+
assert result.exit_code == 3
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TestAC9BatchVerification:
|
|
242
|
+
"""AC9: CLI batch verification: verify-batch <path1> <path2> ..."""
|
|
243
|
+
|
|
244
|
+
def test_batch_all_valid_exit_0(self, tmp_path: Path) -> None:
|
|
245
|
+
seed, public = _make_keypair()
|
|
246
|
+
m1 = _make_signed_manifest_dict(public.hex(), seed, tree_height=1)
|
|
247
|
+
m2 = _make_signed_manifest_dict(public.hex(), seed, tree_height=2)
|
|
248
|
+
p1 = _write_manifest(tmp_path, m1, "m1.json")
|
|
249
|
+
p2 = _write_manifest(tmp_path, m2, "m2.json")
|
|
250
|
+
key_file = _make_key_file(tmp_path, public.hex())
|
|
251
|
+
|
|
252
|
+
runner = CliRunner()
|
|
253
|
+
result = runner.invoke(cli, ["verify-batch", str(p1), str(p2), "--keys", str(key_file)])
|
|
254
|
+
|
|
255
|
+
assert result.exit_code == 0
|
|
256
|
+
output = _extract_json(result.output)
|
|
257
|
+
assert output["total"] == 2
|
|
258
|
+
assert output["valid"] == 2
|
|
259
|
+
assert output["invalid"] == 0
|
|
260
|
+
|
|
261
|
+
def test_batch_one_invalid_exit_2(self, tmp_path: Path) -> None:
|
|
262
|
+
seed, public = _make_keypair()
|
|
263
|
+
m1 = _make_signed_manifest_dict(public.hex(), seed, tree_height=1)
|
|
264
|
+
m2 = _make_signed_manifest_dict(public.hex(), seed, tree_height=2)
|
|
265
|
+
m2["merkle_root"] = _sha3_512_root("tampered")
|
|
266
|
+
p1 = _write_manifest(tmp_path, m1, "m1.json")
|
|
267
|
+
p2 = _write_manifest(tmp_path, m2, "m2.json")
|
|
268
|
+
key_file = _make_key_file(tmp_path, public.hex())
|
|
269
|
+
|
|
270
|
+
runner = CliRunner()
|
|
271
|
+
result = runner.invoke(cli, ["verify-batch", str(p1), str(p2), "--keys", str(key_file)])
|
|
272
|
+
|
|
273
|
+
assert result.exit_code == 2
|
|
274
|
+
output = _extract_json(result.output)
|
|
275
|
+
assert output["total"] == 2
|
|
276
|
+
assert output["valid"] == 1
|
|
277
|
+
assert output["invalid"] == 1
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class TestAC10HelpText:
|
|
281
|
+
"""AC10: CLI help text comprehensive with examples."""
|
|
282
|
+
|
|
283
|
+
def test_top_level_help(self) -> None:
|
|
284
|
+
runner = CliRunner()
|
|
285
|
+
result = runner.invoke(cli, ["--help"])
|
|
286
|
+
|
|
287
|
+
assert result.exit_code == 0
|
|
288
|
+
assert "verify-manifest" in result.output
|
|
289
|
+
assert "verify-batch" in result.output
|
|
290
|
+
assert "validate-schema" in result.output
|
|
291
|
+
assert "list-keys" in result.output
|
|
292
|
+
assert "list-manifests" in result.output
|
|
293
|
+
|
|
294
|
+
def test_verify_manifest_help(self) -> None:
|
|
295
|
+
runner = CliRunner()
|
|
296
|
+
result = runner.invoke(cli, ["verify-manifest", "--help"])
|
|
297
|
+
|
|
298
|
+
assert result.exit_code == 0
|
|
299
|
+
assert "SOURCE" in result.output
|
|
300
|
+
assert "schema" in result.output
|
|
301
|
+
assert "signature" in result.output
|
|
302
|
+
|
|
303
|
+
def test_verify_batch_help(self) -> None:
|
|
304
|
+
runner = CliRunner()
|
|
305
|
+
result = runner.invoke(cli, ["verify-batch", "--help"])
|
|
306
|
+
|
|
307
|
+
assert result.exit_code == 0
|
|
308
|
+
assert "SOURCES" in result.output or "sources" in result.output.lower()
|
|
309
|
+
|
|
310
|
+
def test_version_flag(self) -> None:
|
|
311
|
+
runner = CliRunner()
|
|
312
|
+
result = runner.invoke(cli, ["--version"])
|
|
313
|
+
|
|
314
|
+
assert result.exit_code == 0
|
|
315
|
+
assert "0.1.0" in result.output
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class TestAdditionalCommands:
|
|
319
|
+
"""validate-schema, list-keys, list-manifests commands."""
|
|
320
|
+
|
|
321
|
+
def test_validate_schema_valid_exit_0(self, tmp_path: Path) -> None:
|
|
322
|
+
data: dict[str, Any] = {
|
|
323
|
+
"manifest_version": "v1",
|
|
324
|
+
"timestamp": "2026-06-26T12:00:00Z",
|
|
325
|
+
"merkle_root": _sha3_512_root(),
|
|
326
|
+
"participant_did": "did:arkava:test",
|
|
327
|
+
"batch_count": 1,
|
|
328
|
+
"stump_proof_path": [],
|
|
329
|
+
"algorithm_id": ALGORITHM_ID_SHA3_512,
|
|
330
|
+
"key_version": "v1",
|
|
331
|
+
}
|
|
332
|
+
manifest_path = _write_manifest(tmp_path, data, "valid.json")
|
|
333
|
+
|
|
334
|
+
runner = CliRunner()
|
|
335
|
+
result = runner.invoke(cli, ["validate-schema", str(manifest_path)])
|
|
336
|
+
|
|
337
|
+
assert result.exit_code == 0
|
|
338
|
+
output = _extract_json(result.output)
|
|
339
|
+
assert output["schema_valid"] is True
|
|
340
|
+
|
|
341
|
+
def test_validate_schema_invalid_exit_1(self, tmp_path: Path) -> None:
|
|
342
|
+
data: dict[str, Any] = {"not": "valid"}
|
|
343
|
+
manifest_path = _write_manifest(tmp_path, data, "invalid.json")
|
|
344
|
+
|
|
345
|
+
runner = CliRunner()
|
|
346
|
+
result = runner.invoke(cli, ["validate-schema", str(manifest_path)])
|
|
347
|
+
|
|
348
|
+
assert result.exit_code == 1
|
|
349
|
+
|
|
350
|
+
def test_list_keys(self, tmp_path: Path) -> None:
|
|
351
|
+
key_file = _make_key_file(tmp_path, "ab" * 32, did="did:arkava:list-test")
|
|
352
|
+
|
|
353
|
+
runner = CliRunner()
|
|
354
|
+
result = runner.invoke(cli, ["list-keys", "--keys", str(key_file)])
|
|
355
|
+
|
|
356
|
+
assert result.exit_code == 0
|
|
357
|
+
output = _extract_json(result.output)
|
|
358
|
+
assert len(output["keys"]) == 1
|
|
359
|
+
assert output["keys"][0]["participant_did"] == "did:arkava:list-test"
|
|
360
|
+
|
|
361
|
+
def test_list_manifests(self, tmp_path: Path) -> None:
|
|
362
|
+
witness_dir = tmp_path / "witness-history" / "2026" / "06" / "26"
|
|
363
|
+
witness_dir.mkdir(parents=True)
|
|
364
|
+
(witness_dir / "manifest_2026-06-26.json").write_text("{}", encoding="utf-8")
|
|
365
|
+
|
|
366
|
+
runner = CliRunner()
|
|
367
|
+
result = runner.invoke(cli, ["list-manifests", str(tmp_path)])
|
|
368
|
+
|
|
369
|
+
assert result.exit_code == 0
|
|
370
|
+
output = _extract_json(result.output)
|
|
371
|
+
assert len(output["manifests"]) == 1
|
|
372
|
+
|
|
373
|
+
def test_list_manifests_nonexistent_dir_exit_3(self, tmp_path: Path) -> None:
|
|
374
|
+
runner = CliRunner()
|
|
375
|
+
result = runner.invoke(cli, ["list-manifests", str(tmp_path / "nope")])
|
|
376
|
+
|
|
377
|
+
assert result.exit_code == 3
|