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.
Files changed (22) hide show
  1. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/PKG-INFO +2 -2
  2. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/README.md +1 -1
  3. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/pyproject.toml +1 -1
  4. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/__init__.py +1 -1
  5. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/client.py +21 -5
  6. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/test_client.py +80 -0
  7. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/.gitignore +0 -0
  8. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/demo.py +0 -0
  9. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/requirements.txt +0 -0
  10. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/__init__.py +0 -0
  11. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/crewai.py +0 -0
  12. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/langchain.py +0 -0
  13. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/adapters/llamaindex.py +0 -0
  14. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/config.py +0 -0
  15. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/enforce.py +0 -0
  16. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/exceptions.py +0 -0
  17. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/src/ledgix_python/models.py +0 -0
  18. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/__init__.py +0 -0
  19. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/conftest.py +0 -0
  20. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/test_adapters.py +0 -0
  21. {ledgix_python-0.1.6 → ledgix_python-0.1.7}/tests/test_enforce.py +0 -0
  22. {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.6
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
- [![PyPI](https://img.shields.io/badge/pypi-v0.1.6-blue)](https://pypi.org/project/ledgix-python/)
43
+ [![PyPI](https://img.shields.io/badge/pypi-v0.1.7-blue)](https://pypi.org/project/ledgix-python/)
44
44
  [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
45
45
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
46
46
 
@@ -1,6 +1,6 @@
1
1
  # Ledgix ALCV — Python SDK
2
2
 
3
- [![PyPI](https://img.shields.io/badge/pypi-v0.1.6-blue)](https://pypi.org/project/ledgix-python/)
3
+ [![PyPI](https://img.shields.io/badge/pypi-v0.1.7-blue)](https://pypi.org/project/ledgix-python/)
4
4
  [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
6
6
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ledgix-python"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "Agent-agnostic compliance shim for SOX 404 policy enforcement via the ALCV Vault"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -39,7 +39,7 @@ from .models import (
39
39
  PolicyRegistrationResponse,
40
40
  )
41
41
 
42
- __version__ = "0.1.6"
42
+ __version__ = "0.1.7"
43
43
 
44
44
  __all__ = [
45
45
  # Core
@@ -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
- expected_event_hash = self._build_event_hash(entry)
617
- if expected_event_hash != entry.event_hash:
618
- raise TokenVerificationError(f"Ledger event hash mismatch at seq {entry.seq}")
619
- expected_leaf_hash = self._hash_leaf(expected_event_hash)
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
- coverage_note = (
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