bylaw-python 0.4.0__py3-none-any.whl
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.
- bylaw_python/__init__.py +114 -0
- bylaw_python/adapters/__init__.py +1 -0
- bylaw_python/adapters/_core.py +58 -0
- bylaw_python/adapters/crewai.py +99 -0
- bylaw_python/adapters/langchain.py +167 -0
- bylaw_python/adapters/llamaindex.py +90 -0
- bylaw_python/cli.py +366 -0
- bylaw_python/client.py +1595 -0
- bylaw_python/config.py +95 -0
- bylaw_python/counterparty.py +145 -0
- bylaw_python/enforce.py +561 -0
- bylaw_python/exceptions.py +104 -0
- bylaw_python/manifest.py +152 -0
- bylaw_python/models.py +330 -0
- bylaw_python/pending.py +128 -0
- bylaw_python/webhook.py +44 -0
- bylaw_python-0.4.0.dist-info/METADATA +227 -0
- bylaw_python-0.4.0.dist-info/RECORD +20 -0
- bylaw_python-0.4.0.dist-info/WHEEL +4 -0
- bylaw_python-0.4.0.dist-info/entry_points.txt +2 -0
bylaw_python/cli.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Ledgix CLI — local dev quickstart and management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import textwrap
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
COMPOSE_FILE = "docker-compose.ledgix.yml"
|
|
17
|
+
ENV_FILE = ".env.ledgix"
|
|
18
|
+
MANIFEST_FILE = "ledgix.yaml"
|
|
19
|
+
|
|
20
|
+
VAULT_PORT = 8080
|
|
21
|
+
JUDGE_PORT = 8000
|
|
22
|
+
POSTGRES_PORT = 5433
|
|
23
|
+
DEV_API_KEY = "ldgx-dev-key-00000000"
|
|
24
|
+
DEV_AGENT_ID = "my-agent"
|
|
25
|
+
DEV_TENANT = "dev-tenant"
|
|
26
|
+
|
|
27
|
+
COMPOSE_TEMPLATE = textwrap.dedent("""\
|
|
28
|
+
services:
|
|
29
|
+
postgres:
|
|
30
|
+
image: pgvector/pgvector:pg16
|
|
31
|
+
container_name: ledgix_dev_postgres
|
|
32
|
+
restart: unless-stopped
|
|
33
|
+
ports:
|
|
34
|
+
- "127.0.0.1:{postgres_port}:5432"
|
|
35
|
+
environment:
|
|
36
|
+
POSTGRES_USER: ledgix
|
|
37
|
+
POSTGRES_PASSWORD: ledgix-dev
|
|
38
|
+
POSTGRES_DB: ledgix_dev
|
|
39
|
+
volumes:
|
|
40
|
+
- ledgix_pgdata:/var/lib/postgresql/data
|
|
41
|
+
healthcheck:
|
|
42
|
+
test: ["CMD-SHELL", "pg_isready -U ledgix"]
|
|
43
|
+
interval: 5s
|
|
44
|
+
timeout: 3s
|
|
45
|
+
retries: 10
|
|
46
|
+
|
|
47
|
+
vault:
|
|
48
|
+
image: ghcr.io/ledgix-dev/alcv-vault:latest
|
|
49
|
+
container_name: ledgix_dev_vault
|
|
50
|
+
restart: unless-stopped
|
|
51
|
+
ports:
|
|
52
|
+
- "127.0.0.1:{vault_port}:8000"
|
|
53
|
+
environment:
|
|
54
|
+
VAULT_PORT: "8000"
|
|
55
|
+
VAULT_JUDGE_URL: http://ledgix_dev_judge:8000
|
|
56
|
+
VAULT_JWT_ISSUER: alcv-vault
|
|
57
|
+
VAULT_JWT_AUDIENCE: ledgix-sdk
|
|
58
|
+
VAULT_JWT_TTL: "300"
|
|
59
|
+
VAULT_KEY_ID: dev-key-001
|
|
60
|
+
VAULT_ALLOW_INSECURE_DEV_MODE: "true"
|
|
61
|
+
VAULT_DEV_TENANT_ID: {tenant_id}
|
|
62
|
+
VAULT_DEV_API_KEY: {api_key}
|
|
63
|
+
VAULT_DEV_DB_URL: postgres://ledgix:ledgix-dev@ledgix_dev_postgres:5432/ledgix_dev?sslmode=disable
|
|
64
|
+
VAULT_RATE_LIMIT_RPS: "0"
|
|
65
|
+
depends_on:
|
|
66
|
+
postgres:
|
|
67
|
+
condition: service_healthy
|
|
68
|
+
networks:
|
|
69
|
+
- ledgix_dev_net
|
|
70
|
+
|
|
71
|
+
judge:
|
|
72
|
+
image: ghcr.io/ledgix-dev/llm-judge:latest
|
|
73
|
+
container_name: ledgix_dev_judge
|
|
74
|
+
restart: unless-stopped
|
|
75
|
+
environment:
|
|
76
|
+
DATABASE_URL: postgres://ledgix:ledgix-dev@ledgix_dev_postgres:5432/ledgix_dev?sslmode=disable
|
|
77
|
+
EMBEDDING_MODEL: bedrock/amazon.titan-embed-text-v2:0
|
|
78
|
+
EVAL_MODEL: bedrock/amazon.nova-pro-v1:0
|
|
79
|
+
AWS_REGION: us-east-1
|
|
80
|
+
LOG_FORMAT: json
|
|
81
|
+
depends_on:
|
|
82
|
+
postgres:
|
|
83
|
+
condition: service_healthy
|
|
84
|
+
networks:
|
|
85
|
+
- ledgix_dev_net
|
|
86
|
+
healthcheck:
|
|
87
|
+
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
88
|
+
interval: 10s
|
|
89
|
+
timeout: 3s
|
|
90
|
+
retries: 5
|
|
91
|
+
start_period: 10s
|
|
92
|
+
|
|
93
|
+
volumes:
|
|
94
|
+
ledgix_pgdata:
|
|
95
|
+
|
|
96
|
+
networks:
|
|
97
|
+
ledgix_dev_net:
|
|
98
|
+
driver: bridge
|
|
99
|
+
""")
|
|
100
|
+
|
|
101
|
+
MANIFEST_TEMPLATE = textwrap.dedent("""\
|
|
102
|
+
# Ledgix manifest — maps tool names to policy IDs.
|
|
103
|
+
# See https://docs.ledgix.dev/sdk/manifest for full syntax.
|
|
104
|
+
version: "1"
|
|
105
|
+
defaults:
|
|
106
|
+
review_mode: block
|
|
107
|
+
|
|
108
|
+
tools:
|
|
109
|
+
# Example: uncomment and adjust for your tools
|
|
110
|
+
# stripe_refund:
|
|
111
|
+
# policy_id: refund-policy
|
|
112
|
+
# confidence_floor: high
|
|
113
|
+
# send_email:
|
|
114
|
+
# policy_id: comms-policy
|
|
115
|
+
""")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _docker_available() -> bool:
|
|
119
|
+
return shutil.which("docker") is not None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _compose_cmd() -> list[str]:
|
|
123
|
+
result = subprocess.run(
|
|
124
|
+
["docker", "compose", "version"],
|
|
125
|
+
capture_output=True, text=True,
|
|
126
|
+
)
|
|
127
|
+
if result.returncode == 0:
|
|
128
|
+
return ["docker", "compose"]
|
|
129
|
+
result2 = subprocess.run(
|
|
130
|
+
["docker-compose", "version"],
|
|
131
|
+
capture_output=True, text=True,
|
|
132
|
+
)
|
|
133
|
+
if result2.returncode == 0:
|
|
134
|
+
return ["docker-compose"]
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _compose_base() -> list[str]:
|
|
139
|
+
cmd = _compose_cmd()
|
|
140
|
+
if not cmd:
|
|
141
|
+
click.echo("Error: docker compose is not available.", err=True)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
return [*cmd, "-f", COMPOSE_FILE, "-p", "ledgix-dev"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _poll_health(url: str, timeout: int = 90) -> bool:
|
|
147
|
+
"""Poll a health endpoint until it returns 200 or timeout."""
|
|
148
|
+
import urllib.request
|
|
149
|
+
import urllib.error
|
|
150
|
+
|
|
151
|
+
deadline = time.monotonic() + timeout
|
|
152
|
+
while time.monotonic() < deadline:
|
|
153
|
+
try:
|
|
154
|
+
req = urllib.request.Request(url, method="GET")
|
|
155
|
+
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
156
|
+
if resp.status == 200:
|
|
157
|
+
return True
|
|
158
|
+
except (urllib.error.URLError, OSError, TimeoutError):
|
|
159
|
+
pass
|
|
160
|
+
time.sleep(2)
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@click.group()
|
|
165
|
+
@click.version_option(package_name="bylaw-python")
|
|
166
|
+
def main():
|
|
167
|
+
"""Ledgix CLI — local development tools for the ALCV platform."""
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@main.command()
|
|
172
|
+
@click.option("--vault-port", default=VAULT_PORT, type=int, help="Host port for vault")
|
|
173
|
+
@click.option("--api-key", default=DEV_API_KEY, help="Dev API key")
|
|
174
|
+
@click.option("--tenant-id", default=DEV_TENANT, help="Dev tenant ID")
|
|
175
|
+
@click.option("--skip-health-check", is_flag=True, help="Don't wait for services to become healthy")
|
|
176
|
+
def init(vault_port: int, api_key: str, tenant_id: str, skip_health_check: bool):
|
|
177
|
+
"""Scaffold and start a local Ledgix dev environment.
|
|
178
|
+
|
|
179
|
+
Creates docker-compose, env, and manifest files, then starts all
|
|
180
|
+
services. On a clean machine with Docker, first clearance request
|
|
181
|
+
should succeed within 60 seconds.
|
|
182
|
+
"""
|
|
183
|
+
if not _docker_available():
|
|
184
|
+
click.echo("Error: Docker is not installed or not in PATH.", err=True)
|
|
185
|
+
click.echo("Install Docker Desktop: https://docs.docker.com/get-docker/", err=True)
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
|
|
188
|
+
compose_cmd = _compose_cmd()
|
|
189
|
+
if not compose_cmd:
|
|
190
|
+
click.echo("Error: Neither 'docker compose' nor 'docker-compose' found.", err=True)
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
cwd = Path.cwd()
|
|
194
|
+
|
|
195
|
+
compose_path = cwd / COMPOSE_FILE
|
|
196
|
+
if compose_path.exists():
|
|
197
|
+
click.echo(f" {COMPOSE_FILE} already exists, skipping.")
|
|
198
|
+
else:
|
|
199
|
+
compose_content = COMPOSE_TEMPLATE.format(
|
|
200
|
+
vault_port=vault_port,
|
|
201
|
+
postgres_port=POSTGRES_PORT,
|
|
202
|
+
api_key=api_key,
|
|
203
|
+
tenant_id=tenant_id,
|
|
204
|
+
)
|
|
205
|
+
compose_path.write_text(compose_content)
|
|
206
|
+
click.echo(f" Created {COMPOSE_FILE}")
|
|
207
|
+
|
|
208
|
+
env_path = cwd / ENV_FILE
|
|
209
|
+
if env_path.exists():
|
|
210
|
+
click.echo(f" {ENV_FILE} already exists, skipping.")
|
|
211
|
+
else:
|
|
212
|
+
env_content = "\n".join([
|
|
213
|
+
f"LEDGIX_VAULT_URL=http://localhost:{vault_port}",
|
|
214
|
+
f"LEDGIX_VAULT_API_KEY={api_key}",
|
|
215
|
+
f"LEDGIX_AGENT_ID={DEV_AGENT_ID}",
|
|
216
|
+
"",
|
|
217
|
+
])
|
|
218
|
+
env_path.write_text(env_content)
|
|
219
|
+
click.echo(f" Created {ENV_FILE}")
|
|
220
|
+
|
|
221
|
+
manifest_path = cwd / MANIFEST_FILE
|
|
222
|
+
if manifest_path.exists():
|
|
223
|
+
click.echo(f" {MANIFEST_FILE} already exists, skipping.")
|
|
224
|
+
else:
|
|
225
|
+
manifest_path.write_text(MANIFEST_TEMPLATE)
|
|
226
|
+
click.echo(f" Created {MANIFEST_FILE}")
|
|
227
|
+
|
|
228
|
+
click.echo("\nStarting services...")
|
|
229
|
+
base = _compose_base()
|
|
230
|
+
result = subprocess.run([*base, "up", "-d", "--pull", "always"], capture_output=True, text=True)
|
|
231
|
+
if result.returncode != 0:
|
|
232
|
+
click.echo(f"Error starting services:\n{result.stderr}", err=True)
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
click.echo(" Containers started.")
|
|
235
|
+
|
|
236
|
+
if not skip_health_check:
|
|
237
|
+
click.echo("\nWaiting for services to become healthy...")
|
|
238
|
+
vault_url = f"http://localhost:{vault_port}/health"
|
|
239
|
+
if _poll_health(vault_url, timeout=90):
|
|
240
|
+
click.echo(" Vault is healthy.")
|
|
241
|
+
else:
|
|
242
|
+
click.echo(
|
|
243
|
+
f" Warning: Vault did not respond at {vault_url} within 90s.\n"
|
|
244
|
+
" Run 'ledgix status' to check, or 'docker compose -f docker-compose.ledgix.yml logs' for details.",
|
|
245
|
+
err=True,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
click.echo("\n" + "=" * 56)
|
|
249
|
+
click.echo(" Ledgix dev environment is ready!")
|
|
250
|
+
click.echo("=" * 56)
|
|
251
|
+
click.echo(f"""
|
|
252
|
+
Vault: http://localhost:{vault_port}
|
|
253
|
+
API Key: {api_key}
|
|
254
|
+
Tenant: {tenant_id}
|
|
255
|
+
|
|
256
|
+
Next steps:
|
|
257
|
+
|
|
258
|
+
1. Add to your agent:
|
|
259
|
+
|
|
260
|
+
import bylaw_python as ledgix
|
|
261
|
+
ledgix.configure(
|
|
262
|
+
vault_url="http://localhost:{vault_port}",
|
|
263
|
+
vault_api_key="{api_key}",
|
|
264
|
+
agent_id="{DEV_AGENT_ID}",
|
|
265
|
+
)
|
|
266
|
+
ledgix.auto_instrument(tools)
|
|
267
|
+
|
|
268
|
+
2. Or source the env file and configure() picks it up:
|
|
269
|
+
|
|
270
|
+
source {ENV_FILE} # or: set -a; . {ENV_FILE}; set +a
|
|
271
|
+
ledgix.configure()
|
|
272
|
+
|
|
273
|
+
3. Upload a policy:
|
|
274
|
+
|
|
275
|
+
curl -X POST http://localhost:{vault_port}/admin/upload-policy \\
|
|
276
|
+
-H "X-Vault-API-Key: {api_key}" \\
|
|
277
|
+
-F "file=@your-policy.md" \\
|
|
278
|
+
-F "documentName=My Policy" \\
|
|
279
|
+
-F "policyId=my-policy"
|
|
280
|
+
""")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@main.command()
|
|
284
|
+
def status():
|
|
285
|
+
"""Check whether the local Ledgix dev environment is running."""
|
|
286
|
+
if not (Path.cwd() / COMPOSE_FILE).exists():
|
|
287
|
+
click.echo(f"No {COMPOSE_FILE} found in current directory.")
|
|
288
|
+
click.echo("Run 'ledgix init' to create a local dev environment.")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
base = _compose_base()
|
|
292
|
+
result = subprocess.run([*base, "ps", "--format", "json"], capture_output=True, text=True)
|
|
293
|
+
if result.returncode != 0:
|
|
294
|
+
click.echo("Could not query container status.")
|
|
295
|
+
click.echo(result.stderr)
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
containers: list[dict] = []
|
|
299
|
+
raw = result.stdout.strip()
|
|
300
|
+
if raw:
|
|
301
|
+
try:
|
|
302
|
+
parsed = json.loads(raw)
|
|
303
|
+
if isinstance(parsed, list):
|
|
304
|
+
containers = parsed
|
|
305
|
+
elif isinstance(parsed, dict):
|
|
306
|
+
containers = [parsed]
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
for line in raw.splitlines():
|
|
309
|
+
line = line.strip()
|
|
310
|
+
if line:
|
|
311
|
+
try:
|
|
312
|
+
containers.append(json.loads(line))
|
|
313
|
+
except json.JSONDecodeError:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
if not containers:
|
|
317
|
+
click.echo("No containers running. Run 'ledgix init' to start.")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
click.echo(f"{'Service':<20} {'State':<12} {'Health':<12} {'Ports'}")
|
|
321
|
+
click.echo("-" * 70)
|
|
322
|
+
for c in containers:
|
|
323
|
+
name = c.get("Service") or c.get("Name", "?")
|
|
324
|
+
state = c.get("State", "?")
|
|
325
|
+
health = c.get("Health", "-")
|
|
326
|
+
ports = c.get("Publishers") or c.get("Ports", "")
|
|
327
|
+
if isinstance(ports, list):
|
|
328
|
+
port_strs = []
|
|
329
|
+
for p in ports:
|
|
330
|
+
if isinstance(p, dict) and p.get("PublishedPort"):
|
|
331
|
+
port_strs.append(f":{p['PublishedPort']}")
|
|
332
|
+
ports = ", ".join(port_strs) if port_strs else "-"
|
|
333
|
+
click.echo(f"{name:<20} {state:<12} {health:<12} {ports}")
|
|
334
|
+
|
|
335
|
+
env_path = Path.cwd() / ENV_FILE
|
|
336
|
+
if env_path.exists():
|
|
337
|
+
click.echo(f"\nEnv file: {env_path}")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@main.command()
|
|
341
|
+
@click.option("--volumes", is_flag=True, help="Also remove data volumes")
|
|
342
|
+
@click.confirmation_option(prompt="This will stop and remove all Ledgix dev containers. Continue?")
|
|
343
|
+
def teardown(volumes: bool):
|
|
344
|
+
"""Stop and remove the local Ledgix dev environment."""
|
|
345
|
+
compose_path = Path.cwd() / COMPOSE_FILE
|
|
346
|
+
if not compose_path.exists():
|
|
347
|
+
click.echo(f"No {COMPOSE_FILE} found in current directory. Nothing to tear down.")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
base = _compose_base()
|
|
351
|
+
cmd = [*base, "down"]
|
|
352
|
+
if volumes:
|
|
353
|
+
cmd.append("-v")
|
|
354
|
+
|
|
355
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
356
|
+
if result.returncode != 0:
|
|
357
|
+
click.echo(f"Error during teardown:\n{result.stderr}", err=True)
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
click.echo("Ledgix dev environment stopped and removed.")
|
|
361
|
+
if volumes:
|
|
362
|
+
click.echo("Data volumes removed.")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if __name__ == "__main__":
|
|
366
|
+
main()
|