codeastra 1.0.1__tar.gz → 1.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.
- {codeastra-1.0.1 → codeastra-1.1.0}/PKG-INFO +1 -1
- codeastra-1.1.0/codeastra/client.py +414 -0
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra/middleware.py +18 -5
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra.egg-info/PKG-INFO +1 -1
- {codeastra-1.0.1 → codeastra-1.1.0}/pyproject.toml +1 -1
- codeastra-1.0.1/codeastra/client.py +0 -239
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra/__init__.py +0 -0
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra/wrappers.py +0 -0
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra.egg-info/SOURCES.txt +0 -0
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra.egg-info/dependency_links.txt +0 -0
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra.egg-info/requires.txt +0 -0
- {codeastra-1.0.1 → codeastra-1.1.0}/codeastra.egg-info/top_level.txt +0 -0
- {codeastra-1.0.1 → codeastra-1.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeastra
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Blind Agent SDK — drop-in middleware for LangChain, CrewAI, AutoGPT. Two lines makes any agent blind to real data.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://codeastra.dev
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeAstraClient — full-featured async/sync HTTP client for the Codeastra API.
|
|
3
|
+
|
|
4
|
+
New in v1.1.0:
|
|
5
|
+
- mode="cloud" — default, uses app.codeastra.dev
|
|
6
|
+
- mode="onprem" — pulls deployment package, runs vault locally
|
|
7
|
+
- mode="hybrid" — local vault + cloud LLM (best for enterprise)
|
|
8
|
+
- zero_log=True — zero logging mode, max privacy
|
|
9
|
+
- Auto-register executor on init
|
|
10
|
+
- Auto-detect environment
|
|
11
|
+
- Auto-generate on-premise package
|
|
12
|
+
- HMAC verification of executor calls
|
|
13
|
+
- Tamper-proof audit verification
|
|
14
|
+
- Auto-signup on first use
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import os
|
|
20
|
+
import json
|
|
21
|
+
import hmac
|
|
22
|
+
import socket
|
|
23
|
+
import hashlib
|
|
24
|
+
import asyncio
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Optional
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
TOKEN_RE = re.compile(r'\[CVT:[A-Z]+:[A-F0-9]+\]')
|
|
31
|
+
|
|
32
|
+
_DEFAULT_BASE = "https://app.codeastra.dev"
|
|
33
|
+
_ONPREM_DEFAULT = "http://localhost:4000"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detect_environment() -> str:
|
|
37
|
+
env_mode = os.environ.get("CODEASTRA_MODE", "").lower()
|
|
38
|
+
if env_mode in ("cloud", "onprem", "hybrid"):
|
|
39
|
+
return env_mode
|
|
40
|
+
try:
|
|
41
|
+
s = socket.create_connection(("localhost", 4000), timeout=1)
|
|
42
|
+
s.close()
|
|
43
|
+
return "onprem"
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
return "cloud"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_base_url(mode: str, base_url: str = None) -> str:
|
|
50
|
+
if base_url:
|
|
51
|
+
return base_url.rstrip("/")
|
|
52
|
+
if mode in ("onprem", "hybrid"):
|
|
53
|
+
return os.environ.get("CODEASTRA_ONPREM_URL", _ONPREM_DEFAULT)
|
|
54
|
+
return _DEFAULT_BASE
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CodeAstraClient:
|
|
58
|
+
"""
|
|
59
|
+
Full-featured Codeastra client.
|
|
60
|
+
|
|
61
|
+
Modes:
|
|
62
|
+
cloud — default. Uses app.codeastra.dev
|
|
63
|
+
onprem — local vault. Auto-generates deployment package on first use.
|
|
64
|
+
hybrid — local vault + cloud LLM. Best for enterprise.
|
|
65
|
+
|
|
66
|
+
Usage:
|
|
67
|
+
# Cloud (default — zero config)
|
|
68
|
+
client = CodeAstraClient(api_key="sk-guard-xxx")
|
|
69
|
+
|
|
70
|
+
# On-premise (auto-generates docker-compose + setup.sh)
|
|
71
|
+
client = CodeAstraClient(api_key="sk-guard-xxx", mode="onprem")
|
|
72
|
+
|
|
73
|
+
# Hybrid (local vault, cloud LLM)
|
|
74
|
+
client = CodeAstraClient(api_key="sk-guard-xxx", mode="hybrid")
|
|
75
|
+
|
|
76
|
+
# Zero logging
|
|
77
|
+
client = CodeAstraClient(api_key="sk-guard-xxx", zero_log=True)
|
|
78
|
+
|
|
79
|
+
# With executor auto-registered
|
|
80
|
+
client = CodeAstraClient(api_key="sk-guard-xxx",
|
|
81
|
+
executor_url="https://your-app.com/execute")
|
|
82
|
+
|
|
83
|
+
# No API key — auto-signup
|
|
84
|
+
client = CodeAstraClient()
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
api_key: str = None,
|
|
90
|
+
base_url: str = None,
|
|
91
|
+
agent_id: str = "sdk-agent",
|
|
92
|
+
timeout: float = 10.0,
|
|
93
|
+
executor_url: str = None,
|
|
94
|
+
mode: str = "auto",
|
|
95
|
+
zero_log: bool = False,
|
|
96
|
+
onprem_dir: str = "./codeastra-onprem",
|
|
97
|
+
verbose: bool = False,
|
|
98
|
+
):
|
|
99
|
+
# Auto-signup if no API key
|
|
100
|
+
if not api_key:
|
|
101
|
+
api_key = os.environ.get("CODEASTRA_API_KEY")
|
|
102
|
+
if not api_key:
|
|
103
|
+
api_key = self._auto_signup()
|
|
104
|
+
|
|
105
|
+
# Auto-detect mode
|
|
106
|
+
if mode == "auto":
|
|
107
|
+
mode = _detect_environment()
|
|
108
|
+
|
|
109
|
+
self.api_key = api_key
|
|
110
|
+
self.agent_id = agent_id
|
|
111
|
+
self.mode = mode
|
|
112
|
+
self.zero_log = zero_log
|
|
113
|
+
self._verbose = verbose
|
|
114
|
+
self._timeout = timeout
|
|
115
|
+
self._onprem_dir = Path(onprem_dir)
|
|
116
|
+
self.base_url = _get_base_url(mode, base_url)
|
|
117
|
+
|
|
118
|
+
self._headers = {
|
|
119
|
+
"X-API-Key": api_key,
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
}
|
|
122
|
+
if zero_log:
|
|
123
|
+
self._headers["X-Zero-Log"] = "true"
|
|
124
|
+
|
|
125
|
+
self._sync_client: Optional[httpx.Client] = None
|
|
126
|
+
self._async_client: Optional[httpx.AsyncClient] = None
|
|
127
|
+
|
|
128
|
+
if verbose:
|
|
129
|
+
print(f"[CodeAstra] mode={mode} base={self.base_url} zero_log={zero_log}")
|
|
130
|
+
|
|
131
|
+
# On-premise: auto-generate deployment package
|
|
132
|
+
if mode in ("onprem", "hybrid"):
|
|
133
|
+
self._setup_onprem(mode)
|
|
134
|
+
|
|
135
|
+
# Auto-register executor if provided
|
|
136
|
+
if executor_url:
|
|
137
|
+
self._executor_url = executor_url
|
|
138
|
+
try:
|
|
139
|
+
self._post("/agent/executor", {
|
|
140
|
+
"execution_url": executor_url,
|
|
141
|
+
"action_type": "*",
|
|
142
|
+
"agent_id": agent_id,
|
|
143
|
+
"description": f"Auto-registered by SDK agent {agent_id} ({mode})",
|
|
144
|
+
})
|
|
145
|
+
if verbose:
|
|
146
|
+
print(f"[CodeAstra] Executor auto-registered: {executor_url}")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
if verbose:
|
|
149
|
+
print(f"[CodeAstra] Executor registration skipped: {e}")
|
|
150
|
+
|
|
151
|
+
# ── Auto-signup ───────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def _auto_signup(self) -> str:
|
|
154
|
+
"""Auto-create account on first use. Saves key to ~/.codeastra/credentials."""
|
|
155
|
+
creds_path = Path.home() / ".codeastra" / "credentials"
|
|
156
|
+
|
|
157
|
+
if creds_path.exists():
|
|
158
|
+
try:
|
|
159
|
+
data = json.loads(creds_path.read_text())
|
|
160
|
+
key = data.get("api_key")
|
|
161
|
+
if key:
|
|
162
|
+
return key
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
import uuid
|
|
167
|
+
email = os.environ.get("CODEASTRA_EMAIL", f"user-{uuid.uuid4().hex[:8]}@codeastra.local")
|
|
168
|
+
password = os.environ.get("CODEASTRA_PASSWORD", uuid.uuid4().hex)
|
|
169
|
+
name = os.environ.get("CODEASTRA_NAME", f"SDK User {uuid.uuid4().hex[:6]}")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
r = httpx.post(f"{_DEFAULT_BASE}/auth/signup", json={
|
|
173
|
+
"name": name, "email": email, "password": password,
|
|
174
|
+
}, timeout=10)
|
|
175
|
+
if r.is_success:
|
|
176
|
+
data = r.json()
|
|
177
|
+
api_key = data.get("api_key")
|
|
178
|
+
if api_key:
|
|
179
|
+
creds_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
creds_path.write_text(json.dumps({
|
|
181
|
+
"api_key": api_key, "email": email, "password": password,
|
|
182
|
+
}))
|
|
183
|
+
print(f"[CodeAstra] Account created. Key saved to {creds_path}")
|
|
184
|
+
return api_key
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"No API key. Set CODEASTRA_API_KEY or pass api_key= "
|
|
190
|
+
"or sign up at https://app.codeastra.dev"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# ── On-premise setup ──────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def _setup_onprem(self, mode: str):
|
|
196
|
+
"""Auto-generate on-premise deployment package if not already present."""
|
|
197
|
+
setup_sh = self._onprem_dir / "setup.sh"
|
|
198
|
+
if setup_sh.exists():
|
|
199
|
+
if self._verbose:
|
|
200
|
+
print(f"[CodeAstra] On-premise package at {self._onprem_dir}")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if self._verbose:
|
|
204
|
+
print(f"[CodeAstra] Generating on-premise package...")
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
resp = self._post("/onprem/generate", {
|
|
208
|
+
"deployment_mode": "docker",
|
|
209
|
+
"llm_provider": "ollama",
|
|
210
|
+
"llm_model": "llama3",
|
|
211
|
+
"air_gapped": mode != "hybrid",
|
|
212
|
+
"name": f"codeastra-{self.agent_id}",
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
files = resp.get("files", {})
|
|
216
|
+
if files:
|
|
217
|
+
self._onprem_dir.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
for filename, content in files.items():
|
|
219
|
+
fpath = self._onprem_dir / filename
|
|
220
|
+
fpath.write_text(content)
|
|
221
|
+
if setup_sh.exists():
|
|
222
|
+
setup_sh.chmod(0o755)
|
|
223
|
+
print(f"\n[CodeAstra] On-premise package ready: {self._onprem_dir}")
|
|
224
|
+
print(f" Run: cd {self._onprem_dir} && bash setup.sh\n")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
if self._verbose:
|
|
228
|
+
print(f"[CodeAstra] On-premise setup warning: {e} — falling back to cloud")
|
|
229
|
+
self.base_url = _DEFAULT_BASE
|
|
230
|
+
self.mode = "cloud"
|
|
231
|
+
|
|
232
|
+
# ── HMAC verification ─────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def verify_executor_call(payload: str, signature: str, secret: str) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Verify an incoming executor call is genuinely from Codeastra.
|
|
238
|
+
Use in your executor endpoint to reject forged requests.
|
|
239
|
+
|
|
240
|
+
Usage:
|
|
241
|
+
@app.post("/execute")
|
|
242
|
+
def execute(request):
|
|
243
|
+
if not CodeAstraClient.verify_executor_call(
|
|
244
|
+
request.body, request.headers["X-Codeastra-Signature"], YOUR_SECRET
|
|
245
|
+
):
|
|
246
|
+
raise HTTPException(401)
|
|
247
|
+
"""
|
|
248
|
+
expected = "sha256=" + hmac.new(
|
|
249
|
+
secret.encode(),
|
|
250
|
+
payload.encode() if isinstance(payload, str) else payload,
|
|
251
|
+
hashlib.sha256
|
|
252
|
+
).hexdigest()
|
|
253
|
+
return hmac.compare_digest(expected, signature)
|
|
254
|
+
|
|
255
|
+
# ── Audit verification ────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
def verify_audit(self) -> dict:
|
|
258
|
+
"""Verify tamper-proof audit chain integrity."""
|
|
259
|
+
try:
|
|
260
|
+
return self._get("/audit/secure/verify")
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return {"verified": False, "error": str(e)}
|
|
263
|
+
|
|
264
|
+
def export_audit(self, output_path: str = "audit_report.json") -> str:
|
|
265
|
+
"""Export full compliance audit report."""
|
|
266
|
+
try:
|
|
267
|
+
data = self._get("/audit/secure/export")
|
|
268
|
+
Path(output_path).write_text(json.dumps(data, indent=2))
|
|
269
|
+
return output_path
|
|
270
|
+
except Exception as e:
|
|
271
|
+
return str(e)
|
|
272
|
+
|
|
273
|
+
# ── Zero-log mode ─────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
def set_zero_log(self, enabled: bool = True):
|
|
276
|
+
"""Enable/disable zero-logging mode."""
|
|
277
|
+
self.zero_log = enabled
|
|
278
|
+
if enabled:
|
|
279
|
+
self._headers["X-Zero-Log"] = "true"
|
|
280
|
+
else:
|
|
281
|
+
self._headers.pop("X-Zero-Log", None)
|
|
282
|
+
self._sync_client = None
|
|
283
|
+
self._async_client = None
|
|
284
|
+
|
|
285
|
+
# ── sync helpers ──────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
def _get_sync(self) -> httpx.Client:
|
|
288
|
+
if self._sync_client is None or self._sync_client.is_closed:
|
|
289
|
+
self._sync_client = httpx.Client(
|
|
290
|
+
headers=self._headers, timeout=self._timeout)
|
|
291
|
+
return self._sync_client
|
|
292
|
+
|
|
293
|
+
def _post(self, path: str, body: dict) -> dict:
|
|
294
|
+
r = self._get_sync().post(f"{self.base_url}{path}", json=body)
|
|
295
|
+
r.raise_for_status()
|
|
296
|
+
return r.json()
|
|
297
|
+
|
|
298
|
+
def _get(self, path: str, params: dict = None) -> dict:
|
|
299
|
+
r = self._get_sync().get(f"{self.base_url}{path}", params=params or {})
|
|
300
|
+
r.raise_for_status()
|
|
301
|
+
return r.json()
|
|
302
|
+
|
|
303
|
+
# ── async helpers ─────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
def _get_async(self) -> httpx.AsyncClient:
|
|
306
|
+
if self._async_client is None or self._async_client.is_closed:
|
|
307
|
+
self._async_client = httpx.AsyncClient(
|
|
308
|
+
headers=self._headers, timeout=self._timeout)
|
|
309
|
+
return self._async_client
|
|
310
|
+
|
|
311
|
+
async def _apost(self, path: str, body: dict) -> dict:
|
|
312
|
+
r = await self._get_async().post(f"{self.base_url}{path}", json=body)
|
|
313
|
+
r.raise_for_status()
|
|
314
|
+
return r.json()
|
|
315
|
+
|
|
316
|
+
async def _aget(self, path: str, params: dict = None) -> dict:
|
|
317
|
+
r = await self._get_async().get(
|
|
318
|
+
f"{self.base_url}{path}", params=params or {})
|
|
319
|
+
r.raise_for_status()
|
|
320
|
+
return r.json()
|
|
321
|
+
|
|
322
|
+
# ── public sync API ───────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
def tokenize(self, data: dict, classification: str = "pii", ttl_hours: int = 24) -> dict:
|
|
325
|
+
resp = self._post("/vault/store", {
|
|
326
|
+
"data": data, "agent_id": self.agent_id,
|
|
327
|
+
"classification": classification, "ttl_hours": ttl_hours,
|
|
328
|
+
})
|
|
329
|
+
return resp.get("tokens", {})
|
|
330
|
+
|
|
331
|
+
def execute(self, action_type: str, params: dict, pipeline_id: str = None) -> dict:
|
|
332
|
+
body = {"agent_id": self.agent_id, "action_type": action_type, "params": params}
|
|
333
|
+
if pipeline_id:
|
|
334
|
+
body["pipeline_id"] = pipeline_id
|
|
335
|
+
return self._post("/pipeline/action", body)
|
|
336
|
+
return self._post("/agent/action", body)
|
|
337
|
+
|
|
338
|
+
def grant(self, receiving_agent: str, tokens: list, allowed_actions: list = [],
|
|
339
|
+
pipeline_id: str = None, purpose: str = None) -> dict:
|
|
340
|
+
return self._post("/vault/grant", {
|
|
341
|
+
"granting_agent": self.agent_id, "receiving_agent": receiving_agent,
|
|
342
|
+
"tokens": tokens, "allowed_actions": allowed_actions,
|
|
343
|
+
"pipeline_id": pipeline_id, "purpose": purpose,
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
def audit(self, pipeline_id: str = None, token: str = None) -> list:
|
|
347
|
+
params = {}
|
|
348
|
+
if pipeline_id: params["pipeline_id"] = pipeline_id
|
|
349
|
+
if token: params["token"] = token
|
|
350
|
+
return self._get("/pipeline/audit", params).get("audit", [])
|
|
351
|
+
|
|
352
|
+
def stats(self) -> dict:
|
|
353
|
+
return self._get("/vault/stats")
|
|
354
|
+
|
|
355
|
+
# ── public async API ──────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
async def atokenize(self, data: dict, classification: str = "pii", ttl_hours: int = 24) -> dict:
|
|
358
|
+
resp = await self._apost("/vault/store", {
|
|
359
|
+
"data": data, "agent_id": self.agent_id,
|
|
360
|
+
"classification": classification, "ttl_hours": ttl_hours,
|
|
361
|
+
})
|
|
362
|
+
return resp.get("tokens", {})
|
|
363
|
+
|
|
364
|
+
async def aexecute(self, action_type: str, params: dict, pipeline_id: str = None) -> dict:
|
|
365
|
+
body = {"agent_id": self.agent_id, "action_type": action_type, "params": params}
|
|
366
|
+
if pipeline_id:
|
|
367
|
+
body["pipeline_id"] = pipeline_id
|
|
368
|
+
return await self._apost("/pipeline/action", body)
|
|
369
|
+
return await self._apost("/agent/action", body)
|
|
370
|
+
|
|
371
|
+
async def agrant(self, receiving_agent: str, tokens: list,
|
|
372
|
+
allowed_actions: list = [], pipeline_id: str = None) -> dict:
|
|
373
|
+
return await self._apost("/vault/grant", {
|
|
374
|
+
"granting_agent": self.agent_id, "receiving_agent": receiving_agent,
|
|
375
|
+
"tokens": tokens, "allowed_actions": allowed_actions, "pipeline_id": pipeline_id,
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
# ── utility ───────────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def extract_tokens(obj: Any) -> list:
|
|
382
|
+
text = json.dumps(obj) if not isinstance(obj, str) else obj
|
|
383
|
+
return TOKEN_RE.findall(text)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def contains_token(val: Any) -> bool:
|
|
387
|
+
text = json.dumps(val) if not isinstance(val, str) else str(val)
|
|
388
|
+
return bool(TOKEN_RE.search(text))
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def is_token(val: str) -> bool:
|
|
392
|
+
return bool(TOKEN_RE.fullmatch(val.strip()))
|
|
393
|
+
|
|
394
|
+
def info(self) -> dict:
|
|
395
|
+
return {
|
|
396
|
+
"mode": self.mode,
|
|
397
|
+
"base_url": self.base_url,
|
|
398
|
+
"agent_id": self.agent_id,
|
|
399
|
+
"zero_log": self.zero_log,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
def close(self):
|
|
403
|
+
if self._sync_client: self._sync_client.close()
|
|
404
|
+
|
|
405
|
+
async def aclose(self):
|
|
406
|
+
if self._async_client: await self._async_client.aclose()
|
|
407
|
+
|
|
408
|
+
def __enter__(self): return self
|
|
409
|
+
def __exit__(self, *_): self.close()
|
|
410
|
+
async def __aenter__(self): return self
|
|
411
|
+
async def __aexit__(self, *_): await self.aclose()
|
|
412
|
+
|
|
413
|
+
def __repr__(self):
|
|
414
|
+
return f"CodeAstraClient(mode={self.mode!r}, agent_id={self.agent_id!r}, zero_log={self.zero_log})"
|
|
@@ -145,16 +145,29 @@ class BlindAgentMiddleware:
|
|
|
145
145
|
def __init__(
|
|
146
146
|
self,
|
|
147
147
|
agent: Any,
|
|
148
|
-
api_key: str,
|
|
149
|
-
agent_id: str
|
|
150
|
-
base_url: str
|
|
151
|
-
classification: str
|
|
148
|
+
api_key: str = None,
|
|
149
|
+
agent_id: str = "sdk-agent",
|
|
150
|
+
base_url: str = None,
|
|
151
|
+
classification: str = "pii",
|
|
152
152
|
pipeline_id: Optional[str] = None,
|
|
153
153
|
on_tokenize: Optional[Callable] = None,
|
|
154
154
|
verbose: bool = False,
|
|
155
|
+
mode: str = "auto", # auto | cloud | onprem | hybrid
|
|
156
|
+
zero_log: bool = False,
|
|
157
|
+
executor_url: str = None,
|
|
158
|
+
onprem_dir: str = "./codeastra-onprem",
|
|
155
159
|
):
|
|
156
160
|
self._agent = agent
|
|
157
|
-
self._client = CodeAstraClient(
|
|
161
|
+
self._client = CodeAstraClient(
|
|
162
|
+
api_key = api_key,
|
|
163
|
+
base_url = base_url,
|
|
164
|
+
agent_id = agent_id,
|
|
165
|
+
mode = mode,
|
|
166
|
+
zero_log = zero_log,
|
|
167
|
+
executor_url = executor_url,
|
|
168
|
+
onprem_dir = onprem_dir,
|
|
169
|
+
verbose = verbose,
|
|
170
|
+
)
|
|
158
171
|
self._classification = classification
|
|
159
172
|
self._pipeline_id = pipeline_id
|
|
160
173
|
self._on_tokenize = on_tokenize
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeastra
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Blind Agent SDK — drop-in middleware for LangChain, CrewAI, AutoGPT. Two lines makes any agent blind to real data.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://codeastra.dev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codeastra"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Blind Agent SDK — drop-in middleware for LangChain, CrewAI, AutoGPT. Two lines makes any agent blind to real data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
CodeAstraClient — low-level async/sync HTTP client for the Codeastra API.
|
|
3
|
-
All SDK components use this. Customers can also use it directly.
|
|
4
|
-
"""
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import re
|
|
8
|
-
import json
|
|
9
|
-
import asyncio
|
|
10
|
-
import threading
|
|
11
|
-
from typing import Any, Optional
|
|
12
|
-
|
|
13
|
-
import httpx
|
|
14
|
-
|
|
15
|
-
TOKEN_RE = re.compile(r'\[CVT:[A-Z]+:[A-F0-9]+\]')
|
|
16
|
-
|
|
17
|
-
_DEFAULT_BASE = "https://app.codeastra.dev"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class CodeAstraClient:
|
|
21
|
-
"""
|
|
22
|
-
Thin wrapper around the Codeastra REST API.
|
|
23
|
-
|
|
24
|
-
Usage:
|
|
25
|
-
client = CodeAstraClient(api_key="sk-guard-xxx")
|
|
26
|
-
tokens = client.tokenize({"name": "John Smith", "ssn": "123-45-6789"})
|
|
27
|
-
# → {"name": "[CVT:NAME:A1B2]", "ssn": "[CVT:SSN:C3D4]"}
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
api_key: str,
|
|
33
|
-
base_url: str = _DEFAULT_BASE,
|
|
34
|
-
agent_id: str = "sdk-agent",
|
|
35
|
-
timeout: float = 10.0,
|
|
36
|
-
executor_url: str = None, # optional: bring your own executor
|
|
37
|
-
):
|
|
38
|
-
self.api_key = api_key
|
|
39
|
-
self.base_url = base_url.rstrip("/")
|
|
40
|
-
self.agent_id = agent_id
|
|
41
|
-
self._headers = {
|
|
42
|
-
"X-API-Key": api_key,
|
|
43
|
-
"Content-Type": "application/json",
|
|
44
|
-
}
|
|
45
|
-
self._timeout = timeout
|
|
46
|
-
self._executor_url = executor_url
|
|
47
|
-
# Sync client (lazy)
|
|
48
|
-
self._sync_client: Optional[httpx.Client] = None
|
|
49
|
-
# Async client (lazy)
|
|
50
|
-
self._async_client: Optional[httpx.AsyncClient] = None
|
|
51
|
-
# Auto-register executor if provided
|
|
52
|
-
if executor_url:
|
|
53
|
-
try:
|
|
54
|
-
self._post("/agent/executor", {
|
|
55
|
-
"execution_url": executor_url,
|
|
56
|
-
"action_type": "*",
|
|
57
|
-
"agent_id": agent_id,
|
|
58
|
-
"description": f"Auto-registered by SDK agent {agent_id}",
|
|
59
|
-
})
|
|
60
|
-
except Exception:
|
|
61
|
-
pass # non-fatal — zero-config mode still works
|
|
62
|
-
|
|
63
|
-
# ── sync helpers ──────────────────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
def _get_sync(self) -> httpx.Client:
|
|
66
|
-
if self._sync_client is None or self._sync_client.is_closed:
|
|
67
|
-
self._sync_client = httpx.Client(
|
|
68
|
-
headers=self._headers, timeout=self._timeout)
|
|
69
|
-
return self._sync_client
|
|
70
|
-
|
|
71
|
-
def _post(self, path: str, body: dict) -> dict:
|
|
72
|
-
r = self._get_sync().post(f"{self.base_url}{path}", json=body)
|
|
73
|
-
r.raise_for_status()
|
|
74
|
-
return r.json()
|
|
75
|
-
|
|
76
|
-
def _get(self, path: str, params: dict = None) -> dict:
|
|
77
|
-
r = self._get_sync().get(f"{self.base_url}{path}", params=params or {})
|
|
78
|
-
r.raise_for_status()
|
|
79
|
-
return r.json()
|
|
80
|
-
|
|
81
|
-
# ── async helpers ─────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
def _get_async(self) -> httpx.AsyncClient:
|
|
84
|
-
if self._async_client is None or self._async_client.is_closed:
|
|
85
|
-
self._async_client = httpx.AsyncClient(
|
|
86
|
-
headers=self._headers, timeout=self._timeout)
|
|
87
|
-
return self._async_client
|
|
88
|
-
|
|
89
|
-
async def _apost(self, path: str, body: dict) -> dict:
|
|
90
|
-
r = await self._get_async().post(f"{self.base_url}{path}", json=body)
|
|
91
|
-
r.raise_for_status()
|
|
92
|
-
return r.json()
|
|
93
|
-
|
|
94
|
-
async def _aget(self, path: str, params: dict = None) -> dict:
|
|
95
|
-
r = await self._get_async().get(
|
|
96
|
-
f"{self.base_url}{path}", params=params or {})
|
|
97
|
-
r.raise_for_status()
|
|
98
|
-
return r.json()
|
|
99
|
-
|
|
100
|
-
# ── public sync API ───────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
def tokenize(
|
|
103
|
-
self,
|
|
104
|
-
data: dict,
|
|
105
|
-
classification: str = "pii",
|
|
106
|
-
ttl_hours: int = 24,
|
|
107
|
-
) -> dict:
|
|
108
|
-
"""
|
|
109
|
-
Store real data in vault. Returns token map.
|
|
110
|
-
{"name": "John"} → {"name": "[CVT:NAME:A1B2]"}
|
|
111
|
-
"""
|
|
112
|
-
resp = self._post("/vault/store", {
|
|
113
|
-
"data": data,
|
|
114
|
-
"agent_id": self.agent_id,
|
|
115
|
-
"classification": classification,
|
|
116
|
-
"ttl_hours": ttl_hours,
|
|
117
|
-
})
|
|
118
|
-
return resp.get("tokens", {})
|
|
119
|
-
|
|
120
|
-
def execute(
|
|
121
|
-
self,
|
|
122
|
-
action_type: str,
|
|
123
|
-
params: dict,
|
|
124
|
-
pipeline_id: str = None,
|
|
125
|
-
) -> dict:
|
|
126
|
-
"""
|
|
127
|
-
Submit an action with token params.
|
|
128
|
-
Codeastra resolves tokens → real values → POSTs to your executor.
|
|
129
|
-
Agent never sees real values.
|
|
130
|
-
"""
|
|
131
|
-
body = {
|
|
132
|
-
"agent_id": self.agent_id,
|
|
133
|
-
"action_type": action_type,
|
|
134
|
-
"params": params,
|
|
135
|
-
}
|
|
136
|
-
if pipeline_id:
|
|
137
|
-
body["pipeline_id"] = pipeline_id
|
|
138
|
-
return self._post("/pipeline/action", body)
|
|
139
|
-
return self._post("/agent/action", body)
|
|
140
|
-
|
|
141
|
-
def grant(
|
|
142
|
-
self,
|
|
143
|
-
receiving_agent: str,
|
|
144
|
-
tokens: list[str],
|
|
145
|
-
allowed_actions: list[str] = [],
|
|
146
|
-
pipeline_id: str = None,
|
|
147
|
-
purpose: str = None,
|
|
148
|
-
) -> dict:
|
|
149
|
-
"""Grant tokens to another agent in a pipeline."""
|
|
150
|
-
return self._post("/vault/grant", {
|
|
151
|
-
"granting_agent": self.agent_id,
|
|
152
|
-
"receiving_agent": receiving_agent,
|
|
153
|
-
"tokens": tokens,
|
|
154
|
-
"allowed_actions": allowed_actions,
|
|
155
|
-
"pipeline_id": pipeline_id,
|
|
156
|
-
"purpose": purpose,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
def audit(self, pipeline_id: str = None, token: str = None) -> list:
|
|
160
|
-
"""Get chain of custody for a pipeline or token."""
|
|
161
|
-
params = {}
|
|
162
|
-
if pipeline_id: params["pipeline_id"] = pipeline_id
|
|
163
|
-
if token: params["token"] = token
|
|
164
|
-
return self._get("/pipeline/audit", params).get("audit", [])
|
|
165
|
-
|
|
166
|
-
# ── public async API ──────────────────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
async def atokenize(
|
|
169
|
-
self,
|
|
170
|
-
data: dict,
|
|
171
|
-
classification: str = "pii",
|
|
172
|
-
ttl_hours: int = 24,
|
|
173
|
-
) -> dict:
|
|
174
|
-
resp = await self._apost("/vault/store", {
|
|
175
|
-
"data": data,
|
|
176
|
-
"agent_id": self.agent_id,
|
|
177
|
-
"classification": classification,
|
|
178
|
-
"ttl_hours": ttl_hours,
|
|
179
|
-
})
|
|
180
|
-
return resp.get("tokens", {})
|
|
181
|
-
|
|
182
|
-
async def aexecute(
|
|
183
|
-
self,
|
|
184
|
-
action_type: str,
|
|
185
|
-
params: dict,
|
|
186
|
-
pipeline_id: str = None,
|
|
187
|
-
) -> dict:
|
|
188
|
-
body = {
|
|
189
|
-
"agent_id": self.agent_id,
|
|
190
|
-
"action_type": action_type,
|
|
191
|
-
"params": params,
|
|
192
|
-
}
|
|
193
|
-
if pipeline_id:
|
|
194
|
-
body["pipeline_id"] = pipeline_id
|
|
195
|
-
return await self._apost("/pipeline/action", body)
|
|
196
|
-
return await self._apost("/agent/action", body)
|
|
197
|
-
|
|
198
|
-
async def agrant(
|
|
199
|
-
self,
|
|
200
|
-
receiving_agent: str,
|
|
201
|
-
tokens: list[str],
|
|
202
|
-
allowed_actions: list[str] = [],
|
|
203
|
-
pipeline_id: str = None,
|
|
204
|
-
) -> dict:
|
|
205
|
-
return await self._apost("/vault/grant", {
|
|
206
|
-
"granting_agent": self.agent_id,
|
|
207
|
-
"receiving_agent": receiving_agent,
|
|
208
|
-
"tokens": tokens,
|
|
209
|
-
"allowed_actions": allowed_actions,
|
|
210
|
-
"pipeline_id": pipeline_id,
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
# ── utility ───────────────────────────────────────────────────────────────
|
|
214
|
-
|
|
215
|
-
@staticmethod
|
|
216
|
-
def extract_tokens(obj: Any) -> list[str]:
|
|
217
|
-
"""Extract all vault tokens from any string/dict/list."""
|
|
218
|
-
text = json.dumps(obj) if not isinstance(obj, str) else obj
|
|
219
|
-
return TOKEN_RE.findall(text)
|
|
220
|
-
|
|
221
|
-
@staticmethod
|
|
222
|
-
def contains_token(val: Any) -> bool:
|
|
223
|
-
text = json.dumps(val) if not isinstance(val, str) else str(val)
|
|
224
|
-
return bool(TOKEN_RE.search(text))
|
|
225
|
-
|
|
226
|
-
@staticmethod
|
|
227
|
-
def is_token(val: str) -> bool:
|
|
228
|
-
return bool(TOKEN_RE.fullmatch(val.strip()))
|
|
229
|
-
|
|
230
|
-
def close(self):
|
|
231
|
-
if self._sync_client: self._sync_client.close()
|
|
232
|
-
|
|
233
|
-
async def aclose(self):
|
|
234
|
-
if self._async_client: await self._async_client.aclose()
|
|
235
|
-
|
|
236
|
-
def __enter__(self): return self
|
|
237
|
-
def __exit__(self, *_): self.close()
|
|
238
|
-
async def __aenter__(self): return self
|
|
239
|
-
async def __aexit__(self, *_): await self.aclose()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|