etch-record 0.7.0__tar.gz → 0.9.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 (31) hide show
  1. {etch_record-0.7.0 → etch_record-0.9.0}/CHANGELOG.md +84 -0
  2. {etch_record-0.7.0 → etch_record-0.9.0}/PKG-INFO +6 -2
  3. {etch_record-0.7.0 → etch_record-0.9.0}/README.md +5 -1
  4. {etch_record-0.7.0 → etch_record-0.9.0}/pyproject.toml +1 -1
  5. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/__init__.py +1 -1
  6. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/cli.py +390 -1
  7. etch_record-0.9.0/src/etch_record/cross_chain_reference_client.py +86 -0
  8. etch_record-0.9.0/src/etch_record/hsm_attestation_client.py +87 -0
  9. etch_record-0.9.0/src/etch_record/postmortem_client.py +80 -0
  10. {etch_record-0.7.0 → etch_record-0.9.0}/.github/workflows/release.yml +0 -0
  11. {etch_record-0.7.0 → etch_record-0.9.0}/.gitignore +0 -0
  12. {etch_record-0.7.0 → etch_record-0.9.0}/LICENSE +0 -0
  13. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/attestation_client.py +0 -0
  14. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/authority_client.py +0 -0
  15. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/config.py +0 -0
  16. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/governance_client.py +0 -0
  17. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/import_client.py +0 -0
  18. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/mcp_client.py +0 -0
  19. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/rate_limit.py +0 -0
  20. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/stop_condition_client.py +0 -0
  21. {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/wave1_4567_clients.py +0 -0
  22. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_attestation_flags.py +0 -0
  23. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_authority_flags.py +0 -0
  24. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_cli.py +0 -0
  25. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_cli_hardening.py +0 -0
  26. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_governance_flags.py +0 -0
  27. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_import_flags.py +0 -0
  28. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_mcp_client.py +0 -0
  29. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_rate_limit.py +0 -0
  30. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_stop_condition_flags.py +0 -0
  31. {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_wave1_4567_flags.py +0 -0
@@ -3,6 +3,90 @@
3
3
  All notable changes to `etch-record` (the CLI helper for the Etch signed
4
4
  audit chain).
5
5
 
6
+ ## v0.9.0 - 2026-08-02
7
+
8
+ Wave 5 batch: signed postmortem (#20), HSM/TPM/enclave attestation
9
+ (#17), and cross-chain reference (#18). Three new sub-record layers
10
+ in one release. All three follow the "if X is not None: ..." block
11
+ pattern so they compose independently with any prior Wave layer in
12
+ a single etch-record invocation.
13
+
14
+ Server-side dependency: etch commits shipping the three new
15
+ endpoints (`POST /v1/etch-chain/postmortem`,
16
+ `/hsm-attestation`, `/cross-chain-reference`).
17
+
18
+ ### Added
19
+
20
+ Wave 5 #20 postmortem:
21
+
22
+ - `--postmortem-about-event <oss-event-id>` - retroactive
23
+ postmortem target. Triggers POST /v1/etch-chain/postmortem.
24
+ - `--postmortem-finding <text>` - required with the above.
25
+ - `--postmortem-corrective-action <text>` - required with the above.
26
+ - `--postmortem-corrective-action-signed-by <id>` - optional signer id.
27
+ - `--postmortem-retro-confidence <0.0-1.0>` - optional retroactive
28
+ confidence downgrade.
29
+
30
+ Wave 5 #17 HSM/TPM/enclave attestation:
31
+
32
+ - `--hsm-vendor <id>` - tpm2, yubikey, aws-nitro, gcp-shielded,
33
+ azure-attestation, sev-snp, intel-tdx. Triggers POST
34
+ /v1/etch-chain/hsm-attestation.
35
+ - `--hsm-format <id>` - vendor-specific format id (tpm2-quote-v1,
36
+ yubico-attestation-cert, aws-nitro-cose-sign1, and others).
37
+ - `--hsm-blob-file <path>` - raw attestation blob, read as bytes and
38
+ base64-encoded client-side. Required with --hsm-vendor.
39
+ - `--hsm-key-ref <ref>` - optional stable pubkey reference.
40
+ - `--hsm-serial <serial>` - optional HSM/enclave serial.
41
+
42
+ Wave 5 #18 cross-chain reference (Etch-to-Etch federation):
43
+
44
+ - `--cross-chain-target-chain-id <sha256:...>` - derived chain id of
45
+ the target chain (compute_chain_id). Triggers POST
46
+ /v1/etch-chain/cross-chain-reference.
47
+ - `--cross-chain-target-epoch <int>` - epoch_seq on target chain.
48
+ - `--cross-chain-target-event-hash <sha256:...>` - row_hash of the
49
+ referenced target event.
50
+ - `--cross-chain-label <text>` - optional free-form label.
51
+
52
+ ### Exit codes added
53
+
54
+ - `14` postmortem sub-record failed
55
+ - `15` cross-chain-reference sub-record failed
56
+ - `16` hsm-attestation sub-record failed
57
+
58
+ ## v0.8.0 — 2026-08-02
59
+
60
+ Wave 4 #15 8-category drift engine. Extends the governance object
61
+ with 3 new optional fields (mission / terminology / context) so the
62
+ server can populate an 8-dimension projection index. Drift detection
63
+ runs offline via `etch-chain-verify` (added server-side).
64
+
65
+ Server-side dependency: etch commit adding the multi-dim projection
66
+ (this release cycle's server change).
67
+
68
+ ### Added
69
+
70
+ - `--mission <string>` — mission statement (e.g. "customer KYC
71
+ compliance decisions"). Hashed server-side.
72
+ - `--terminology <t1,t2,...>` — comma-separated key terms. Set-hashed
73
+ server-side.
74
+ - `--context-file <path>` — JSON object describing operating context.
75
+ Any shape accepted; canonical-json-hashed server-side.
76
+
77
+ Any of the new flags (alone or with legacy governance flags) triggers
78
+ the governance sub-record call and populates the multi-dim index.
79
+
80
+ ### Positioning
81
+
82
+ Wave 4 #15 is the largest single feature in the boundary plan (10
83
+ dev-days). Ships the full OHRP v2.0 8-category surface:
84
+ mission / assumption / confidence / evidence / scope / terminology /
85
+ authority / context. No observability vendor claims this surface.
86
+
87
+ Content angle for marketing: "8 drift dimensions with independent
88
+ comparators, per-decision on-chain evidence, offline verifier."
89
+
6
90
  ## v0.7.0 — 2026-08-02
7
91
 
8
92
  Wave 2 #10. Adds three flags for recording an explicit halt event
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etch-record
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: CLI helper: sign any event into your Etch audit chain from the command line.
5
5
  Author: Saravanan Jaichandaran
6
6
  License: MIT
@@ -17,7 +17,7 @@ Small CLI helper. Signs any event into your Etch audit chain from the command li
17
17
 
18
18
  Built for the marketing-agent workflow: every research call, draft generation, review decision, and outreach send emits a signed event. Also usable standalone for any local activity you want notarized.
19
19
 
20
- **Latest: v0.2.0** — Wave 1 #1 governance flags (`--policy-hash`, `--authority-file`, `--assumptions-file`, `--uncertainty`, `--uncertainty-file`, `--invalidation-file`) attach a signed governance sub-record on the Etch parallel chain. See [CHANGELOG.md](CHANGELOG.md).
20
+ **Latest: v0.9.0** — Wave 5 batch. Three new sub-record layers land in one release: signed postmortem (`--postmortem-about-event` + `--postmortem-finding` + `--postmortem-corrective-action`), HSM/TPM/enclave attestation (`--hsm-vendor` + `--hsm-format` + `--hsm-blob-file`), and cross-chain reference (`--cross-chain-target-chain-id` + `--cross-chain-target-epoch` + `--cross-chain-target-event-hash`). Twelve endpoints total now available on your Etch chain. See [CHANGELOG.md](CHANGELOG.md).
21
21
 
22
22
  ## Install
23
23
 
@@ -146,3 +146,7 @@ The base event was recorded via the OSS chain; the governance sub-record was sig
146
146
  - `3` unexpected exception
147
147
  - `4` rate limit (client-side sliding window)
148
148
  - `5` governance sub-record failed — the base event was recorded successfully; use the printed `event_id` to retry the governance call
149
+ - `13` stop-condition sub-record failed
150
+ - `14` postmortem sub-record failed
151
+ - `15` cross-chain-reference sub-record failed
152
+ - `16` hsm-attestation sub-record failed
@@ -4,7 +4,7 @@ Small CLI helper. Signs any event into your Etch audit chain from the command li
4
4
 
5
5
  Built for the marketing-agent workflow: every research call, draft generation, review decision, and outreach send emits a signed event. Also usable standalone for any local activity you want notarized.
6
6
 
7
- **Latest: v0.2.0** — Wave 1 #1 governance flags (`--policy-hash`, `--authority-file`, `--assumptions-file`, `--uncertainty`, `--uncertainty-file`, `--invalidation-file`) attach a signed governance sub-record on the Etch parallel chain. See [CHANGELOG.md](CHANGELOG.md).
7
+ **Latest: v0.9.0** — Wave 5 batch. Three new sub-record layers land in one release: signed postmortem (`--postmortem-about-event` + `--postmortem-finding` + `--postmortem-corrective-action`), HSM/TPM/enclave attestation (`--hsm-vendor` + `--hsm-format` + `--hsm-blob-file`), and cross-chain reference (`--cross-chain-target-chain-id` + `--cross-chain-target-epoch` + `--cross-chain-target-event-hash`). Twelve endpoints total now available on your Etch chain. See [CHANGELOG.md](CHANGELOG.md).
8
8
 
9
9
  ## Install
10
10
 
@@ -133,3 +133,7 @@ The base event was recorded via the OSS chain; the governance sub-record was sig
133
133
  - `3` unexpected exception
134
134
  - `4` rate limit (client-side sliding window)
135
135
  - `5` governance sub-record failed — the base event was recorded successfully; use the printed `event_id` to retry the governance call
136
+ - `13` stop-condition sub-record failed
137
+ - `14` postmortem sub-record failed
138
+ - `15` cross-chain-reference sub-record failed
139
+ - `16` hsm-attestation sub-record failed
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "etch-record"
7
- version = "0.7.0"
7
+ version = "0.9.0"
8
8
  description = "CLI helper: sign any event into your Etch audit chain from the command line."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """etch-record: CLI helper for signing arbitrary events into your Etch chain."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.9.0"
@@ -45,6 +45,18 @@ from .stop_condition_client import (
45
45
  StopConditionError,
46
46
  record_stop_condition,
47
47
  )
48
+ from .postmortem_client import (
49
+ PostmortemError,
50
+ record_postmortem,
51
+ )
52
+ from .hsm_attestation_client import (
53
+ HsmAttestationError,
54
+ record_hsm_attestation,
55
+ )
56
+ from .cross_chain_reference_client import (
57
+ CrossChainReferenceError,
58
+ record_cross_chain_reference,
59
+ )
48
60
  from .mcp_client import McpError, extract_event_id, record_event
49
61
  from .rate_limit import check_and_record
50
62
  from .wave1_4567_clients import (
@@ -424,6 +436,39 @@ def _maybe_warn_large_arg(name: str, value: str) -> None:
424
436
  ),
425
437
  )
426
438
  # ---------------------------------------------------------------------------
439
+ # Wave 4 #15 (2026-08-02) — 8-category governance schema extension.
440
+ # Adds mission / terminology / context fields. Extraction is server-
441
+ # side; drift detection runs via etch-chain-verify's dimensional
442
+ # check. Any of these flags being set (alone or with the legacy
443
+ # governance fields) triggers the second call and populates the
444
+ # multi-dim projection index.
445
+ # ---------------------------------------------------------------------------
446
+ @click.option(
447
+ "--mission", "mission", type=str, default=None,
448
+ help=(
449
+ "Wave 4 #15 governance: mission statement string (e.g. "
450
+ "'customer KYC compliance decisions'). Hashed server-side "
451
+ "for drift detection."
452
+ ),
453
+ )
454
+ @click.option(
455
+ "--terminology", "terminology_raw", type=str, default=None,
456
+ help=(
457
+ "Wave 4 #15 governance: comma-separated key terms (e.g. "
458
+ "'KYC,PII,customer identity'). Set-hashed server-side."
459
+ ),
460
+ )
461
+ @click.option(
462
+ "--context-file", "context_file",
463
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
464
+ default=None,
465
+ help=(
466
+ "Wave 4 #15 governance: JSON object describing the operating "
467
+ "context (e.g. {model, prompt_hash, policy_context}). Any "
468
+ "shape accepted; canonical-json-hashed server-side."
469
+ ),
470
+ )
471
+ # ---------------------------------------------------------------------------
427
472
  # Wave 1 #2 (2026-08-01) — bounded authority receipts.
428
473
  # When --authority-privkey-file + --authority-id + --receipt-type are all
429
474
  # set, the CLI adds a THIRD call:
@@ -703,6 +748,139 @@ def _maybe_warn_large_arg(name: str, value: str) -> None:
703
748
  "session-scoped vs global halts."
704
749
  ),
705
750
  )
751
+ # ---------------------------------------------------------------------------
752
+ # Wave 5 #20 (2026-08-02) — signed postmortem
753
+ # ---------------------------------------------------------------------------
754
+ @click.option(
755
+ "--postmortem-about-event", "postmortem_about_event",
756
+ type=str, default=None,
757
+ help=(
758
+ "Wave 5 #20: OSS event id the postmortem is retroactively "
759
+ "about. Triggers POST /v1/etch-chain/postmortem. Requires "
760
+ "--postmortem-finding + --postmortem-corrective-action."
761
+ ),
762
+ )
763
+ @click.option(
764
+ "--postmortem-finding", "postmortem_finding",
765
+ type=str, default=None,
766
+ help=(
767
+ "Wave 5 #20: free-text finding text. Required whenever "
768
+ "--postmortem-about-event is set."
769
+ ),
770
+ )
771
+ @click.option(
772
+ "--postmortem-corrective-action", "postmortem_corrective_action",
773
+ type=str, default=None,
774
+ help=(
775
+ "Wave 5 #20: corrective-action text. Required whenever "
776
+ "--postmortem-about-event is set."
777
+ ),
778
+ )
779
+ @click.option(
780
+ "--postmortem-corrective-action-signed-by",
781
+ "postmortem_corrective_action_signed_by",
782
+ type=str, default=None,
783
+ help=(
784
+ "Wave 5 #20: optional signer id for the corrective action."
785
+ ),
786
+ )
787
+ @click.option(
788
+ "--postmortem-retro-confidence", "postmortem_retro_confidence",
789
+ type=float, default=None,
790
+ help=(
791
+ "Wave 5 #20: optional retroactive confidence downgrade "
792
+ "(0.0-1.0). When set, indicates the original event's "
793
+ "confidence was, in hindsight, too high."
794
+ ),
795
+ )
796
+ # ---------------------------------------------------------------------------
797
+ # Wave 5 #17 (2026-08-02) — HSM / TPM / enclave attestation
798
+ # ---------------------------------------------------------------------------
799
+ @click.option(
800
+ "--hsm-vendor", "hsm_vendor",
801
+ type=str, default=None,
802
+ help=(
803
+ "Wave 5 #17: HSM/TPM vendor id (tpm2, yubikey, aws-nitro, "
804
+ "gcp-shielded, azure-attestation, sev-snp, intel-tdx). "
805
+ "Triggers POST /v1/etch-chain/hsm-attestation. Requires "
806
+ "--hsm-format + --hsm-blob-file."
807
+ ),
808
+ )
809
+ @click.option(
810
+ "--hsm-format", "hsm_format",
811
+ type=str, default=None,
812
+ help=(
813
+ "Wave 5 #17: attestation format id (tpm2-quote-v1, "
814
+ "yubico-attestation-cert, aws-nitro-cose-sign1, "
815
+ "gcp-shielded-vm-quote, azure-tdx-attestation, "
816
+ "sev-snp-report-v1, intel-tdx-quote-v4). Required "
817
+ "whenever --hsm-vendor is set."
818
+ ),
819
+ )
820
+ @click.option(
821
+ "--hsm-blob-file", "hsm_blob_file",
822
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
823
+ default=None,
824
+ help=(
825
+ "Wave 5 #17: path to the raw attestation blob. Read as "
826
+ "bytes and base64-encoded client-side before POST. Required "
827
+ "whenever --hsm-vendor is set."
828
+ ),
829
+ )
830
+ @click.option(
831
+ "--hsm-key-ref", "hsm_key_ref",
832
+ type=str, default=None,
833
+ help=(
834
+ "Wave 5 #17: optional stable key reference (typically "
835
+ "sha256:... of the pubkey the HSM signed with)."
836
+ ),
837
+ )
838
+ @click.option(
839
+ "--hsm-serial", "hsm_serial",
840
+ type=str, default=None,
841
+ help=(
842
+ "Wave 5 #17: optional HSM/enclave serial for the pubkey ledger."
843
+ ),
844
+ )
845
+ # ---------------------------------------------------------------------------
846
+ # Wave 5 #18 (2026-08-02) — cross-chain reference (federated evidence)
847
+ # ---------------------------------------------------------------------------
848
+ @click.option(
849
+ "--cross-chain-target-chain-id", "cross_chain_target_chain_id",
850
+ type=str, default=None,
851
+ help=(
852
+ "Wave 5 #18: sha256:... chain id of the OTHER chain being "
853
+ "referenced (produced by compute_chain_id). Triggers POST "
854
+ "/v1/etch-chain/cross-chain-reference. Requires --target-epoch "
855
+ "and --target-event-hash."
856
+ ),
857
+ )
858
+ @click.option(
859
+ "--cross-chain-target-epoch", "cross_chain_target_epoch",
860
+ type=int, default=None,
861
+ help=(
862
+ "Wave 5 #18: epoch_seq on the target chain that contains "
863
+ "the referenced event. Required whenever "
864
+ "--cross-chain-target-chain-id is set."
865
+ ),
866
+ )
867
+ @click.option(
868
+ "--cross-chain-target-event-hash", "cross_chain_target_event_hash",
869
+ type=str, default=None,
870
+ help=(
871
+ "Wave 5 #18: sha256:... row_hash of the specific event on "
872
+ "the target chain being referenced. Required whenever "
873
+ "--cross-chain-target-chain-id is set."
874
+ ),
875
+ )
876
+ @click.option(
877
+ "--cross-chain-label", "cross_chain_label",
878
+ type=str, default=None,
879
+ help=(
880
+ "Wave 5 #18: optional free-form label describing what the "
881
+ "referenced event is (e.g. 'supplier X kyc-approval')."
882
+ ),
883
+ )
706
884
  @click.version_option(__version__, prog_name="etch-record")
707
885
  def main(
708
886
  description: str,
@@ -725,6 +903,9 @@ def main(
725
903
  uncertainty: str | None,
726
904
  uncertainty_file: Path | None,
727
905
  invalidation_file: Path | None,
906
+ mission: str | None,
907
+ terminology_raw: str | None,
908
+ context_file: Path | None,
728
909
  authority_privkey_file: Path | None,
729
910
  authority_id: str | None,
730
911
  authority_scope: str,
@@ -752,6 +933,20 @@ def main(
752
933
  stop_condition_id: str | None,
753
934
  stop_condition_reason: str | None,
754
935
  stop_condition_scope: str | None,
936
+ postmortem_about_event: str | None,
937
+ postmortem_finding: str | None,
938
+ postmortem_corrective_action: str | None,
939
+ postmortem_corrective_action_signed_by: str | None,
940
+ postmortem_retro_confidence: float | None,
941
+ hsm_vendor: str | None,
942
+ hsm_format: str | None,
943
+ hsm_blob_file: Path | None,
944
+ hsm_key_ref: str | None,
945
+ hsm_serial: str | None,
946
+ cross_chain_target_chain_id: str | None,
947
+ cross_chain_target_epoch: int | None,
948
+ cross_chain_target_event_hash: str | None,
949
+ cross_chain_label: str | None,
755
950
  ) -> None:
756
951
  # Wave 2 #8 — bulk import mode. Runs the file through the format
757
952
  # adapter server-side; single-event flow is completely skipped
@@ -860,6 +1055,9 @@ def main(
860
1055
  uncertainty=uncertainty,
861
1056
  uncertainty_file=uncertainty_file,
862
1057
  invalidation_file=invalidation_file,
1058
+ mission=mission,
1059
+ terminology_raw=terminology_raw,
1060
+ context_file=context_file,
863
1061
  )
864
1062
  if governance is not None:
865
1063
  try:
@@ -1020,6 +1218,181 @@ def main(
1020
1218
  f"stop_condition_hash={r.stop_condition_hash}",
1021
1219
  )
1022
1220
 
1221
+ # Wave 5 #20 — signed postmortem. Independent of every prior
1222
+ # layer. Follows the "if X is not None: ..." pattern locked in
1223
+ # memory after the v0.4.0 short-circuit incident.
1224
+ postmortem_payload = _prepare_postmortem(
1225
+ postmortem_about_event,
1226
+ postmortem_finding,
1227
+ postmortem_corrective_action,
1228
+ postmortem_corrective_action_signed_by,
1229
+ postmortem_retro_confidence,
1230
+ )
1231
+ if postmortem_payload is not None:
1232
+ try:
1233
+ r = record_postmortem(cfg, event_id, postmortem_payload)
1234
+ except PostmortemError as exc:
1235
+ click.echo(f"etch-record: postmortem: {exc}", err=True)
1236
+ sys.exit(14)
1237
+ click.echo(
1238
+ f"OK postmortem_seq={r.etch_chain_seq} "
1239
+ f"postmortem_hash={r.postmortem_hash}",
1240
+ )
1241
+
1242
+ # Wave 5 #17 — HSM / TPM / enclave attestation. Independent.
1243
+ hsm_payload = _prepare_hsm_attestation(
1244
+ hsm_vendor, hsm_format, hsm_blob_file,
1245
+ hsm_key_ref, hsm_serial,
1246
+ )
1247
+ if hsm_payload is not None:
1248
+ try:
1249
+ r = record_hsm_attestation(cfg, event_id, hsm_payload)
1250
+ except HsmAttestationError as exc:
1251
+ click.echo(
1252
+ f"etch-record: hsm-attestation: {exc}", err=True,
1253
+ )
1254
+ sys.exit(16)
1255
+ click.echo(
1256
+ f"OK hsm_attestation_seq={r.etch_chain_seq} "
1257
+ f"hsm_attestation_hash={r.hsm_attestation_hash}",
1258
+ )
1259
+
1260
+ # Wave 5 #18 — cross-chain reference. Independent.
1261
+ cross_ref_payload = _prepare_cross_chain_reference(
1262
+ cross_chain_target_chain_id,
1263
+ cross_chain_target_epoch,
1264
+ cross_chain_target_event_hash,
1265
+ cross_chain_label,
1266
+ )
1267
+ if cross_ref_payload is not None:
1268
+ try:
1269
+ r = record_cross_chain_reference(
1270
+ cfg, event_id, cross_ref_payload,
1271
+ )
1272
+ except CrossChainReferenceError as exc:
1273
+ click.echo(
1274
+ f"etch-record: cross-chain-reference: {exc}",
1275
+ err=True,
1276
+ )
1277
+ sys.exit(15)
1278
+ click.echo(
1279
+ f"OK cross_chain_reference_seq={r.etch_chain_seq} "
1280
+ f"cross_ref_hash={r.cross_ref_hash} "
1281
+ f"target_chain_id={r.target_chain_id}",
1282
+ )
1283
+
1284
+
1285
+ def _prepare_postmortem(
1286
+ about_event: str | None,
1287
+ finding: str | None,
1288
+ corrective_action: str | None,
1289
+ corrective_action_signed_by: str | None,
1290
+ retro_confidence: float | None,
1291
+ ) -> dict | None:
1292
+ provided = any((
1293
+ about_event, finding, corrective_action,
1294
+ corrective_action_signed_by, retro_confidence is not None,
1295
+ ))
1296
+ if not provided:
1297
+ return None
1298
+ missing = []
1299
+ if about_event is None:
1300
+ missing.append("--postmortem-about-event")
1301
+ if finding is None:
1302
+ missing.append("--postmortem-finding")
1303
+ if corrective_action is None:
1304
+ missing.append("--postmortem-corrective-action")
1305
+ if missing:
1306
+ raise click.UsageError(
1307
+ "Wave 5 #20 postmortem requires: " + ", ".join(missing),
1308
+ )
1309
+ payload = {
1310
+ "about_event_id": about_event,
1311
+ "finding": finding,
1312
+ "corrective_action": corrective_action,
1313
+ "attested_at": utc_iso_ms_now(),
1314
+ }
1315
+ if corrective_action_signed_by is not None:
1316
+ payload["corrective_action_signed_by"] = (
1317
+ corrective_action_signed_by
1318
+ )
1319
+ if retro_confidence is not None:
1320
+ payload["retroactive_confidence_downgrade"] = retro_confidence
1321
+ return payload
1322
+
1323
+
1324
+ def _prepare_hsm_attestation(
1325
+ vendor: str | None,
1326
+ fmt: str | None,
1327
+ blob_file: Path | None,
1328
+ key_ref: str | None,
1329
+ serial: str | None,
1330
+ ) -> dict | None:
1331
+ import base64
1332
+ provided = any((vendor, fmt, blob_file, key_ref, serial))
1333
+ if not provided:
1334
+ return None
1335
+ missing = []
1336
+ if vendor is None:
1337
+ missing.append("--hsm-vendor")
1338
+ if fmt is None:
1339
+ missing.append("--hsm-format")
1340
+ if blob_file is None:
1341
+ missing.append("--hsm-blob-file")
1342
+ if missing:
1343
+ raise click.UsageError(
1344
+ "Wave 5 #17 hsm-attestation requires: "
1345
+ + ", ".join(missing),
1346
+ )
1347
+ with open(blob_file, "rb") as fh:
1348
+ blob_bytes = fh.read()
1349
+ payload = {
1350
+ "vendor": vendor,
1351
+ "attestation_format": fmt,
1352
+ "attestation_blob_b64": base64.b64encode(blob_bytes).decode(),
1353
+ "attested_at": utc_iso_ms_now(),
1354
+ }
1355
+ if key_ref is not None:
1356
+ payload["key_ref"] = key_ref
1357
+ if serial is not None:
1358
+ payload["hsm_serial"] = serial
1359
+ return payload
1360
+
1361
+
1362
+ def _prepare_cross_chain_reference(
1363
+ target_chain_id: str | None,
1364
+ target_epoch: int | None,
1365
+ target_event_hash: str | None,
1366
+ label: str | None,
1367
+ ) -> dict | None:
1368
+ provided = any((
1369
+ target_chain_id, target_epoch is not None,
1370
+ target_event_hash, label,
1371
+ ))
1372
+ if not provided:
1373
+ return None
1374
+ missing = []
1375
+ if target_chain_id is None:
1376
+ missing.append("--cross-chain-target-chain-id")
1377
+ if target_epoch is None:
1378
+ missing.append("--cross-chain-target-epoch")
1379
+ if target_event_hash is None:
1380
+ missing.append("--cross-chain-target-event-hash")
1381
+ if missing:
1382
+ raise click.UsageError(
1383
+ "Wave 5 #18 cross-chain-reference requires: "
1384
+ + ", ".join(missing),
1385
+ )
1386
+ payload = {
1387
+ "chain_id": target_chain_id,
1388
+ "epoch_seq": target_epoch,
1389
+ "event_hash": target_event_hash,
1390
+ "attested_at": utc_iso_ms_now(),
1391
+ }
1392
+ if label is not None:
1393
+ payload["label"] = label
1394
+ return payload
1395
+
1023
1396
 
1024
1397
  def _prepare_stop_condition(
1025
1398
  stop_condition_id: str | None,
@@ -1060,12 +1433,20 @@ def _assemble_governance_from_flags(
1060
1433
  uncertainty: str | None,
1061
1434
  uncertainty_file: Path | None,
1062
1435
  invalidation_file: Path | None,
1436
+ mission: str | None = None,
1437
+ terminology_raw: str | None = None,
1438
+ context_file: Path | None = None,
1063
1439
  ) -> dict | None:
1064
1440
  """Compose the governance object from CLI flags, or return None if
1065
- none of them are set (signals: skip the second API call)."""
1441
+ none of them are set (signals: skip the second API call).
1442
+
1443
+ Wave 4 #15 additions: `mission` string, `terminology` list,
1444
+ `context` dict. Any of these plus the legacy 5 fields triggers
1445
+ the second call."""
1066
1446
  if not any((
1067
1447
  policy_hash, authority_file, assumptions_file,
1068
1448
  uncertainty, uncertainty_file, invalidation_file,
1449
+ mission, terminology_raw, context_file,
1069
1450
  )):
1070
1451
  return None
1071
1452
 
@@ -1094,6 +1475,14 @@ def _assemble_governance_from_flags(
1094
1475
  gov["invalidation_conditions"] = _read_json_list(
1095
1476
  invalidation_file, "--invalidation-file",
1096
1477
  )
1478
+ if mission:
1479
+ gov["mission"] = mission
1480
+ if terminology_raw:
1481
+ gov["terminology"] = [
1482
+ t.strip() for t in terminology_raw.split(",") if t.strip()
1483
+ ]
1484
+ if context_file is not None:
1485
+ gov["context"] = _read_json_object(context_file, "--context-file")
1097
1486
  return gov
1098
1487
 
1099
1488
 
@@ -0,0 +1,86 @@
1
+ """HTTP client for Wave 5 #18 cross-chain reference endpoint
2
+ (2026-08-02)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ import httpx
10
+
11
+ from .config import Config
12
+
13
+
14
+ _TIMEOUT_S = 30.0
15
+
16
+
17
+ class CrossChainReferenceError(RuntimeError):
18
+ """Raised when the cross-chain-reference call fails.
19
+ CLI exit code 15."""
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CrossChainReferenceResult:
24
+ etch_chain_seq: int
25
+ etch_row_id: str
26
+ cross_ref_hash: str
27
+ target_chain_id: str
28
+ target_epoch_seq: int
29
+ target_event_hash: str
30
+ oss_event_id_ref: Optional[str]
31
+ recorded_at: str
32
+
33
+
34
+ def record_cross_chain_reference(
35
+ cfg: Config,
36
+ oss_event_id: str,
37
+ cross_ref: dict,
38
+ client: Optional[httpx.Client] = None,
39
+ ) -> CrossChainReferenceResult:
40
+ """POST /v1/etch-chain/cross-chain-reference."""
41
+ url = f"{cfg.base_url}/v1/etch-chain/cross-chain-reference"
42
+ body = {"oss_event_id": oss_event_id, "cross_ref": cross_ref}
43
+ headers = {
44
+ "Authorization": f"Bearer {cfg.app_token}",
45
+ "Content-Type": "application/json",
46
+ }
47
+
48
+ _client = client if client is not None else httpx.Client(
49
+ timeout=_TIMEOUT_S,
50
+ )
51
+ try:
52
+ try:
53
+ r = _client.post(url, headers=headers, json=body)
54
+ except httpx.HTTPError as exc:
55
+ raise CrossChainReferenceError(
56
+ f"transport failed: {exc}",
57
+ ) from exc
58
+ finally:
59
+ if client is None:
60
+ _client.close()
61
+
62
+ if r.status_code != 200:
63
+ try:
64
+ envelope = r.json()
65
+ except Exception:
66
+ envelope = {
67
+ "error": "non_json_response", "raw": r.text[:200],
68
+ }
69
+ err = envelope.get("error", "unknown")
70
+ detail = {k: v for k, v in envelope.items() if k != "error"}
71
+ raise CrossChainReferenceError(
72
+ f"{r.status_code} {err}"
73
+ + (f" — {detail}" if detail else ""),
74
+ )
75
+
76
+ data = r.json()
77
+ return CrossChainReferenceResult(
78
+ etch_chain_seq=data["etch_chain_seq"],
79
+ etch_row_id=data["etch_row_id"],
80
+ cross_ref_hash=data["cross_ref_hash"],
81
+ target_chain_id=data["target_chain_id"],
82
+ target_epoch_seq=data["target_epoch_seq"],
83
+ target_event_hash=data["target_event_hash"],
84
+ oss_event_id_ref=data.get("oss_event_id_ref"),
85
+ recorded_at=data["recorded_at"],
86
+ )
@@ -0,0 +1,87 @@
1
+ """HTTP client for Wave 5 #17 HSM/TPM/enclave attestation endpoint
2
+ (2026-08-02)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ import httpx
10
+
11
+ from .config import Config
12
+
13
+
14
+ _TIMEOUT_S = 30.0
15
+
16
+
17
+ class HsmAttestationError(RuntimeError):
18
+ """Raised when the hsm-attestation call fails.
19
+ CLI exit code 16."""
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class HsmAttestationResult:
24
+ etch_chain_seq: int
25
+ etch_row_id: str
26
+ hsm_attestation_hash: str
27
+ vendor: str
28
+ attestation_format: str
29
+ oss_event_id_ref: Optional[str]
30
+ recorded_at: str
31
+
32
+
33
+ def record_hsm_attestation(
34
+ cfg: Config,
35
+ oss_event_id: str,
36
+ hsm_attestation: dict,
37
+ client: Optional[httpx.Client] = None,
38
+ ) -> HsmAttestationResult:
39
+ """POST /v1/etch-chain/hsm-attestation."""
40
+ url = f"{cfg.base_url}/v1/etch-chain/hsm-attestation"
41
+ body = {
42
+ "oss_event_id": oss_event_id,
43
+ "hsm_attestation": hsm_attestation,
44
+ }
45
+ headers = {
46
+ "Authorization": f"Bearer {cfg.app_token}",
47
+ "Content-Type": "application/json",
48
+ }
49
+
50
+ _client = client if client is not None else httpx.Client(
51
+ timeout=_TIMEOUT_S,
52
+ )
53
+ try:
54
+ try:
55
+ r = _client.post(url, headers=headers, json=body)
56
+ except httpx.HTTPError as exc:
57
+ raise HsmAttestationError(
58
+ f"transport failed: {exc}",
59
+ ) from exc
60
+ finally:
61
+ if client is None:
62
+ _client.close()
63
+
64
+ if r.status_code != 200:
65
+ try:
66
+ envelope = r.json()
67
+ except Exception:
68
+ envelope = {
69
+ "error": "non_json_response", "raw": r.text[:200],
70
+ }
71
+ err = envelope.get("error", "unknown")
72
+ detail = {k: v for k, v in envelope.items() if k != "error"}
73
+ raise HsmAttestationError(
74
+ f"{r.status_code} {err}"
75
+ + (f" — {detail}" if detail else ""),
76
+ )
77
+
78
+ data = r.json()
79
+ return HsmAttestationResult(
80
+ etch_chain_seq=data["etch_chain_seq"],
81
+ etch_row_id=data["etch_row_id"],
82
+ hsm_attestation_hash=data["hsm_attestation_hash"],
83
+ vendor=data["vendor"],
84
+ attestation_format=data["attestation_format"],
85
+ oss_event_id_ref=data.get("oss_event_id_ref"),
86
+ recorded_at=data["recorded_at"],
87
+ )
@@ -0,0 +1,80 @@
1
+ """HTTP client for Wave 5 #20 signed postmortem endpoint (2026-08-02)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ import httpx
9
+
10
+ from .config import Config
11
+
12
+
13
+ _TIMEOUT_S = 30.0
14
+
15
+
16
+ class PostmortemError(RuntimeError):
17
+ """Raised when the postmortem call fails. CLI exit code 14."""
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class PostmortemResult:
22
+ etch_chain_seq: int
23
+ etch_row_id: str
24
+ postmortem_hash: str
25
+ about_event_id: str
26
+ oss_event_id_ref: Optional[str]
27
+ recorded_at: str
28
+
29
+
30
+ def record_postmortem(
31
+ cfg: Config,
32
+ oss_event_id: str,
33
+ postmortem: dict,
34
+ client: Optional[httpx.Client] = None,
35
+ ) -> PostmortemResult:
36
+ """POST /v1/etch-chain/postmortem."""
37
+ url = f"{cfg.base_url}/v1/etch-chain/postmortem"
38
+ body = {"oss_event_id": oss_event_id, "postmortem": postmortem}
39
+ headers = {
40
+ "Authorization": f"Bearer {cfg.app_token}",
41
+ "Content-Type": "application/json",
42
+ }
43
+
44
+ _client = client if client is not None else httpx.Client(
45
+ timeout=_TIMEOUT_S,
46
+ )
47
+ try:
48
+ try:
49
+ r = _client.post(url, headers=headers, json=body)
50
+ except httpx.HTTPError as exc:
51
+ raise PostmortemError(
52
+ f"transport failed: {exc}",
53
+ ) from exc
54
+ finally:
55
+ if client is None:
56
+ _client.close()
57
+
58
+ if r.status_code != 200:
59
+ try:
60
+ envelope = r.json()
61
+ except Exception:
62
+ envelope = {
63
+ "error": "non_json_response", "raw": r.text[:200],
64
+ }
65
+ err = envelope.get("error", "unknown")
66
+ detail = {k: v for k, v in envelope.items() if k != "error"}
67
+ raise PostmortemError(
68
+ f"{r.status_code} {err}"
69
+ + (f" — {detail}" if detail else ""),
70
+ )
71
+
72
+ data = r.json()
73
+ return PostmortemResult(
74
+ etch_chain_seq=data["etch_chain_seq"],
75
+ etch_row_id=data["etch_row_id"],
76
+ postmortem_hash=data["postmortem_hash"],
77
+ about_event_id=data["about_event_id"],
78
+ oss_event_id_ref=data.get("oss_event_id_ref"),
79
+ recorded_at=data["recorded_at"],
80
+ )
File without changes
File without changes