aevum-cli 0.6.0__tar.gz → 0.7.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.
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/PKG-INFO +1 -1
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/pyproject.toml +1 -1
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/__init__.py +1 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/__main__.py +1 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/app.py +169 -1
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/commands/__init__.py +1 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/commands/complication.py +1 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/commands/conformance.py +1 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/commands/server.py +1 -0
- aevum_cli-0.7.0/src/aevum/cli/commands/store.py +121 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/commands/version.py +1 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/tests/test_cli.py +1 -0
- aevum_cli-0.7.0/tests/test_verify_receipt.py +143 -0
- aevum_cli-0.6.0/src/aevum/cli/commands/store.py +0 -51
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/.gitignore +0 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/README.md +0 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/src/aevum/cli/py.typed +0 -0
- {aevum_cli-0.6.0 → aevum_cli-0.7.0}/tests/test_phase8_cli.py +0 -0
|
@@ -5,9 +5,10 @@ Top-level typer app — CLI v2. Sub-commands and direct commands registered here
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import hashlib
|
|
8
9
|
import json
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Annotated
|
|
11
|
+
from typing import Annotated, Any
|
|
11
12
|
|
|
12
13
|
import typer
|
|
13
14
|
|
|
@@ -199,6 +200,173 @@ def conform(
|
|
|
199
200
|
raise typer.Exit(code=1)
|
|
200
201
|
|
|
201
202
|
|
|
203
|
+
@app.command(name="verify-receipt")
|
|
204
|
+
def verify_receipt(
|
|
205
|
+
receipt_file: Annotated[
|
|
206
|
+
Path | None,
|
|
207
|
+
typer.Argument(help="Path to COSE_Sign1 receipt file"),
|
|
208
|
+
] = None,
|
|
209
|
+
receipt_hash: Annotated[
|
|
210
|
+
str | None,
|
|
211
|
+
typer.Option("--hash", help="SHA3-256 hex hash — lookup from AEVUM_RECEIPT_DB"),
|
|
212
|
+
] = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Verify an Aevum COSE_Sign1 receipt file or hash.
|
|
216
|
+
|
|
217
|
+
Decodes the receipt, verifies the Ed25519 signature over the canonical payload,
|
|
218
|
+
and prints a human-readable summary. Exit 0 on valid, exit 1 on invalid signature.
|
|
219
|
+
Exit 2 on unsupported algorithm or hash not found.
|
|
220
|
+
|
|
221
|
+
Examples:
|
|
222
|
+
aevum verify-receipt receipt.cbor
|
|
223
|
+
aevum verify-receipt --hash <sha3-256-hex>
|
|
224
|
+
"""
|
|
225
|
+
import cbor2
|
|
226
|
+
import nacl.exceptions
|
|
227
|
+
import nacl.signing
|
|
228
|
+
from aevum.core.receipt import AevumReceipt
|
|
229
|
+
|
|
230
|
+
if receipt_file is None and receipt_hash is None:
|
|
231
|
+
typer.echo("Provide a receipt file path or --hash <hash>.", err=True)
|
|
232
|
+
raise typer.Exit(code=1)
|
|
233
|
+
if receipt_file is not None and receipt_hash is not None:
|
|
234
|
+
typer.echo("Provide either a file path or --hash, not both.", err=True)
|
|
235
|
+
raise typer.Exit(code=1)
|
|
236
|
+
|
|
237
|
+
store_info: dict[str, object] | None = None
|
|
238
|
+
|
|
239
|
+
if receipt_hash is not None:
|
|
240
|
+
try:
|
|
241
|
+
from aevum.core.sqlite_store import SqliteReceiptStore
|
|
242
|
+
store = SqliteReceiptStore.from_env()
|
|
243
|
+
raw = store.get(receipt_hash)
|
|
244
|
+
if raw is None:
|
|
245
|
+
typer.echo(f"RECEIPT NOT FOUND: {receipt_hash}", err=True)
|
|
246
|
+
raise typer.Exit(code=2)
|
|
247
|
+
store_info = store.get_receipt_info(receipt_hash)
|
|
248
|
+
except typer.Exit:
|
|
249
|
+
raise
|
|
250
|
+
except RuntimeError as exc:
|
|
251
|
+
typer.echo(f"Store error: {exc}", err=True)
|
|
252
|
+
raise typer.Exit(code=1) from None
|
|
253
|
+
else:
|
|
254
|
+
assert receipt_file is not None
|
|
255
|
+
if not receipt_file.exists():
|
|
256
|
+
typer.echo(f"File not found: {receipt_file}", err=True)
|
|
257
|
+
raise typer.Exit(code=1)
|
|
258
|
+
raw = receipt_file.read_bytes()
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
cose = cbor2.loads(raw)
|
|
262
|
+
except Exception as exc: # noqa: BLE001
|
|
263
|
+
typer.echo(f"INVALID: not a valid CBOR file: {exc}", err=True)
|
|
264
|
+
raise typer.Exit(code=1) from None
|
|
265
|
+
|
|
266
|
+
if not isinstance(cose, list) or len(cose) != 4:
|
|
267
|
+
typer.echo("INVALID: not a 4-element COSE_Sign1 array", err=True)
|
|
268
|
+
raise typer.Exit(code=1)
|
|
269
|
+
|
|
270
|
+
protected_bstr, unprotected, payload_bstr, signature_bytes = cose
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
protected = cbor2.loads(protected_bstr)
|
|
274
|
+
except Exception as exc: # noqa: BLE001
|
|
275
|
+
typer.echo(f"INVALID: cannot decode protected header: {exc}", err=True)
|
|
276
|
+
raise typer.Exit(code=1) from None
|
|
277
|
+
|
|
278
|
+
alg = protected.get(1)
|
|
279
|
+
if alg != -8:
|
|
280
|
+
typer.echo(f"UNSUPPORTED ALGORITHM: alg={alg!r} (expected -8 for EdDSA/Ed25519)", err=True)
|
|
281
|
+
raise typer.Exit(code=2) from None
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
receipt_data = cbor2.loads(payload_bstr)
|
|
285
|
+
receipt = AevumReceipt.model_validate(receipt_data)
|
|
286
|
+
except Exception as exc: # noqa: BLE001
|
|
287
|
+
typer.echo(f"INVALID: cannot decode receipt payload: {exc}", err=True)
|
|
288
|
+
raise typer.Exit(code=1) from None
|
|
289
|
+
|
|
290
|
+
# Reconstruct Sig_Structure and verify signature
|
|
291
|
+
sig_structure = cbor2.dumps(["Signature1", protected_bstr, b"", payload_bstr])
|
|
292
|
+
digest = hashlib.sha3_256(sig_structure).digest()
|
|
293
|
+
|
|
294
|
+
# Try to find Ed25519 public key from state dir (kid field in protected header reserved for future)
|
|
295
|
+
pub_key_bytes: bytes | None = None
|
|
296
|
+
state_dir = Path.home() / ".aevum"
|
|
297
|
+
ed25519_pub_path = state_dir / "ed25519.pub"
|
|
298
|
+
if ed25519_pub_path.exists():
|
|
299
|
+
pub_key_bytes = ed25519_pub_path.read_bytes()
|
|
300
|
+
|
|
301
|
+
if pub_key_bytes is None:
|
|
302
|
+
typer.echo(
|
|
303
|
+
"WARNING: no public key found in ~/.aevum/ed25519.pub — skipping signature check.",
|
|
304
|
+
err=True,
|
|
305
|
+
)
|
|
306
|
+
typer.echo("WARNING: receipt content is displayed but authenticity is NOT verified.", err=True)
|
|
307
|
+
verified = False
|
|
308
|
+
else:
|
|
309
|
+
try:
|
|
310
|
+
verify_key = nacl.signing.VerifyKey(pub_key_bytes)
|
|
311
|
+
verify_key.verify(digest, bytes(signature_bytes))
|
|
312
|
+
verified = True
|
|
313
|
+
except nacl.exceptions.BadSignatureError:
|
|
314
|
+
typer.echo("SIGNATURE INVALID", err=True)
|
|
315
|
+
_print_receipt_summary(receipt, unprotected, verified=False, store_info=store_info)
|
|
316
|
+
raise typer.Exit(code=1) from None
|
|
317
|
+
except Exception as exc: # noqa: BLE001
|
|
318
|
+
typer.echo(f"SIGNATURE INVALID: {exc}", err=True)
|
|
319
|
+
raise typer.Exit(code=1) from None
|
|
320
|
+
|
|
321
|
+
_print_receipt_summary(receipt, unprotected, verified=verified, store_info=store_info)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _print_receipt_summary(
|
|
325
|
+
receipt: Any,
|
|
326
|
+
unprotected: dict, # type: ignore[type-arg]
|
|
327
|
+
verified: bool,
|
|
328
|
+
store_info: dict[str, object] | None = None,
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Print human-readable receipt summary."""
|
|
331
|
+
|
|
332
|
+
status_line = "✓ Aevum Receipt Verified" if verified else "! Aevum Receipt (unverified)"
|
|
333
|
+
typer.echo(status_line)
|
|
334
|
+
typer.echo("─" * 36)
|
|
335
|
+
|
|
336
|
+
tsa_info = "none"
|
|
337
|
+
if 9 in unprotected:
|
|
338
|
+
tsa_info = f"<RFC 3161 token, {len(unprotected[9])} bytes>"
|
|
339
|
+
|
|
340
|
+
barriers_summary = (
|
|
341
|
+
", ".join(f"{k}:{v}" for k, v in receipt.barrier_evaluations.items())
|
|
342
|
+
if receipt.barrier_evaluations
|
|
343
|
+
else "none"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
prior_display = receipt.prior_hash[:12] if len(receipt.prior_hash) >= 12 else receipt.prior_hash
|
|
347
|
+
model_display = receipt.model_identity_hash[:12] if len(receipt.model_identity_hash) >= 12 else receipt.model_identity_hash
|
|
348
|
+
|
|
349
|
+
typer.echo(f"Action: {receipt.action}")
|
|
350
|
+
typer.echo(f"Agent: {receipt.agent_id}")
|
|
351
|
+
typer.echo(f"Principal: {receipt.principal}")
|
|
352
|
+
typer.echo(f"Occurred at: {receipt.occurred_at}")
|
|
353
|
+
typer.echo(f"Sequence: {receipt.sequence}")
|
|
354
|
+
typer.echo(f"Prior hash: {prior_display}...")
|
|
355
|
+
typer.echo(f"Handoff type: {receipt.handoff_type or 'none'}")
|
|
356
|
+
typer.echo(f"Model hash: {model_display}...")
|
|
357
|
+
typer.echo(f"Policy version: {receipt.policy_version}")
|
|
358
|
+
typer.echo(f"TSA timestamp: {tsa_info}")
|
|
359
|
+
typer.echo(f"Barriers: {barriers_summary}")
|
|
360
|
+
|
|
361
|
+
if store_info is not None:
|
|
362
|
+
tier = store_info.get("tier", "unknown")
|
|
363
|
+
locked = store_info.get("locked", False)
|
|
364
|
+
rekor_ref = store_info.get("rekor_entry_ref", "") or "not submitted"
|
|
365
|
+
typer.echo(f"Tier: {tier}")
|
|
366
|
+
typer.echo(f"Crash-protected:{' yes' if locked else ' no'}")
|
|
367
|
+
typer.echo(f"Rekor ref: {rekor_ref}")
|
|
368
|
+
|
|
369
|
+
|
|
202
370
|
@app.command()
|
|
203
371
|
def replay(
|
|
204
372
|
session_id: Annotated[str, typer.Argument(help="Session ID to replay")],
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""
|
|
3
|
+
aevum store — manage graph store backends and receipt storage.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Manage graph store backends and receipt storage.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("migrate")
|
|
16
|
+
def migrate(
|
|
17
|
+
from_backend: Annotated[str, typer.Option("--from", help="Source backend (oxigraph:<path>)")] = "",
|
|
18
|
+
to_backend: Annotated[str, typer.Option("--to", help="Target backend (postgres:<dsn>)")] = "",
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Migrate graph data between backends."""
|
|
21
|
+
if not from_backend or not to_backend:
|
|
22
|
+
typer.echo("Both --from and --to are required.", err=True)
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
|
|
25
|
+
if not from_backend.startswith("oxigraph:"):
|
|
26
|
+
typer.echo(f"Unsupported source backend: {from_backend!r}", err=True)
|
|
27
|
+
typer.echo("Currently supported source: oxigraph:<path>", err=True)
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
|
|
30
|
+
if not to_backend.startswith("postgres:"):
|
|
31
|
+
typer.echo(f"Unsupported target backend: {to_backend!r}", err=True)
|
|
32
|
+
typer.echo("Currently supported target: postgres:<dsn>", err=True)
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from aevum.store.postgres.migrate import migrate_from_oxigraph
|
|
37
|
+
except ImportError:
|
|
38
|
+
typer.echo("Error: aevum-store-postgres is not installed.", err=True)
|
|
39
|
+
raise typer.Exit(code=1) from None
|
|
40
|
+
|
|
41
|
+
oxigraph_path = from_backend[len("oxigraph:"):]
|
|
42
|
+
postgres_dsn = to_backend[len("postgres:"):]
|
|
43
|
+
|
|
44
|
+
typer.echo(f"Migrating: {oxigraph_path} -> PostgreSQL")
|
|
45
|
+
try:
|
|
46
|
+
import psycopg
|
|
47
|
+
conn = psycopg.connect(postgres_dsn)
|
|
48
|
+
migrated = migrate_from_oxigraph(oxigraph_path, conn) # type: ignore[arg-type] # Phase 2: wire store construction
|
|
49
|
+
typer.echo(f"Migration complete: {migrated} entities transferred.")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
typer.echo(f"Migration failed: {e}", err=True)
|
|
52
|
+
raise typer.Exit(code=1) from e
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("migrate-receipts")
|
|
56
|
+
def migrate_receipts(
|
|
57
|
+
oxigraph_path: Annotated[
|
|
58
|
+
str,
|
|
59
|
+
typer.Option("--oxigraph", help="Path to Oxigraph store directory"),
|
|
60
|
+
] = "",
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Migrate receipt blobs from Oxigraph provenance graph to SQLite receipt store.
|
|
64
|
+
|
|
65
|
+
Checks for receipt blobs stored as xsd:base64Binary literals in the Oxigraph
|
|
66
|
+
provenance graph and moves them to the SQLite receipt store (AEVUM_RECEIPT_DB).
|
|
67
|
+
|
|
68
|
+
This is a one-time idempotent migration. For new deployments (no receipts in
|
|
69
|
+
Oxigraph), this is a no-op. Receipt blobs were never stored in Oxigraph in
|
|
70
|
+
versions <= 0.6.0, so this command is a no-op for all current deployments.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
from aevum.core.sqlite_store import SqliteReceiptStore
|
|
74
|
+
store = SqliteReceiptStore.from_env()
|
|
75
|
+
except RuntimeError as exc:
|
|
76
|
+
typer.echo(f"Store error: {exc}", err=True)
|
|
77
|
+
raise typer.Exit(code=1) from None
|
|
78
|
+
|
|
79
|
+
if not oxigraph_path:
|
|
80
|
+
typer.echo("No receipts to migrate. SQLite store is current.")
|
|
81
|
+
raise typer.Exit(code=0)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
from aevum.store.oxigraph.store import OxigraphStore
|
|
85
|
+
graph_store = OxigraphStore(path=oxigraph_path)
|
|
86
|
+
except ImportError:
|
|
87
|
+
typer.echo("Error: aevum-store-oxigraph is not installed.", err=True)
|
|
88
|
+
raise typer.Exit(code=1) from None
|
|
89
|
+
|
|
90
|
+
# Query for any receipt blobs stored as literals in the provenance graph.
|
|
91
|
+
# Receipt blobs were never stored in Oxigraph in v0.6.0 or earlier, so
|
|
92
|
+
# this query returns zero rows for all current deployments.
|
|
93
|
+
sparql = """
|
|
94
|
+
SELECT ?h ?b WHERE {
|
|
95
|
+
GRAPH <urn:aevum:provenance> {
|
|
96
|
+
?h <https://aevum.build/vocab/receiptBlob> ?b
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
rows = graph_store.sparql_select(sparql)
|
|
101
|
+
|
|
102
|
+
if not rows:
|
|
103
|
+
typer.echo("No receipts to migrate. SQLite store is current.")
|
|
104
|
+
raise typer.Exit(code=0)
|
|
105
|
+
|
|
106
|
+
import base64
|
|
107
|
+
migrated = 0
|
|
108
|
+
for row in rows:
|
|
109
|
+
h = row.get("h", "")
|
|
110
|
+
b_str = row.get("b", "")
|
|
111
|
+
if not h or not b_str:
|
|
112
|
+
continue
|
|
113
|
+
try:
|
|
114
|
+
blob = base64.b64decode(b_str)
|
|
115
|
+
receipt_hash = h.split(":")[-1] if ":" in h else h
|
|
116
|
+
store.put(receipt_hash=receipt_hash, blob=blob)
|
|
117
|
+
migrated += 1
|
|
118
|
+
except Exception as exc: # noqa: BLE001
|
|
119
|
+
typer.echo(f" Skipped {h}: {exc}", err=True)
|
|
120
|
+
|
|
121
|
+
typer.echo(f"Migrated {migrated} receipts from Oxigraph to SQLite.")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""
|
|
3
|
+
Tests for aevum verify-receipt CLI command.
|
|
4
|
+
Uses typer's CliRunner.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import cbor2
|
|
12
|
+
import nacl.signing
|
|
13
|
+
import pytest
|
|
14
|
+
from typer.testing import CliRunner
|
|
15
|
+
|
|
16
|
+
from aevum.cli.app import app
|
|
17
|
+
|
|
18
|
+
runner = CliRunner()
|
|
19
|
+
|
|
20
|
+
_ANSI = re.compile(r"\x1b\[[0-9;]*[mGKH]")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def plain(text: str) -> str:
|
|
24
|
+
return _ANSI.sub("", text)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_valid_receipt_file(tmp_path: Path) -> tuple[Path, nacl.signing.SigningKey]:
|
|
28
|
+
"""Create a valid COSE_Sign1 receipt file and return its path + signing key."""
|
|
29
|
+
from aevum.core.audit.event import AuditEvent
|
|
30
|
+
from aevum.core.receipt import AevumReceipt
|
|
31
|
+
from aevum.publish.encoder import ReceiptEncoder
|
|
32
|
+
|
|
33
|
+
signer_key = nacl.signing.SigningKey.generate()
|
|
34
|
+
|
|
35
|
+
class _NaClSigner:
|
|
36
|
+
key_id = "test-verify-key"
|
|
37
|
+
provenance = "test"
|
|
38
|
+
def sign(self, digest: bytes) -> bytes:
|
|
39
|
+
return bytes(signer_key.sign(digest).signature)
|
|
40
|
+
def public_key_bytes(self) -> bytes:
|
|
41
|
+
return bytes(signer_key.verify_key)
|
|
42
|
+
|
|
43
|
+
encoder = ReceiptEncoder(signer=_NaClSigner(), dev_mode=True) # type: ignore[arg-type]
|
|
44
|
+
event = AuditEvent(
|
|
45
|
+
event_id="ev-cli-test-01",
|
|
46
|
+
episode_id="ep-cli-01",
|
|
47
|
+
sequence=1,
|
|
48
|
+
event_type="cli.test.action",
|
|
49
|
+
schema_version="1.0",
|
|
50
|
+
valid_from="2026-05-24T00:00:00+00:00",
|
|
51
|
+
valid_to=None,
|
|
52
|
+
system_time=0,
|
|
53
|
+
causation_id=None,
|
|
54
|
+
correlation_id=None,
|
|
55
|
+
actor="cli-test-agent",
|
|
56
|
+
trace_id=None,
|
|
57
|
+
span_id=None,
|
|
58
|
+
payload={},
|
|
59
|
+
payload_hash=AuditEvent.hash_payload({}),
|
|
60
|
+
prior_hash="c" * 64,
|
|
61
|
+
signature="dGVzdA",
|
|
62
|
+
signer_key_id="test-key",
|
|
63
|
+
)
|
|
64
|
+
receipt = AevumReceipt.from_sigchain_event(event)
|
|
65
|
+
receipt_cbor = encoder.encode(receipt)
|
|
66
|
+
|
|
67
|
+
receipt_file = tmp_path / "test_receipt.cose"
|
|
68
|
+
receipt_file.write_bytes(receipt_cbor)
|
|
69
|
+
|
|
70
|
+
pub_key_path = tmp_path / ".aevum" / "ed25519.pub"
|
|
71
|
+
pub_key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
pub_key_path.write_bytes(bytes(signer_key.verify_key))
|
|
73
|
+
|
|
74
|
+
return receipt_file, signer_key
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_verify_receipt_help() -> None:
|
|
78
|
+
result = runner.invoke(app, ["verify-receipt", "--help"])
|
|
79
|
+
assert result.exit_code == 0
|
|
80
|
+
assert "receipt" in plain(result.output).lower()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_verify_receipt_file_not_found(tmp_path: Path) -> None:
|
|
84
|
+
result = runner.invoke(app, ["verify-receipt", str(tmp_path / "nonexistent.cose")])
|
|
85
|
+
assert result.exit_code == 1
|
|
86
|
+
assert "not found" in plain(result.output + (result.stdout or "")).lower()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_verify_receipt_invalid_cbor(tmp_path: Path) -> None:
|
|
90
|
+
bad_file = tmp_path / "bad.cose"
|
|
91
|
+
bad_file.write_bytes(b"\xff\xff\xff not valid cbor")
|
|
92
|
+
result = runner.invoke(app, ["verify-receipt", str(bad_file)])
|
|
93
|
+
assert result.exit_code == 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_verify_receipt_wrong_algorithm(tmp_path: Path) -> None:
|
|
97
|
+
protected_bstr = cbor2.dumps({1: -7, 3: "application/aevum-receipt+cbor", 4: b"kid"})
|
|
98
|
+
payload = cbor2.dumps({"action": "test"})
|
|
99
|
+
cose = [protected_bstr, {}, payload, b"sig"]
|
|
100
|
+
bad_file = tmp_path / "wrong_alg.cose"
|
|
101
|
+
bad_file.write_bytes(cbor2.dumps(cose))
|
|
102
|
+
result = runner.invoke(app, ["verify-receipt", str(bad_file)])
|
|
103
|
+
assert result.exit_code == 2
|
|
104
|
+
assert "UNSUPPORTED ALGORITHM" in plain(result.output + (result.stdout or ""))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_verify_receipt_no_public_key_shows_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
108
|
+
from aevum.core.audit.event import AuditEvent
|
|
109
|
+
from aevum.core.audit.signer import InProcessSigner
|
|
110
|
+
from aevum.core.receipt import AevumReceipt
|
|
111
|
+
from aevum.publish.encoder import ReceiptEncoder
|
|
112
|
+
|
|
113
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
114
|
+
signer = InProcessSigner()
|
|
115
|
+
encoder = ReceiptEncoder(signer=signer, dev_mode=True)
|
|
116
|
+
event = AuditEvent(
|
|
117
|
+
event_id="ev-warn-01",
|
|
118
|
+
episode_id="ep-warn-01",
|
|
119
|
+
sequence=1,
|
|
120
|
+
event_type="warn.test",
|
|
121
|
+
schema_version="1.0",
|
|
122
|
+
valid_from="2026-05-24T00:00:00+00:00",
|
|
123
|
+
valid_to=None,
|
|
124
|
+
system_time=0,
|
|
125
|
+
causation_id=None,
|
|
126
|
+
correlation_id=None,
|
|
127
|
+
actor="warn-agent",
|
|
128
|
+
trace_id=None,
|
|
129
|
+
span_id=None,
|
|
130
|
+
payload={},
|
|
131
|
+
payload_hash=AuditEvent.hash_payload({}),
|
|
132
|
+
prior_hash="d" * 64,
|
|
133
|
+
signature="dGVzdA",
|
|
134
|
+
signer_key_id="test-key",
|
|
135
|
+
)
|
|
136
|
+
receipt = AevumReceipt.from_sigchain_event(event)
|
|
137
|
+
receipt_cbor = encoder.encode(receipt)
|
|
138
|
+
receipt_file = tmp_path / "no_key_receipt.cose"
|
|
139
|
+
receipt_file.write_bytes(receipt_cbor)
|
|
140
|
+
|
|
141
|
+
result = runner.invoke(app, ["verify-receipt", str(receipt_file)])
|
|
142
|
+
output = plain(result.output + (result.stdout or ""))
|
|
143
|
+
assert "WARNING" in output or "unverified" in output.lower()
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
aevum store migrate -- migrate between graph backends.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
from typing import Annotated
|
|
8
|
-
|
|
9
|
-
import typer
|
|
10
|
-
|
|
11
|
-
app = typer.Typer(help="Manage graph store backends.")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.command("migrate")
|
|
15
|
-
def migrate(
|
|
16
|
-
from_backend: Annotated[str, typer.Option("--from", help="Source backend (oxigraph:<path>)")] = "",
|
|
17
|
-
to_backend: Annotated[str, typer.Option("--to", help="Target backend (postgres:<dsn>)")] = "",
|
|
18
|
-
) -> None:
|
|
19
|
-
"""Migrate graph data between backends."""
|
|
20
|
-
if not from_backend or not to_backend:
|
|
21
|
-
typer.echo("Both --from and --to are required.", err=True)
|
|
22
|
-
raise typer.Exit(code=1)
|
|
23
|
-
|
|
24
|
-
if not from_backend.startswith("oxigraph:"):
|
|
25
|
-
typer.echo(f"Unsupported source backend: {from_backend!r}", err=True)
|
|
26
|
-
typer.echo("Currently supported source: oxigraph:<path>", err=True)
|
|
27
|
-
raise typer.Exit(code=1)
|
|
28
|
-
|
|
29
|
-
if not to_backend.startswith("postgres:"):
|
|
30
|
-
typer.echo(f"Unsupported target backend: {to_backend!r}", err=True)
|
|
31
|
-
typer.echo("Currently supported target: postgres:<dsn>", err=True)
|
|
32
|
-
raise typer.Exit(code=1)
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
from aevum.store.postgres.migrate import migrate_from_oxigraph
|
|
36
|
-
except ImportError:
|
|
37
|
-
typer.echo("Error: aevum-store-postgres is not installed.", err=True)
|
|
38
|
-
raise typer.Exit(code=1) from None
|
|
39
|
-
|
|
40
|
-
oxigraph_path = from_backend[len("oxigraph:"):]
|
|
41
|
-
postgres_dsn = to_backend[len("postgres:"):]
|
|
42
|
-
|
|
43
|
-
typer.echo(f"Migrating: {oxigraph_path} -> PostgreSQL")
|
|
44
|
-
try:
|
|
45
|
-
import psycopg
|
|
46
|
-
conn = psycopg.connect(postgres_dsn)
|
|
47
|
-
migrated = migrate_from_oxigraph(oxigraph_path, conn) # type: ignore[arg-type] # Phase 2: wire store construction
|
|
48
|
-
typer.echo(f"Migration complete: {migrated} entities transferred.")
|
|
49
|
-
except Exception as e:
|
|
50
|
-
typer.echo(f"Migration failed: {e}", err=True)
|
|
51
|
-
raise typer.Exit(code=1) from e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|