codeastra 1.0.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.0/PKG-INFO +30 -0
- codeastra-1.0.0/codeastra/__init__.py +12 -0
- codeastra-1.0.0/codeastra/client.py +239 -0
- codeastra-1.0.0/codeastra/middleware.py +412 -0
- codeastra-1.0.0/codeastra/wrappers.py +154 -0
- codeastra-1.0.0/codeastra.egg-info/PKG-INFO +30 -0
- codeastra-1.0.0/codeastra.egg-info/SOURCES.txt +10 -0
- codeastra-1.0.0/codeastra.egg-info/dependency_links.txt +1 -0
- codeastra-1.0.0/codeastra.egg-info/requires.txt +13 -0
- codeastra-1.0.0/codeastra.egg-info/top_level.txt +1 -0
- codeastra-1.0.0/pyproject.toml +41 -0
- codeastra-1.0.0/setup.cfg +4 -0
codeastra-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codeastra
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Blind Agent SDK — drop-in middleware for LangChain, CrewAI, AutoGPT. Two lines makes any agent blind to real data.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://codeastra.dev
|
|
7
|
+
Project-URL: Documentation, https://docs.codeastra.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/codeastra/codeastra-python
|
|
9
|
+
Keywords: ai,agents,langchain,crewai,privacy,hipaa,security,tokenization
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Provides-Extra: langchain
|
|
23
|
+
Requires-Dist: langchain>=0.2.0; extra == "langchain"
|
|
24
|
+
Requires-Dist: langchain-core>=0.2.0; extra == "langchain"
|
|
25
|
+
Provides-Extra: crewai
|
|
26
|
+
Requires-Dist: crewai>=0.30.0; extra == "crewai"
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: langchain>=0.2.0; extra == "all"
|
|
29
|
+
Requires-Dist: langchain-core>=0.2.0; extra == "all"
|
|
30
|
+
Requires-Dist: crewai>=0.30.0; extra == "all"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .middleware import BlindAgentMiddleware
|
|
2
|
+
from .client import CodeAstraClient
|
|
3
|
+
from .wrappers import blind_tool, BlindCrewAIAgent, BlindAutoGPTAgent
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BlindAgentMiddleware",
|
|
8
|
+
"CodeAstraClient",
|
|
9
|
+
"blind_tool",
|
|
10
|
+
"BlindCrewAIAgent",
|
|
11
|
+
"BlindAutoGPTAgent",
|
|
12
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
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()
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlindAgentMiddleware — drop-in middleware for LangChain, CrewAI, AutoGPT.
|
|
3
|
+
|
|
4
|
+
Two lines. Any agent becomes blind.
|
|
5
|
+
|
|
6
|
+
from codeastra import BlindAgentMiddleware
|
|
7
|
+
agent = BlindAgentMiddleware(your_langchain_agent, api_key="sk-guard-xxx")
|
|
8
|
+
|
|
9
|
+
How it works:
|
|
10
|
+
1. Intercepts every tool call before the agent sees the result
|
|
11
|
+
2. Scans the result for PII/PHI/PCI fields
|
|
12
|
+
3. Tokenizes detected fields → stores real values in Codeastra vault
|
|
13
|
+
4. Returns tokens to the agent — agent reasons on tokens, never real data
|
|
14
|
+
5. When agent submits a final action, intercepts it, resolves tokens → executes
|
|
15
|
+
|
|
16
|
+
Supports: LangChain AgentExecutor, CrewAI Agent, AutoGPT-style run() agents,
|
|
17
|
+
any object with .run() / .invoke() / .chat() / .step()
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import json
|
|
23
|
+
import inspect
|
|
24
|
+
import functools
|
|
25
|
+
from typing import Any, Callable, Optional
|
|
26
|
+
|
|
27
|
+
from .client import CodeAstraClient, TOKEN_RE
|
|
28
|
+
|
|
29
|
+
# Fields that trigger automatic tokenization when found in tool output
|
|
30
|
+
_PII_FIELDS = {
|
|
31
|
+
"name", "first_name", "last_name", "full_name",
|
|
32
|
+
"email", "email_address",
|
|
33
|
+
"phone", "phone_number", "mobile",
|
|
34
|
+
"ssn", "social_security", "social_security_number",
|
|
35
|
+
"dob", "date_of_birth", "birthday",
|
|
36
|
+
"address", "street", "zip", "postal_code",
|
|
37
|
+
"credit_card", "card_number", "cvv", "expiry",
|
|
38
|
+
"mrn", "patient_id", "npi",
|
|
39
|
+
"account_number", "routing_number", "iban",
|
|
40
|
+
"passport", "license", "drivers_license",
|
|
41
|
+
"ip", "ip_address", "mac_address",
|
|
42
|
+
"username", "user_id", "employee_id",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_PHI_FIELDS = {
|
|
46
|
+
"diagnosis", "icd_code", "medication", "prescription",
|
|
47
|
+
"allergy", "lab_result", "test_result", "condition",
|
|
48
|
+
"treatment", "procedure", "insurance_id", "member_id",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_PCI_FIELDS = {
|
|
52
|
+
"card_number", "credit_card", "cvv", "expiry",
|
|
53
|
+
"account_number", "routing_number",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _classify(fields: set) -> str:
|
|
58
|
+
if fields & _PCI_FIELDS: return "pci"
|
|
59
|
+
if fields & _PHI_FIELDS: return "phi"
|
|
60
|
+
return "pii"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_sensitive(obj: Any) -> dict:
|
|
64
|
+
"""
|
|
65
|
+
Walk a dict/str/list and extract fields that look sensitive.
|
|
66
|
+
Returns flat dict of {field: value} pairs to tokenize.
|
|
67
|
+
"""
|
|
68
|
+
found = {}
|
|
69
|
+
|
|
70
|
+
def _walk(o, prefix=""):
|
|
71
|
+
if isinstance(o, dict):
|
|
72
|
+
for k, v in o.items():
|
|
73
|
+
key = k.lower().replace(" ", "_").replace("-", "_")
|
|
74
|
+
if key in (_PII_FIELDS | _PHI_FIELDS | _PCI_FIELDS):
|
|
75
|
+
if isinstance(v, str) and v and not TOKEN_RE.fullmatch(v.strip()):
|
|
76
|
+
found[k] = v
|
|
77
|
+
else:
|
|
78
|
+
_walk(v, prefix=k)
|
|
79
|
+
elif isinstance(o, list):
|
|
80
|
+
for item in o:
|
|
81
|
+
_walk(item, prefix)
|
|
82
|
+
|
|
83
|
+
if isinstance(obj, dict):
|
|
84
|
+
_walk(obj)
|
|
85
|
+
elif isinstance(obj, str):
|
|
86
|
+
# Try JSON parse
|
|
87
|
+
try:
|
|
88
|
+
_walk(json.loads(obj))
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
return found
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tokenize_in_place(obj: Any, token_map: dict) -> Any:
|
|
95
|
+
"""
|
|
96
|
+
Replace real values with tokens throughout a nested object.
|
|
97
|
+
token_map: {real_value: token}
|
|
98
|
+
"""
|
|
99
|
+
if isinstance(obj, str):
|
|
100
|
+
for real, token in token_map.items():
|
|
101
|
+
obj = obj.replace(str(real), token)
|
|
102
|
+
return obj
|
|
103
|
+
elif isinstance(obj, dict):
|
|
104
|
+
return {k: _tokenize_in_place(v, token_map) for k, v in obj.items()}
|
|
105
|
+
elif isinstance(obj, list):
|
|
106
|
+
return [_tokenize_in_place(i, token_map) for i in obj]
|
|
107
|
+
return obj
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class BlindAgentMiddleware:
|
|
111
|
+
"""
|
|
112
|
+
Drop-in middleware that makes any agent framework blind to real data.
|
|
113
|
+
|
|
114
|
+
Works with:
|
|
115
|
+
- LangChain: AgentExecutor, RunnableAgent, Chain
|
|
116
|
+
- CrewAI: Agent, Crew
|
|
117
|
+
- AutoGPT: any object with .run() / .step()
|
|
118
|
+
- Generic: anything with .run() / .invoke() / .chat()
|
|
119
|
+
|
|
120
|
+
Usage:
|
|
121
|
+
# LangChain
|
|
122
|
+
from codeastra import BlindAgentMiddleware
|
|
123
|
+
agent = BlindAgentMiddleware(langchain_executor, api_key="sk-guard-xxx")
|
|
124
|
+
result = agent.invoke({"input": "Schedule appointment for patient"})
|
|
125
|
+
|
|
126
|
+
# CrewAI
|
|
127
|
+
crew = BlindAgentMiddleware(my_crew, api_key="sk-guard-xxx")
|
|
128
|
+
result = crew.run()
|
|
129
|
+
|
|
130
|
+
# With pipeline (multi-agent)
|
|
131
|
+
agent_a = BlindAgentMiddleware(intake_agent, api_key="sk-guard-xxx", agent_id="intake")
|
|
132
|
+
agent_b = BlindAgentMiddleware(scheduling_agent, api_key="sk-guard-xxx", agent_id="scheduling")
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
agent: The underlying agent object to wrap
|
|
136
|
+
api_key: Your Codeastra API key (sk-guard-xxx)
|
|
137
|
+
agent_id: Unique ID for this agent in the pipeline (default: "sdk-agent")
|
|
138
|
+
base_url: Codeastra API base URL (default: https://app.codeastra.dev)
|
|
139
|
+
classification: Default data classification: "pii", "phi", or "pci"
|
|
140
|
+
pipeline_id: Optional pipeline ID for multi-agent tracking
|
|
141
|
+
on_tokenize: Optional callback(field, token) called when data is tokenized
|
|
142
|
+
verbose: Print tokenization events to stdout (default: False)
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
agent: Any,
|
|
148
|
+
api_key: str,
|
|
149
|
+
agent_id: str = "sdk-agent",
|
|
150
|
+
base_url: str = "https://app.codeastra.dev",
|
|
151
|
+
classification: str = "pii",
|
|
152
|
+
pipeline_id: Optional[str] = None,
|
|
153
|
+
on_tokenize: Optional[Callable] = None,
|
|
154
|
+
verbose: bool = False,
|
|
155
|
+
):
|
|
156
|
+
self._agent = agent
|
|
157
|
+
self._client = CodeAstraClient(api_key, base_url, agent_id)
|
|
158
|
+
self._classification = classification
|
|
159
|
+
self._pipeline_id = pipeline_id
|
|
160
|
+
self._on_tokenize = on_tokenize
|
|
161
|
+
self._verbose = verbose
|
|
162
|
+
|
|
163
|
+
# Track tokens minted this session: {field_key: token}
|
|
164
|
+
self._session_tokens: dict = {}
|
|
165
|
+
# Reverse map: {real_value: token}
|
|
166
|
+
self._value_to_token: dict = {}
|
|
167
|
+
|
|
168
|
+
# Patch agent's tool call mechanism
|
|
169
|
+
self._patch_tools()
|
|
170
|
+
|
|
171
|
+
# ── tool patching ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def _patch_tools(self):
|
|
174
|
+
"""
|
|
175
|
+
Intercept tool calls on the underlying agent.
|
|
176
|
+
Supports LangChain tools, CrewAI tools, generic callables.
|
|
177
|
+
"""
|
|
178
|
+
agent = self._agent
|
|
179
|
+
|
|
180
|
+
# LangChain: AgentExecutor has .tools list
|
|
181
|
+
if hasattr(agent, "tools") and isinstance(agent.tools, list):
|
|
182
|
+
for i, tool in enumerate(agent.tools):
|
|
183
|
+
agent.tools[i] = self._wrap_tool(tool)
|
|
184
|
+
if self._verbose:
|
|
185
|
+
print(f"[CodeAstra] Patched {len(agent.tools)} LangChain tools")
|
|
186
|
+
|
|
187
|
+
# LangChain: RunnableAgent / chain with .steps
|
|
188
|
+
if hasattr(agent, "steps"):
|
|
189
|
+
for step in agent.steps:
|
|
190
|
+
if hasattr(step, "tool"):
|
|
191
|
+
step.tool = self._wrap_tool(step.tool)
|
|
192
|
+
|
|
193
|
+
# CrewAI: Agent has .tools
|
|
194
|
+
if hasattr(agent, "agent") and hasattr(agent.agent, "tools"):
|
|
195
|
+
tools = agent.agent.tools
|
|
196
|
+
for i, tool in enumerate(tools):
|
|
197
|
+
tools[i] = self._wrap_tool(tool)
|
|
198
|
+
|
|
199
|
+
def _wrap_tool(self, tool: Any) -> Any:
|
|
200
|
+
"""
|
|
201
|
+
Wrap a single tool so its output is tokenized before the agent sees it.
|
|
202
|
+
Works with LangChain BaseTool, CrewAI tools, and plain callables.
|
|
203
|
+
"""
|
|
204
|
+
# LangChain BaseTool — has ._run and .run
|
|
205
|
+
if hasattr(tool, "_run"):
|
|
206
|
+
original_run = tool._run
|
|
207
|
+
original_arun = getattr(tool, "_arun", None)
|
|
208
|
+
|
|
209
|
+
@functools.wraps(original_run)
|
|
210
|
+
def patched_run(*args, **kwargs):
|
|
211
|
+
result = original_run(*args, **kwargs)
|
|
212
|
+
return self._blind_output(result)
|
|
213
|
+
|
|
214
|
+
tool._run = patched_run
|
|
215
|
+
|
|
216
|
+
if original_arun:
|
|
217
|
+
@functools.wraps(original_arun)
|
|
218
|
+
async def patched_arun(*args, **kwargs):
|
|
219
|
+
result = await original_arun(*args, **kwargs)
|
|
220
|
+
return self._blind_output(result)
|
|
221
|
+
tool._arun = patched_arun
|
|
222
|
+
|
|
223
|
+
return tool
|
|
224
|
+
|
|
225
|
+
# Plain callable
|
|
226
|
+
if callable(tool):
|
|
227
|
+
@functools.wraps(tool)
|
|
228
|
+
def wrapped(*args, **kwargs):
|
|
229
|
+
result = tool(*args, **kwargs)
|
|
230
|
+
return self._blind_output(result)
|
|
231
|
+
return wrapped
|
|
232
|
+
|
|
233
|
+
return tool
|
|
234
|
+
|
|
235
|
+
# ── core blindness logic ──────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
def _blind_output(self, output: Any) -> Any:
|
|
238
|
+
"""
|
|
239
|
+
Given any tool output, tokenize all sensitive fields.
|
|
240
|
+
Returns the same structure with real values replaced by tokens.
|
|
241
|
+
"""
|
|
242
|
+
sensitive = _extract_sensitive(output)
|
|
243
|
+
if not sensitive:
|
|
244
|
+
return output
|
|
245
|
+
|
|
246
|
+
classification = _classify(set(k.lower() for k in sensitive))
|
|
247
|
+
try:
|
|
248
|
+
tokens = self._client.tokenize(sensitive, classification=classification)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
if self._verbose:
|
|
251
|
+
print(f"[CodeAstra] Warning: tokenization failed: {e}")
|
|
252
|
+
return output
|
|
253
|
+
|
|
254
|
+
# Build reverse map for replacement
|
|
255
|
+
for field, token in tokens.items():
|
|
256
|
+
real_val = sensitive.get(field)
|
|
257
|
+
if real_val:
|
|
258
|
+
self._value_to_token[str(real_val)] = token
|
|
259
|
+
self._session_tokens[field] = token
|
|
260
|
+
|
|
261
|
+
if self._on_tokenize:
|
|
262
|
+
for field, token in tokens.items():
|
|
263
|
+
try: self._on_tokenize(field, token)
|
|
264
|
+
except Exception: pass
|
|
265
|
+
|
|
266
|
+
if self._verbose:
|
|
267
|
+
print(f"[CodeAstra] Tokenized {len(tokens)} field(s): {list(tokens.keys())}")
|
|
268
|
+
|
|
269
|
+
return _tokenize_in_place(output, self._value_to_token)
|
|
270
|
+
|
|
271
|
+
async def _ablind_output(self, output: Any) -> Any:
|
|
272
|
+
"""Async version of _blind_output."""
|
|
273
|
+
sensitive = _extract_sensitive(output)
|
|
274
|
+
if not sensitive:
|
|
275
|
+
return output
|
|
276
|
+
|
|
277
|
+
classification = _classify(set(k.lower() for k in sensitive))
|
|
278
|
+
try:
|
|
279
|
+
tokens = await self._client.atokenize(sensitive, classification=classification)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
if self._verbose:
|
|
282
|
+
print(f"[CodeAstra] Warning: async tokenization failed: {e}")
|
|
283
|
+
return output
|
|
284
|
+
|
|
285
|
+
for field, token in tokens.items():
|
|
286
|
+
real_val = sensitive.get(field)
|
|
287
|
+
if real_val:
|
|
288
|
+
self._value_to_token[str(real_val)] = token
|
|
289
|
+
self._session_tokens[field] = token
|
|
290
|
+
|
|
291
|
+
if self._verbose:
|
|
292
|
+
print(f"[CodeAstra] Tokenized {len(tokens)} field(s): {list(tokens.keys())}")
|
|
293
|
+
|
|
294
|
+
return _tokenize_in_place(output, self._value_to_token)
|
|
295
|
+
|
|
296
|
+
# ── pipeline: grant tokens to next agent ─────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def grant_to(
|
|
299
|
+
self,
|
|
300
|
+
next_agent_id: str,
|
|
301
|
+
allowed_actions: list[str] = [],
|
|
302
|
+
purpose: str = None,
|
|
303
|
+
) -> dict:
|
|
304
|
+
"""
|
|
305
|
+
Grant all tokens minted this session to the next agent in the pipeline.
|
|
306
|
+
|
|
307
|
+
agent_a.run(input)
|
|
308
|
+
grant = agent_a.grant_to("scheduling-agent", ["schedule_appointment"])
|
|
309
|
+
# Now scheduling-agent can use agent_a's tokens
|
|
310
|
+
"""
|
|
311
|
+
tokens = list(self._session_tokens.values())
|
|
312
|
+
if not tokens:
|
|
313
|
+
return {"granted": False, "error": "No tokens minted this session"}
|
|
314
|
+
return self._client.grant(
|
|
315
|
+
receiving_agent = next_agent_id,
|
|
316
|
+
tokens = tokens,
|
|
317
|
+
allowed_actions = allowed_actions,
|
|
318
|
+
pipeline_id = self._pipeline_id,
|
|
319
|
+
purpose = purpose,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def agrant_to(
|
|
323
|
+
self,
|
|
324
|
+
next_agent_id: str,
|
|
325
|
+
allowed_actions: list[str] = [],
|
|
326
|
+
) -> dict:
|
|
327
|
+
tokens = list(self._session_tokens.values())
|
|
328
|
+
if not tokens:
|
|
329
|
+
return {"granted": False, "error": "No tokens minted this session"}
|
|
330
|
+
return await self._client.agrant(
|
|
331
|
+
receiving_agent = next_agent_id,
|
|
332
|
+
tokens = tokens,
|
|
333
|
+
allowed_actions = allowed_actions,
|
|
334
|
+
pipeline_id = self._pipeline_id,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# ── execute action with tokens ────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
def execute(self, action_type: str, params: dict) -> dict:
|
|
340
|
+
"""Submit a final action. Tokens in params are resolved by Codeastra."""
|
|
341
|
+
return self._client.execute(action_type, params, self._pipeline_id)
|
|
342
|
+
|
|
343
|
+
async def aexecute(self, action_type: str, params: dict) -> dict:
|
|
344
|
+
return await self._client.aexecute(action_type, params, self._pipeline_id)
|
|
345
|
+
|
|
346
|
+
# ── proxy all agent methods ───────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
def run(self, *args, **kwargs):
|
|
349
|
+
"""Proxy .run() — used by CrewAI, AutoGPT, generic agents."""
|
|
350
|
+
result = self._agent.run(*args, **kwargs)
|
|
351
|
+
return self._blind_output(result)
|
|
352
|
+
|
|
353
|
+
def invoke(self, *args, **kwargs):
|
|
354
|
+
"""Proxy .invoke() — used by LangChain LCEL chains."""
|
|
355
|
+
result = self._agent.invoke(*args, **kwargs)
|
|
356
|
+
if isinstance(result, dict) and "output" in result:
|
|
357
|
+
result["output"] = self._blind_output(result["output"])
|
|
358
|
+
return result
|
|
359
|
+
return self._blind_output(result)
|
|
360
|
+
|
|
361
|
+
def chat(self, *args, **kwargs):
|
|
362
|
+
"""Proxy .chat() — used by various chat-style agents."""
|
|
363
|
+
result = self._agent.chat(*args, **kwargs)
|
|
364
|
+
return self._blind_output(result)
|
|
365
|
+
|
|
366
|
+
async def arun(self, *args, **kwargs):
|
|
367
|
+
result = await self._agent.arun(*args, **kwargs)
|
|
368
|
+
return await self._ablind_output(result)
|
|
369
|
+
|
|
370
|
+
async def ainvoke(self, *args, **kwargs):
|
|
371
|
+
result = await self._agent.ainvoke(*args, **kwargs)
|
|
372
|
+
if isinstance(result, dict) and "output" in result:
|
|
373
|
+
result["output"] = await self._ablind_output(result["output"])
|
|
374
|
+
return result
|
|
375
|
+
return await self._ablind_output(result)
|
|
376
|
+
|
|
377
|
+
# ── session info ──────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def tokens(self) -> dict:
|
|
381
|
+
"""All tokens minted this session. {field: token}"""
|
|
382
|
+
return dict(self._session_tokens)
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def token_count(self) -> int:
|
|
386
|
+
return len(self._session_tokens)
|
|
387
|
+
|
|
388
|
+
def audit(self) -> list:
|
|
389
|
+
"""Get chain of custody for this session's pipeline."""
|
|
390
|
+
return self._client.audit(pipeline_id=self._pipeline_id)
|
|
391
|
+
|
|
392
|
+
# ── pass-through attribute access to underlying agent ────────────────────
|
|
393
|
+
|
|
394
|
+
def __getattr__(self, name: str):
|
|
395
|
+
"""Fall through to the underlying agent for any unpatched attribute."""
|
|
396
|
+
return getattr(self._agent, name)
|
|
397
|
+
|
|
398
|
+
def __repr__(self):
|
|
399
|
+
return (f"BlindAgentMiddleware(agent={type(self._agent).__name__}, "
|
|
400
|
+
f"agent_id={self._client.agent_id!r}, "
|
|
401
|
+
f"tokens_minted={self.token_count})")
|
|
402
|
+
|
|
403
|
+
def close(self):
|
|
404
|
+
self._client.close()
|
|
405
|
+
|
|
406
|
+
async def aclose(self):
|
|
407
|
+
await self._client.aclose()
|
|
408
|
+
|
|
409
|
+
def __enter__(self): return self
|
|
410
|
+
def __exit__(self, *_): self.close()
|
|
411
|
+
async def __aenter__(self): return self
|
|
412
|
+
async def __aexit__(self, *_): await self.aclose()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework-specific wrappers and decorators.
|
|
3
|
+
|
|
4
|
+
blind_tool — decorator for individual LangChain/CrewAI tools
|
|
5
|
+
BlindCrewAIAgent — CrewAI-specific wrapper with crew-level pipeline support
|
|
6
|
+
BlindAutoGPTAgent — AutoGPT-style wrapper
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
from typing import Any, Callable, Optional
|
|
12
|
+
|
|
13
|
+
from .client import CodeAstraClient
|
|
14
|
+
from .middleware import BlindAgentMiddleware, _extract_sensitive, _tokenize_in_place
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── @blind_tool decorator ─────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
def blind_tool(api_key: str, agent_id: str = "sdk-agent",
|
|
20
|
+
base_url: str = "https://app.codeastra.dev",
|
|
21
|
+
classification: str = "pii"):
|
|
22
|
+
"""
|
|
23
|
+
Decorator that makes a single tool function blind.
|
|
24
|
+
Any sensitive data in the return value is tokenized before the agent sees it.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
client = CodeAstraClient(api_key="sk-guard-xxx")
|
|
28
|
+
|
|
29
|
+
@blind_tool(api_key="sk-guard-xxx")
|
|
30
|
+
def get_patient_record(patient_id: str) -> dict:
|
|
31
|
+
return db.get_patient(patient_id)
|
|
32
|
+
# Agent receives tokens, not real patient data
|
|
33
|
+
|
|
34
|
+
# As a LangChain tool:
|
|
35
|
+
from langchain.tools import tool
|
|
36
|
+
|
|
37
|
+
@tool
|
|
38
|
+
@blind_tool(api_key="sk-guard-xxx", classification="phi")
|
|
39
|
+
def lookup_patient(patient_id: str) -> str:
|
|
40
|
+
return fetch_from_ehr(patient_id)
|
|
41
|
+
"""
|
|
42
|
+
_client = CodeAstraClient(api_key, base_url, agent_id)
|
|
43
|
+
|
|
44
|
+
def decorator(fn: Callable) -> Callable:
|
|
45
|
+
@functools.wraps(fn)
|
|
46
|
+
def sync_wrapper(*args, **kwargs):
|
|
47
|
+
result = fn(*args, **kwargs)
|
|
48
|
+
sensitive = _extract_sensitive(result)
|
|
49
|
+
if not sensitive:
|
|
50
|
+
return result
|
|
51
|
+
tokens = _client.tokenize(sensitive, classification=classification)
|
|
52
|
+
val_to_tok = {str(sensitive[k]): v for k, v in tokens.items()}
|
|
53
|
+
return _tokenize_in_place(result, val_to_tok)
|
|
54
|
+
|
|
55
|
+
@functools.wraps(fn)
|
|
56
|
+
async def async_wrapper(*args, **kwargs):
|
|
57
|
+
result = await fn(*args, **kwargs)
|
|
58
|
+
sensitive = _extract_sensitive(result)
|
|
59
|
+
if not sensitive:
|
|
60
|
+
return result
|
|
61
|
+
tokens = await _client.atokenize(sensitive, classification=classification)
|
|
62
|
+
val_to_tok = {str(sensitive[k]): v for k, v in tokens.items()}
|
|
63
|
+
return _tokenize_in_place(result, val_to_tok)
|
|
64
|
+
|
|
65
|
+
import asyncio
|
|
66
|
+
if asyncio.iscoroutinefunction(fn):
|
|
67
|
+
return async_wrapper
|
|
68
|
+
return sync_wrapper
|
|
69
|
+
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── BlindCrewAIAgent ──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
class BlindCrewAIAgent(BlindAgentMiddleware):
|
|
76
|
+
"""
|
|
77
|
+
CrewAI-specific blind wrapper.
|
|
78
|
+
|
|
79
|
+
Usage:
|
|
80
|
+
from crewai import Agent, Task, Crew
|
|
81
|
+
from codeastra import BlindCrewAIAgent
|
|
82
|
+
|
|
83
|
+
intake_agent = Agent(role="intake", tools=[ehr_tool, ...])
|
|
84
|
+
blind_intake = BlindCrewAIAgent(
|
|
85
|
+
intake_agent,
|
|
86
|
+
api_key="sk-guard-xxx",
|
|
87
|
+
agent_id="intake-agent",
|
|
88
|
+
pipeline_id="patient_intake_001",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# In your Crew, use blind_intake instead of intake_agent
|
|
92
|
+
# All tool outputs are tokenized. Agent reasons on tokens only.
|
|
93
|
+
|
|
94
|
+
# Pass tokens to next agent:
|
|
95
|
+
blind_intake.grant_to("scheduling-agent", ["schedule_appointment"])
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def kickoff(self, *args, **kwargs):
|
|
99
|
+
"""Proxy CrewAI Crew.kickoff()"""
|
|
100
|
+
if hasattr(self._agent, "kickoff"):
|
|
101
|
+
result = self._agent.kickoff(*args, **kwargs)
|
|
102
|
+
return self._blind_output(result)
|
|
103
|
+
return self.run(*args, **kwargs)
|
|
104
|
+
|
|
105
|
+
async def akickoff(self, *args, **kwargs):
|
|
106
|
+
if hasattr(self._agent, "akickoff"):
|
|
107
|
+
result = await self._agent.akickoff(*args, **kwargs)
|
|
108
|
+
return await self._ablind_output(result)
|
|
109
|
+
return await self.arun(*args, **kwargs)
|
|
110
|
+
|
|
111
|
+
def execute_task(self, task: Any, *args, **kwargs):
|
|
112
|
+
"""Intercept CrewAI task execution."""
|
|
113
|
+
if hasattr(self._agent, "execute_task"):
|
|
114
|
+
result = self._agent.execute_task(task, *args, **kwargs)
|
|
115
|
+
return self._blind_output(result)
|
|
116
|
+
return self.run(*args, **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── BlindAutoGPTAgent ─────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
class BlindAutoGPTAgent(BlindAgentMiddleware):
|
|
122
|
+
"""
|
|
123
|
+
AutoGPT-style blind wrapper.
|
|
124
|
+
Intercepts .step() and .run() calls.
|
|
125
|
+
|
|
126
|
+
Usage:
|
|
127
|
+
from codeastra import BlindAutoGPTAgent
|
|
128
|
+
|
|
129
|
+
agent = BlindAutoGPTAgent(
|
|
130
|
+
your_autogpt_agent,
|
|
131
|
+
api_key="sk-guard-xxx",
|
|
132
|
+
agent_id="autogpt-agent",
|
|
133
|
+
)
|
|
134
|
+
while not agent.is_done():
|
|
135
|
+
agent.step()
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def step(self, *args, **kwargs):
|
|
139
|
+
"""Intercept single step execution."""
|
|
140
|
+
if hasattr(self._agent, "step"):
|
|
141
|
+
result = self._agent.step(*args, **kwargs)
|
|
142
|
+
return self._blind_output(result)
|
|
143
|
+
raise AttributeError("Underlying agent has no .step() method")
|
|
144
|
+
|
|
145
|
+
async def astep(self, *args, **kwargs):
|
|
146
|
+
if hasattr(self._agent, "astep"):
|
|
147
|
+
result = await self._agent.astep(*args, **kwargs)
|
|
148
|
+
return await self._ablind_output(result)
|
|
149
|
+
raise AttributeError("Underlying agent has no .astep() method")
|
|
150
|
+
|
|
151
|
+
def is_done(self) -> bool:
|
|
152
|
+
if hasattr(self._agent, "is_done"):
|
|
153
|
+
return self._agent.is_done()
|
|
154
|
+
return False
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codeastra
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Blind Agent SDK — drop-in middleware for LangChain, CrewAI, AutoGPT. Two lines makes any agent blind to real data.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://codeastra.dev
|
|
7
|
+
Project-URL: Documentation, https://docs.codeastra.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/codeastra/codeastra-python
|
|
9
|
+
Keywords: ai,agents,langchain,crewai,privacy,hipaa,security,tokenization
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Provides-Extra: langchain
|
|
23
|
+
Requires-Dist: langchain>=0.2.0; extra == "langchain"
|
|
24
|
+
Requires-Dist: langchain-core>=0.2.0; extra == "langchain"
|
|
25
|
+
Provides-Extra: crewai
|
|
26
|
+
Requires-Dist: crewai>=0.30.0; extra == "crewai"
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: langchain>=0.2.0; extra == "all"
|
|
29
|
+
Requires-Dist: langchain-core>=0.2.0; extra == "all"
|
|
30
|
+
Requires-Dist: crewai>=0.30.0; extra == "all"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
codeastra/__init__.py
|
|
3
|
+
codeastra/client.py
|
|
4
|
+
codeastra/middleware.py
|
|
5
|
+
codeastra/wrappers.py
|
|
6
|
+
codeastra.egg-info/PKG-INFO
|
|
7
|
+
codeastra.egg-info/SOURCES.txt
|
|
8
|
+
codeastra.egg-info/dependency_links.txt
|
|
9
|
+
codeastra.egg-info/requires.txt
|
|
10
|
+
codeastra.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codeastra
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codeastra"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Blind Agent SDK — drop-in middleware for LangChain, CrewAI, AutoGPT. Two lines makes any agent blind to real data."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["ai", "agents", "langchain", "crewai", "privacy", "hipaa", "security", "tokenization"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Security",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.27.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
langchain = ["langchain>=0.2.0", "langchain-core>=0.2.0"]
|
|
31
|
+
crewai = ["crewai>=0.30.0"]
|
|
32
|
+
all = ["langchain>=0.2.0", "langchain-core>=0.2.0", "crewai>=0.30.0"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://codeastra.dev"
|
|
36
|
+
Documentation = "https://docs.codeastra.dev"
|
|
37
|
+
Repository = "https://github.com/codeastra/codeastra-python"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["."]
|
|
41
|
+
include = ["codeastra*"]
|