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.
- {etch_record-0.7.0 → etch_record-0.9.0}/CHANGELOG.md +84 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/PKG-INFO +6 -2
- {etch_record-0.7.0 → etch_record-0.9.0}/README.md +5 -1
- {etch_record-0.7.0 → etch_record-0.9.0}/pyproject.toml +1 -1
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/__init__.py +1 -1
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/cli.py +390 -1
- etch_record-0.9.0/src/etch_record/cross_chain_reference_client.py +86 -0
- etch_record-0.9.0/src/etch_record/hsm_attestation_client.py +87 -0
- etch_record-0.9.0/src/etch_record/postmortem_client.py +80 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/.github/workflows/release.yml +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/.gitignore +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/LICENSE +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/attestation_client.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/authority_client.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/config.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/governance_client.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/import_client.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/mcp_client.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/rate_limit.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/stop_condition_client.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/src/etch_record/wave1_4567_clients.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_attestation_flags.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_authority_flags.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_cli.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_cli_hardening.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_governance_flags.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_import_flags.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_mcp_client.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_rate_limit.py +0 -0
- {etch_record-0.7.0 → etch_record-0.9.0}/tests/test_stop_condition_flags.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|