ledgix-python 0.1.6__tar.gz → 0.1.7__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.
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/PKG-INFO +2 -2
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/README.md +1 -1
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/pyproject.toml +1 -1
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/__init__.py +1 -1
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/client.py +21 -5
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/test_client.py +80 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/.gitignore +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/demo.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/requirements.txt +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/__init__.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/crewai.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/langchain.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/llamaindex.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/config.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/enforce.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/exceptions.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/models.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/__init__.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/conftest.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/test_adapters.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/test_enforce.py +0 -0
- {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ledgix-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Agent-agnostic compliance shim for SOX 404 policy enforcement via the ALCV Vault
|
|
5
5
|
Project-URL: Homepage, https://github.com/ledgix-dev/python-sdk
|
|
6
6
|
Project-URL: Documentation, https://docs.ledgix.dev
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
# Ledgix ALCV — Python SDK
|
|
42
42
|
|
|
43
|
-
[](https://pypi.org/project/ledgix-python/)
|
|
44
44
|
[](https://python.org)
|
|
45
45
|
[](LICENSE)
|
|
46
46
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ledgix ALCV — Python SDK
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/ledgix-python/)
|
|
4
4
|
[](https://python.org)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
@@ -612,11 +612,16 @@ class LedgixClient:
|
|
|
612
612
|
)
|
|
613
613
|
|
|
614
614
|
latest_leaf_hash: str | None = None
|
|
615
|
+
coverage_notes: list[str] = []
|
|
616
|
+
redacted_entry_count = 0
|
|
615
617
|
for entry in sorted_entries:
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
618
|
+
if self._has_protected_event_fields(entry):
|
|
619
|
+
expected_event_hash = self._build_event_hash(entry)
|
|
620
|
+
if expected_event_hash != entry.event_hash:
|
|
621
|
+
raise TokenVerificationError(f"Ledger event hash mismatch at seq {entry.seq}")
|
|
622
|
+
else:
|
|
623
|
+
redacted_entry_count += 1
|
|
624
|
+
expected_leaf_hash = self._hash_leaf(entry.event_hash)
|
|
620
625
|
if expected_leaf_hash != entry.leaf_hash:
|
|
621
626
|
raise TokenVerificationError(f"Ledger leaf hash mismatch at seq {entry.seq}")
|
|
622
627
|
if entry.receipt_algorithm != "Ed25519":
|
|
@@ -634,6 +639,12 @@ class LedgixClient:
|
|
|
634
639
|
payload_bytes,
|
|
635
640
|
)
|
|
636
641
|
latest_leaf_hash = entry.leaf_hash
|
|
642
|
+
if redacted_entry_count > 0:
|
|
643
|
+
coverage_notes.append(
|
|
644
|
+
"Event-body hash recomputation was skipped for "
|
|
645
|
+
f"{redacted_entry_count} redacted public ledger entr"
|
|
646
|
+
f"{'y' if redacted_entry_count == 1 else 'ies'}; receipt and checkpoint proofs still verified."
|
|
647
|
+
)
|
|
637
648
|
|
|
638
649
|
sorted_checkpoints = sorted(checkpoints, key=lambda item: item.checkpoint_id)
|
|
639
650
|
previous_checkpoint_hash = ""
|
|
@@ -682,10 +693,12 @@ class LedgixClient:
|
|
|
682
693
|
"Latest checkpoint root does not match sequenced leaf hashes"
|
|
683
694
|
)
|
|
684
695
|
else:
|
|
685
|
-
|
|
696
|
+
coverage_notes.append(
|
|
686
697
|
f"Provided {len(sequenced_entries)} sequenced entries for tree size "
|
|
687
698
|
f"{latest_checkpoint.tree_size}; full root verification requires the complete covered set."
|
|
688
699
|
)
|
|
700
|
+
if coverage_notes:
|
|
701
|
+
coverage_note = " ".join(coverage_notes)
|
|
689
702
|
return LedgerVerificationResult(
|
|
690
703
|
intact=True,
|
|
691
704
|
verified_entries=len(sorted_entries),
|
|
@@ -816,6 +829,9 @@ class LedgixClient:
|
|
|
816
829
|
)
|
|
817
830
|
return self._hash_event_payload(payload)
|
|
818
831
|
|
|
832
|
+
def _has_protected_event_fields(self, entry: LedgerEntry) -> bool:
|
|
833
|
+
return isinstance(entry.intent_hash, str) and len(entry.intent_hash) > 0
|
|
834
|
+
|
|
819
835
|
def _build_receipt_payload(self, entry: LedgerEntry) -> bytes:
|
|
820
836
|
return self._encode_deterministic_cbor(
|
|
821
837
|
{
|
|
@@ -579,6 +579,86 @@ class TestLedgerProofVerification:
|
|
|
579
579
|
assert result.verified_manifests == 1
|
|
580
580
|
assert result.latest_leaf_hash == entry["leaf_hash"]
|
|
581
581
|
|
|
582
|
+
@respx.mock
|
|
583
|
+
def test_verify_ledger_proof_with_redacted_public_entry(
|
|
584
|
+
self,
|
|
585
|
+
client: LedgixClient,
|
|
586
|
+
ed25519_private_key,
|
|
587
|
+
jwks_response: dict,
|
|
588
|
+
):
|
|
589
|
+
full_entry = {
|
|
590
|
+
"seq": 1,
|
|
591
|
+
"event_uuid": "evt-1",
|
|
592
|
+
"request_id": "req-1",
|
|
593
|
+
"agent_id": "agent-1",
|
|
594
|
+
"policy_id": "policy-1",
|
|
595
|
+
"intent_hash": "intent-1",
|
|
596
|
+
"tool_name": "stripe_refund",
|
|
597
|
+
"tool_args": {"amount": 45},
|
|
598
|
+
"reason": "ok",
|
|
599
|
+
"citations": [],
|
|
600
|
+
"evidence_chunks": [],
|
|
601
|
+
"confidence": 0.91,
|
|
602
|
+
"approved": True,
|
|
603
|
+
"accepted_at": "2026-03-15T12:00:00Z",
|
|
604
|
+
"canonical_version": 1,
|
|
605
|
+
"event_hash": "",
|
|
606
|
+
"leaf_hash": "",
|
|
607
|
+
"leaf_index": 0,
|
|
608
|
+
"checkpoint_id": 1,
|
|
609
|
+
"receipt_algorithm": "Ed25519",
|
|
610
|
+
"receipt_key_id": "test-key-001",
|
|
611
|
+
"receipt_signature": "",
|
|
612
|
+
"receipt_payload": "",
|
|
613
|
+
}
|
|
614
|
+
full_entry["event_hash"] = self._build_event_hash(full_entry)
|
|
615
|
+
full_entry["leaf_hash"] = self._hash_leaf(full_entry["event_hash"])
|
|
616
|
+
receipt_payload = self._build_receipt_payload(full_entry)
|
|
617
|
+
full_entry["receipt_payload"] = self._b64url(receipt_payload)
|
|
618
|
+
full_entry["receipt_signature"] = self._b64url(ed25519_private_key.sign(receipt_payload))
|
|
619
|
+
|
|
620
|
+
public_entry = {
|
|
621
|
+
**full_entry,
|
|
622
|
+
"intent_hash": "",
|
|
623
|
+
"tool_args": {},
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
checkpoint = {
|
|
627
|
+
"checkpoint_id": 1,
|
|
628
|
+
"microblock_id": 1,
|
|
629
|
+
"tree_size": 1,
|
|
630
|
+
"root_hash": full_entry["leaf_hash"],
|
|
631
|
+
"checkpoint_hash": "",
|
|
632
|
+
"prev_checkpoint_hash": "",
|
|
633
|
+
"signature_algorithm": "Ed25519",
|
|
634
|
+
"signer_key_id": "test-key-001",
|
|
635
|
+
"checkpoint_signature": "",
|
|
636
|
+
"checkpoint_payload": "",
|
|
637
|
+
"signed_at": "2026-03-15T13:00:00Z",
|
|
638
|
+
"mmd_seconds": 30,
|
|
639
|
+
"export_target": "",
|
|
640
|
+
"export_uri": "",
|
|
641
|
+
"export_status": "",
|
|
642
|
+
}
|
|
643
|
+
checkpoint_payload = self._build_checkpoint_payload(checkpoint)
|
|
644
|
+
checkpoint["checkpoint_hash"] = self._hash_checkpoint_payload(checkpoint_payload)
|
|
645
|
+
checkpoint["checkpoint_payload"] = self._b64url(checkpoint_payload)
|
|
646
|
+
checkpoint["checkpoint_signature"] = self._b64url(ed25519_private_key.sign(checkpoint_payload))
|
|
647
|
+
|
|
648
|
+
respx.get("https://vault.test/.well-known/jwks.json").mock(
|
|
649
|
+
return_value=Response(200, json=jwks_response)
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
result = client.verify_ledger_proof(
|
|
653
|
+
entries=[public_entry],
|
|
654
|
+
manifests=[checkpoint],
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
assert result.intact is True
|
|
658
|
+
assert result.verified_entries == 1
|
|
659
|
+
assert result.coverage_note is not None
|
|
660
|
+
assert "redacted public ledger entry" in result.coverage_note
|
|
661
|
+
|
|
582
662
|
@respx.mock
|
|
583
663
|
@pytest.mark.asyncio
|
|
584
664
|
async def test_verify_ledger_proof_async(
|
|
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
|