ledgix-python 0.1.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.
- ledgix_python-0.1.0/.gitignore +33 -0
- ledgix_python-0.1.0/PKG-INFO +178 -0
- ledgix_python-0.1.0/README.md +138 -0
- ledgix_python-0.1.0/demo.py +184 -0
- ledgix_python-0.1.0/pyproject.toml +63 -0
- ledgix_python-0.1.0/requirements.txt +17 -0
- ledgix_python-0.1.0/src/ledgix_python/__init__.py +53 -0
- ledgix_python-0.1.0/src/ledgix_python/adapters/__init__.py +1 -0
- ledgix_python-0.1.0/src/ledgix_python/adapters/crewai.py +95 -0
- ledgix_python-0.1.0/src/ledgix_python/adapters/langchain.py +156 -0
- ledgix_python-0.1.0/src/ledgix_python/adapters/llamaindex.py +85 -0
- ledgix_python-0.1.0/src/ledgix_python/client.py +314 -0
- ledgix_python-0.1.0/src/ledgix_python/config.py +39 -0
- ledgix_python-0.1.0/src/ledgix_python/enforce.py +164 -0
- ledgix_python-0.1.0/src/ledgix_python/exceptions.py +44 -0
- ledgix_python-0.1.0/src/ledgix_python/models.py +56 -0
- ledgix_python-0.1.0/tests/__init__.py +0 -0
- ledgix_python-0.1.0/tests/conftest.py +145 -0
- ledgix_python-0.1.0/tests/test_adapters.py +260 -0
- ledgix_python-0.1.0/tests/test_client.py +309 -0
- ledgix_python-0.1.0/tests/test_enforce.py +212 -0
- ledgix_python-0.1.0/tests/test_models.py +116 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Virtual environments
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
env/
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
*.egg-info/
|
|
11
|
+
*.egg
|
|
12
|
+
dist/
|
|
13
|
+
build/
|
|
14
|
+
*.whl
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.vscode/
|
|
18
|
+
.idea/
|
|
19
|
+
*.swp
|
|
20
|
+
*.swo
|
|
21
|
+
|
|
22
|
+
# OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
25
|
+
|
|
26
|
+
# Testing
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.coverage
|
|
29
|
+
htmlcov/
|
|
30
|
+
|
|
31
|
+
# Env files
|
|
32
|
+
.env
|
|
33
|
+
.env.local
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ledgix-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-agnostic compliance shim for SOX 404 policy enforcement via the ALCV Vault
|
|
5
|
+
Project-URL: Homepage, https://github.com/ledgix-dev/python-sdk
|
|
6
|
+
Project-URL: Documentation, https://docs.ledgix.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/ledgix-dev/python-sdk
|
|
8
|
+
Author-email: Ledgix <team@ledgix.dev>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,ai,compliance,security,sox404,vault
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: pyjwt[crypto]>=2.8.0
|
|
27
|
+
Provides-Extra: crewai
|
|
28
|
+
Requires-Dist: crewai>=0.1.0; extra == 'crewai'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
35
|
+
Provides-Extra: langchain
|
|
36
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
37
|
+
Provides-Extra: llamaindex
|
|
38
|
+
Requires-Dist: llama-index-core>=0.10.0; extra == 'llamaindex'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# Ledgix ALCV — Python SDK
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/ledgix-python/)
|
|
44
|
+
[](https://python.org)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
|
|
47
|
+
Agent-agnostic compliance shim for SOX 404 policy enforcement. Intercepts AI agent tool calls, validates them against your policies via the ALCV Vault, and ensures only approved actions receive a cryptographically signed A-JWT (Agentic JSON Web Token).
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install ledgix-python
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from ledgix_python import LedgixClient, vault_enforce
|
|
57
|
+
|
|
58
|
+
client = LedgixClient() # Reads LEDGIX_VAULT_URL, LEDGIX_VAULT_API_KEY from env
|
|
59
|
+
|
|
60
|
+
@vault_enforce(client, tool_name="stripe_refund")
|
|
61
|
+
def process_refund(amount: float, reason: str, **kwargs):
|
|
62
|
+
token = kwargs["_clearance"].token # Signed A-JWT
|
|
63
|
+
return stripe.refund(amount=amount, metadata={"vault_token": token})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**That's it.** Three lines to SOX 404-compliant tool calls.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
Set environment variables (prefix: `LEDGIX_`):
|
|
71
|
+
|
|
72
|
+
| Variable | Default | Description |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `LEDGIX_VAULT_URL` | `http://localhost:8000` | Vault server URL |
|
|
75
|
+
| `LEDGIX_VAULT_API_KEY` | `""` | API key for Vault auth |
|
|
76
|
+
| `LEDGIX_VAULT_TIMEOUT` | `30.0` | Request timeout (seconds) |
|
|
77
|
+
| `LEDGIX_VERIFY_JWT` | `true` | Verify A-JWT signatures |
|
|
78
|
+
| `LEDGIX_AGENT_ID` | `default-agent` | Agent identifier |
|
|
79
|
+
|
|
80
|
+
Or pass a `VaultConfig` directly:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from ledgix_python import LedgixClient, VaultConfig
|
|
84
|
+
|
|
85
|
+
config = VaultConfig(vault_url="https://vault.mycompany.com", vault_api_key="sk-...")
|
|
86
|
+
client = LedgixClient(config=config)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Framework Adapters
|
|
90
|
+
|
|
91
|
+
### LangChain
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install ledgix-python[langchain]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from ledgix_python.adapters.langchain import LedgixCallbackHandler, LedgixTool
|
|
99
|
+
|
|
100
|
+
# Option 1: Callback handler (intercepts ALL tool calls)
|
|
101
|
+
handler = LedgixCallbackHandler(client)
|
|
102
|
+
agent = create_agent(callbacks=[handler])
|
|
103
|
+
|
|
104
|
+
# Option 2: Wrap individual tools
|
|
105
|
+
guarded_tool = LedgixTool.wrap(client, my_tool, policy_id="refund-policy")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### LlamaIndex
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install ledgix-python[llamaindex]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from ledgix_python.adapters.llamaindex import wrap_tool
|
|
116
|
+
|
|
117
|
+
guarded_tool = wrap_tool(client, my_function_tool, policy_id="refund-policy")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### CrewAI
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install ledgix-python[crewai]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from ledgix_python.adapters.crewai import LedgixCrewAITool
|
|
128
|
+
|
|
129
|
+
guarded_tool = LedgixCrewAITool.wrap(client, my_tool, policy_id="refund-policy")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Context Manager
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from ledgix_python import VaultContext
|
|
136
|
+
|
|
137
|
+
with VaultContext(client, "stripe_refund", {"amount": 45}) as ctx:
|
|
138
|
+
print(ctx.clearance.token) # Use the A-JWT
|
|
139
|
+
|
|
140
|
+
# Async
|
|
141
|
+
async with VaultContext(client, "stripe_refund", {"amount": 45}) as ctx:
|
|
142
|
+
print(ctx.clearance.token)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Error Handling
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from ledgix_python import ClearanceDeniedError, VaultConnectionError, TokenVerificationError
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
result = process_refund(amount=5000, reason="...")
|
|
152
|
+
except ClearanceDeniedError as e:
|
|
153
|
+
print(f"Blocked: {e.reason} (request: {e.request_id})")
|
|
154
|
+
except VaultConnectionError:
|
|
155
|
+
print("Cannot reach Vault — fail-closed")
|
|
156
|
+
except TokenVerificationError:
|
|
157
|
+
print("A-JWT signature invalid")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git clone https://github.com/ledgix-dev/python-sdk.git
|
|
164
|
+
cd python-sdk
|
|
165
|
+
python -m venv .venv && source .venv/bin/activate
|
|
166
|
+
pip install -e ".[dev]"
|
|
167
|
+
pytest tests/ -v --cov
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Demo
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python demo.py
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Ledgix ALCV — Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ledgix-python/)
|
|
4
|
+
[](https://python.org)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Agent-agnostic compliance shim for SOX 404 policy enforcement. Intercepts AI agent tool calls, validates them against your policies via the ALCV Vault, and ensures only approved actions receive a cryptographically signed A-JWT (Agentic JSON Web Token).
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ledgix-python
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from ledgix_python import LedgixClient, vault_enforce
|
|
17
|
+
|
|
18
|
+
client = LedgixClient() # Reads LEDGIX_VAULT_URL, LEDGIX_VAULT_API_KEY from env
|
|
19
|
+
|
|
20
|
+
@vault_enforce(client, tool_name="stripe_refund")
|
|
21
|
+
def process_refund(amount: float, reason: str, **kwargs):
|
|
22
|
+
token = kwargs["_clearance"].token # Signed A-JWT
|
|
23
|
+
return stripe.refund(amount=amount, metadata={"vault_token": token})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**That's it.** Three lines to SOX 404-compliant tool calls.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Set environment variables (prefix: `LEDGIX_`):
|
|
31
|
+
|
|
32
|
+
| Variable | Default | Description |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `LEDGIX_VAULT_URL` | `http://localhost:8000` | Vault server URL |
|
|
35
|
+
| `LEDGIX_VAULT_API_KEY` | `""` | API key for Vault auth |
|
|
36
|
+
| `LEDGIX_VAULT_TIMEOUT` | `30.0` | Request timeout (seconds) |
|
|
37
|
+
| `LEDGIX_VERIFY_JWT` | `true` | Verify A-JWT signatures |
|
|
38
|
+
| `LEDGIX_AGENT_ID` | `default-agent` | Agent identifier |
|
|
39
|
+
|
|
40
|
+
Or pass a `VaultConfig` directly:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from ledgix_python import LedgixClient, VaultConfig
|
|
44
|
+
|
|
45
|
+
config = VaultConfig(vault_url="https://vault.mycompany.com", vault_api_key="sk-...")
|
|
46
|
+
client = LedgixClient(config=config)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Framework Adapters
|
|
50
|
+
|
|
51
|
+
### LangChain
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install ledgix-python[langchain]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from ledgix_python.adapters.langchain import LedgixCallbackHandler, LedgixTool
|
|
59
|
+
|
|
60
|
+
# Option 1: Callback handler (intercepts ALL tool calls)
|
|
61
|
+
handler = LedgixCallbackHandler(client)
|
|
62
|
+
agent = create_agent(callbacks=[handler])
|
|
63
|
+
|
|
64
|
+
# Option 2: Wrap individual tools
|
|
65
|
+
guarded_tool = LedgixTool.wrap(client, my_tool, policy_id="refund-policy")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### LlamaIndex
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install ledgix-python[llamaindex]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from ledgix_python.adapters.llamaindex import wrap_tool
|
|
76
|
+
|
|
77
|
+
guarded_tool = wrap_tool(client, my_function_tool, policy_id="refund-policy")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### CrewAI
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install ledgix-python[crewai]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from ledgix_python.adapters.crewai import LedgixCrewAITool
|
|
88
|
+
|
|
89
|
+
guarded_tool = LedgixCrewAITool.wrap(client, my_tool, policy_id="refund-policy")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Context Manager
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from ledgix_python import VaultContext
|
|
96
|
+
|
|
97
|
+
with VaultContext(client, "stripe_refund", {"amount": 45}) as ctx:
|
|
98
|
+
print(ctx.clearance.token) # Use the A-JWT
|
|
99
|
+
|
|
100
|
+
# Async
|
|
101
|
+
async with VaultContext(client, "stripe_refund", {"amount": 45}) as ctx:
|
|
102
|
+
print(ctx.clearance.token)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Error Handling
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from ledgix_python import ClearanceDeniedError, VaultConnectionError, TokenVerificationError
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
result = process_refund(amount=5000, reason="...")
|
|
112
|
+
except ClearanceDeniedError as e:
|
|
113
|
+
print(f"Blocked: {e.reason} (request: {e.request_id})")
|
|
114
|
+
except VaultConnectionError:
|
|
115
|
+
print("Cannot reach Vault — fail-closed")
|
|
116
|
+
except TokenVerificationError:
|
|
117
|
+
print("A-JWT signature invalid")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
git clone https://github.com/ledgix-dev/python-sdk.git
|
|
124
|
+
cd python-sdk
|
|
125
|
+
python -m venv .venv && source .venv/bin/activate
|
|
126
|
+
pip install -e ".[dev]"
|
|
127
|
+
pytest tests/ -v --cov
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Demo
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python demo.py
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Ledgix ALCV SDK — Demo Script
|
|
3
|
+
|
|
4
|
+
Simulates the "Good Agent" vs "Rogue Agent" scenario from the
|
|
5
|
+
ALCV Vault technical specification.
|
|
6
|
+
|
|
7
|
+
This demo runs without a real Vault server by using a lightweight
|
|
8
|
+
mock. Set LEDGIX_VAULT_URL to point to a real Vault to test live.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python demo.py
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
|
|
21
|
+
import jwt
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
23
|
+
|
|
24
|
+
from ledgix_python import (
|
|
25
|
+
ClearanceDeniedError,
|
|
26
|
+
LedgixClient,
|
|
27
|
+
VaultConfig,
|
|
28
|
+
vault_enforce,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
32
|
+
# Mock Vault (only used when no live Vault is configured)
|
|
33
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
# Generate a demo Ed25519 key pair for A-JWT signing
|
|
36
|
+
_DEMO_PRIVATE_KEY = Ed25519PrivateKey.generate()
|
|
37
|
+
_DEMO_PUBLIC_KEY = _DEMO_PRIVATE_KEY.public_key()
|
|
38
|
+
|
|
39
|
+
REFUND_POLICY = {
|
|
40
|
+
"policy_id": "refund-policy-001",
|
|
41
|
+
"description": "Customer refund policy for shipping delays",
|
|
42
|
+
"rules": [
|
|
43
|
+
"Refunds are allowed up to $100 for shipping delays",
|
|
44
|
+
"Refund recipient must be the original customer",
|
|
45
|
+
"Agent must provide a valid order ID",
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _mock_clearance_decision(tool_name: str, tool_args: dict) -> dict:
|
|
51
|
+
"""Simulates the Vault's policy judge decision."""
|
|
52
|
+
amount = tool_args.get("amount", 0)
|
|
53
|
+
recipient = tool_args.get("recipient", "customer")
|
|
54
|
+
|
|
55
|
+
approved = amount <= 100 and recipient != "agent_personal_account"
|
|
56
|
+
|
|
57
|
+
if approved:
|
|
58
|
+
# Sign a demo A-JWT
|
|
59
|
+
payload = {
|
|
60
|
+
"sub": "clearance",
|
|
61
|
+
"tool": tool_name,
|
|
62
|
+
"amount": amount,
|
|
63
|
+
"iat": datetime.now(timezone.utc),
|
|
64
|
+
"exp": datetime.now(timezone.utc) + timedelta(minutes=5),
|
|
65
|
+
"request_id": f"demo-{int(time.time())}",
|
|
66
|
+
}
|
|
67
|
+
token = jwt.encode(payload, _DEMO_PRIVATE_KEY, algorithm="EdDSA")
|
|
68
|
+
return {
|
|
69
|
+
"approved": True,
|
|
70
|
+
"token": token,
|
|
71
|
+
"reason": "Policy check passed — refund within limits",
|
|
72
|
+
"request_id": payload["request_id"],
|
|
73
|
+
}
|
|
74
|
+
else:
|
|
75
|
+
reason = []
|
|
76
|
+
if amount > 100:
|
|
77
|
+
reason.append(f"Amount ${amount} exceeds $100 limit")
|
|
78
|
+
if recipient == "agent_personal_account":
|
|
79
|
+
reason.append("Recipient is not the original customer")
|
|
80
|
+
return {
|
|
81
|
+
"approved": False,
|
|
82
|
+
"token": None,
|
|
83
|
+
"reason": "; ".join(reason),
|
|
84
|
+
"request_id": f"demo-{int(time.time())}",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
89
|
+
# Simulated Stripe Tool
|
|
90
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def stripe_refund(amount: float, reason: str, order_id: str, recipient: str = "customer", **kwargs) -> str:
|
|
94
|
+
"""Simulates a Stripe refund API call."""
|
|
95
|
+
clearance = kwargs.get("_clearance")
|
|
96
|
+
token_preview = clearance.token[:40] + "..." if clearance and clearance.token else "N/A"
|
|
97
|
+
return (
|
|
98
|
+
f"✅ REFUND PROCESSED\n"
|
|
99
|
+
f" Amount: ${amount:.2f}\n"
|
|
100
|
+
f" Reason: {reason}\n"
|
|
101
|
+
f" Order: {order_id}\n"
|
|
102
|
+
f" A-JWT: {token_preview}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
107
|
+
# Demo Runner
|
|
108
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run_demo():
|
|
112
|
+
"""Run the Good Agent / Rogue Agent demo."""
|
|
113
|
+
|
|
114
|
+
print("=" * 64)
|
|
115
|
+
print(" LEDGIX ALCV — SDK Demo")
|
|
116
|
+
print(" Policy: \"Refunds ≤ $100 for shipping delays only\"")
|
|
117
|
+
print("=" * 64)
|
|
118
|
+
|
|
119
|
+
# Create a client with JWT verification disabled for demo
|
|
120
|
+
# (the mock doesn't serve a real JWKS endpoint)
|
|
121
|
+
config = VaultConfig(
|
|
122
|
+
vault_url="http://localhost:9999", # Won't be called in demo mode
|
|
123
|
+
verify_jwt=False,
|
|
124
|
+
agent_id="demo-agent",
|
|
125
|
+
)
|
|
126
|
+
client = LedgixClient(config=config)
|
|
127
|
+
|
|
128
|
+
# ── Scenario A: Good Agent ─────────────────────────────────────
|
|
129
|
+
print("\n" + "─" * 64)
|
|
130
|
+
print(" SCENARIO A: Good Agent")
|
|
131
|
+
print(" Agent requests $45 refund for a late package")
|
|
132
|
+
print("─" * 64 + "\n")
|
|
133
|
+
|
|
134
|
+
tool_args_good = {
|
|
135
|
+
"amount": 45.00,
|
|
136
|
+
"reason": "Package arrived 5 days late",
|
|
137
|
+
"order_id": "ORD-2026-1234",
|
|
138
|
+
"recipient": "customer",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
decision = _mock_clearance_decision("stripe_refund", tool_args_good)
|
|
142
|
+
print(f" Vault decision: {'✅ APPROVED' if decision['approved'] else '❌ DENIED'}")
|
|
143
|
+
print(f" Reason: {decision['reason']}")
|
|
144
|
+
|
|
145
|
+
if decision["approved"]:
|
|
146
|
+
# Simulate what the decorator would do
|
|
147
|
+
from ledgix_python.models import ClearanceResponse
|
|
148
|
+
|
|
149
|
+
clearance = ClearanceResponse(**decision)
|
|
150
|
+
result = stripe_refund(**tool_args_good, _clearance=clearance)
|
|
151
|
+
print(f"\n{result}")
|
|
152
|
+
|
|
153
|
+
# ── Scenario B: Rogue Agent ────────────────────────────────────
|
|
154
|
+
print("\n" + "─" * 64)
|
|
155
|
+
print(" SCENARIO B: Rogue Agent")
|
|
156
|
+
print(" Agent (prompt-injected) tries $5,000 refund to own account")
|
|
157
|
+
print("─" * 64 + "\n")
|
|
158
|
+
|
|
159
|
+
tool_args_rogue = {
|
|
160
|
+
"amount": 5000.00,
|
|
161
|
+
"reason": "Customer requested full refund",
|
|
162
|
+
"order_id": "ORD-2026-9999",
|
|
163
|
+
"recipient": "agent_personal_account",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
decision = _mock_clearance_decision("stripe_refund", tool_args_rogue)
|
|
167
|
+
print(f" Vault decision: {'✅ APPROVED' if decision['approved'] else '❌ DENIED'}")
|
|
168
|
+
print(f" Reason: {decision['reason']}")
|
|
169
|
+
|
|
170
|
+
if not decision["approved"]:
|
|
171
|
+
print("\n 🛡️ Tool call BLOCKED — no A-JWT issued")
|
|
172
|
+
print(" The Stripe tool would refuse execution without a valid token.")
|
|
173
|
+
|
|
174
|
+
# ── Summary ────────────────────────────────────────────────────
|
|
175
|
+
print("\n" + "=" * 64)
|
|
176
|
+
print(" Demo complete!")
|
|
177
|
+
print(" The ALCV Vault SDK intercepted both tool calls.")
|
|
178
|
+
print(" • Good agent: Approved and received a signed A-JWT")
|
|
179
|
+
print(" • Rogue agent: Denied — policy violation detected")
|
|
180
|
+
print("=" * 64)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
run_demo()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ledgix-python"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agent-agnostic compliance shim for SOX 404 policy enforcement via the ALCV Vault"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Ledgix", email = "team@ledgix.dev" }]
|
|
13
|
+
keywords = ["ai", "agents", "compliance", "sox404", "vault", "security"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.25.0",
|
|
29
|
+
"pydantic>=2.0.0",
|
|
30
|
+
"pydantic-settings>=2.0.0",
|
|
31
|
+
"PyJWT[crypto]>=2.8.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
langchain = ["langchain-core>=0.1.0"]
|
|
36
|
+
llamaindex = ["llama-index-core>=0.10.0"]
|
|
37
|
+
crewai = ["crewai>=0.1.0"]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=7.0.0",
|
|
40
|
+
"pytest-asyncio>=0.23.0",
|
|
41
|
+
"pytest-cov>=4.0.0",
|
|
42
|
+
"respx>=0.21.0",
|
|
43
|
+
"build>=1.0.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/ledgix-dev/python-sdk"
|
|
48
|
+
Documentation = "https://docs.ledgix.dev"
|
|
49
|
+
Repository = "https://github.com/ledgix-dev/python-sdk"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/ledgix_python"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
|
|
58
|
+
[tool.coverage.run]
|
|
59
|
+
source = ["ledgix_python"]
|
|
60
|
+
|
|
61
|
+
[tool.coverage.report]
|
|
62
|
+
show_missing = true
|
|
63
|
+
fail_under = 80
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
httpx>=0.25.0
|
|
2
|
+
pydantic>=2.0.0
|
|
3
|
+
pydantic-settings>=2.0.0
|
|
4
|
+
PyJWT[crypto]>=2.8.0
|
|
5
|
+
|
|
6
|
+
# Optional Adapters
|
|
7
|
+
# langchain-core>=0.1.0
|
|
8
|
+
# llama-index-core>=0.10.0
|
|
9
|
+
# crewai>=0.1.0
|
|
10
|
+
|
|
11
|
+
# Build and Dev Tools
|
|
12
|
+
build>=1.0.0
|
|
13
|
+
twine>=5.0.0
|
|
14
|
+
pytest>=7.0.0
|
|
15
|
+
pytest-asyncio>=0.23.0
|
|
16
|
+
pytest-cov>=4.0.0
|
|
17
|
+
respx>=0.21.0
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Ledgix ALCV — Python SDK
|
|
2
|
+
# Agent-agnostic compliance shim for SOX 404 policy enforcement
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# from ledgix_python import LedgixClient, vault_enforce, VaultConfig
|
|
6
|
+
#
|
|
7
|
+
# client = LedgixClient()
|
|
8
|
+
#
|
|
9
|
+
# @vault_enforce(client, tool_name="stripe_refund")
|
|
10
|
+
# def process_refund(amount: float, reason: str, **kwargs):
|
|
11
|
+
# token = kwargs.get("_clearance").token
|
|
12
|
+
# ...
|
|
13
|
+
|
|
14
|
+
"""Ledgix ALCV — agent-agnostic compliance shim for SOX 404 enforcement."""
|
|
15
|
+
|
|
16
|
+
from .client import LedgixClient
|
|
17
|
+
from .config import VaultConfig
|
|
18
|
+
from .enforce import VaultContext, vault_enforce
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
ClearanceDeniedError,
|
|
21
|
+
PolicyRegistrationError,
|
|
22
|
+
LedgixError,
|
|
23
|
+
TokenVerificationError,
|
|
24
|
+
VaultConnectionError,
|
|
25
|
+
)
|
|
26
|
+
from .models import (
|
|
27
|
+
ClearanceRequest,
|
|
28
|
+
ClearanceResponse,
|
|
29
|
+
PolicyRegistration,
|
|
30
|
+
PolicyRegistrationResponse,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# Core
|
|
37
|
+
"LedgixClient",
|
|
38
|
+
"VaultConfig",
|
|
39
|
+
# Enforcement
|
|
40
|
+
"vault_enforce",
|
|
41
|
+
"VaultContext",
|
|
42
|
+
# Models
|
|
43
|
+
"ClearanceRequest",
|
|
44
|
+
"ClearanceResponse",
|
|
45
|
+
"PolicyRegistration",
|
|
46
|
+
"PolicyRegistrationResponse",
|
|
47
|
+
# Exceptions
|
|
48
|
+
"LedgixError",
|
|
49
|
+
"ClearanceDeniedError",
|
|
50
|
+
"VaultConnectionError",
|
|
51
|
+
"TokenVerificationError",
|
|
52
|
+
"PolicyRegistrationError",
|
|
53
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Ledgix ALCV — Framework Adapters
|