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.
@@ -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,13 @@
1
+ httpx>=0.27.0
2
+
3
+ [all]
4
+ langchain>=0.2.0
5
+ langchain-core>=0.2.0
6
+ crewai>=0.30.0
7
+
8
+ [crewai]
9
+ crewai>=0.30.0
10
+
11
+ [langchain]
12
+ langchain>=0.2.0
13
+ langchain-core>=0.2.0
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+