datacules-agent-identity 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent_identity — Python SDK for the agent-identity credential routing sidecar
|
|
3
|
+
|
|
4
|
+
Installation:
|
|
5
|
+
pip install datacules-agent-identity
|
|
6
|
+
# or directly:
|
|
7
|
+
pip install git+https://github.com/hvrcharon1/agent-identity.git#subdirectory=packages/python-sdk
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from agent_identity import AgentIdentityClient
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
client = AgentIdentityClient(base_url="http://localhost:3001")
|
|
14
|
+
|
|
15
|
+
resolved = client.resolve({
|
|
16
|
+
"userId": "user-abc",
|
|
17
|
+
"resourceId": "crm-db",
|
|
18
|
+
"resourceKind": "shared",
|
|
19
|
+
"provider": "anthropic",
|
|
20
|
+
"model": "claude-sonnet-4-20250514",
|
|
21
|
+
"action": "read",
|
|
22
|
+
"traceId": "trace-xyz",
|
|
23
|
+
"requestedAt": datetime.now(timezone.utc).isoformat(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
pair = client.resolve_migration({
|
|
27
|
+
"migrationId": "migration-2026-q2",
|
|
28
|
+
"phase": "load",
|
|
29
|
+
"sourceResourceId": "crm-postgres-prod",
|
|
30
|
+
"targetResourceId": "crm-postgres-v2",
|
|
31
|
+
"userId": "svc-migration-bot",
|
|
32
|
+
"provider": "anthropic",
|
|
33
|
+
"model": "claude-sonnet-4-20250514",
|
|
34
|
+
"traceId": "trace-abc123",
|
|
35
|
+
"dryRun": False,
|
|
36
|
+
})
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
from datetime import datetime, timezone
|
|
43
|
+
from typing import Any, Dict, Literal, Optional, TypedDict
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
import urllib.request as _urllib
|
|
47
|
+
except ImportError: # pragma: no cover
|
|
48
|
+
raise RuntimeError("agent-identity requires Python 3.8+")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ─── Type aliases ────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
ResourceKind = Literal["shared", "personal"]
|
|
54
|
+
SupportedProvider = Literal["openai", "anthropic", "gemini", "mistral", "local"]
|
|
55
|
+
MigrationPhase = Literal["dry-run", "extract", "transform", "load", "verify", "rollback"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AgentRequestContext(TypedDict, total=False):
|
|
59
|
+
userId: str # required
|
|
60
|
+
resourceId: str # required
|
|
61
|
+
resourceKind: ResourceKind # required
|
|
62
|
+
provider: SupportedProvider # required
|
|
63
|
+
model: str # required
|
|
64
|
+
action: str # required
|
|
65
|
+
traceId: str # required
|
|
66
|
+
requestedAt: str # required (ISO 8601)
|
|
67
|
+
sessionId: str # optional
|
|
68
|
+
parentTraceId: str # optional
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MigrateResolveRequest(TypedDict, total=False):
|
|
72
|
+
migrationId: str # required
|
|
73
|
+
phase: MigrationPhase # required
|
|
74
|
+
sourceResourceId: str # required
|
|
75
|
+
targetResourceId: str # required
|
|
76
|
+
userId: str # required
|
|
77
|
+
provider: SupportedProvider # required
|
|
78
|
+
model: str # required
|
|
79
|
+
traceId: str # required
|
|
80
|
+
dryRun: bool # optional, default False
|
|
81
|
+
batchIndex: int # optional
|
|
82
|
+
totalBatches: int # optional
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ResolveResponse(TypedDict):
|
|
86
|
+
ok: bool
|
|
87
|
+
resolvedFor: str
|
|
88
|
+
expiresAt: Optional[str]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class MigrateResolveResponse(TypedDict):
|
|
92
|
+
migrationId: str
|
|
93
|
+
phase: MigrationPhase
|
|
94
|
+
sourceResolvedFor: str
|
|
95
|
+
targetResolvedFor: str
|
|
96
|
+
dryRun: bool
|
|
97
|
+
expiresAt: Optional[str]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ─── Exceptions ───────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
class AgentIdentityError(Exception):
|
|
103
|
+
"""Base exception for all agent-identity SDK errors."""
|
|
104
|
+
def __init__(self, message: str, status_code: Optional[int] = None) -> None:
|
|
105
|
+
super().__init__(message)
|
|
106
|
+
self.status_code = status_code
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class NoCredentialError(AgentIdentityError):
|
|
110
|
+
"""Raised when the server returns 403 — no routing rule matched."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ValidationError(AgentIdentityError):
|
|
114
|
+
"""Raised when the server returns 400 — invalid request body."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ─── Client ────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
class AgentIdentityClient:
|
|
120
|
+
"""
|
|
121
|
+
Thin HTTP client for the agent-identity sidecar.
|
|
122
|
+
No Node.js required — pure Python 3.8+ stdlib.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
base_url: URL of the running sidecar, e.g. 'http://localhost:3001'
|
|
126
|
+
timeout: Request timeout in seconds (default 10)
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, base_url: str = "http://localhost:3001", timeout: int = 10) -> None:
|
|
130
|
+
self.base_url = base_url.rstrip("/")
|
|
131
|
+
self.timeout = timeout
|
|
132
|
+
|
|
133
|
+
# ─── Public API ────────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
def resolve(self, ctx: AgentRequestContext) -> ResolveResponse:
|
|
136
|
+
"""
|
|
137
|
+
Resolve the best credential for a single agent request.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
ctx: AgentRequestContext dict — all required fields must be present.
|
|
141
|
+
If 'requestedAt' is omitted, the current UTC time is used.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
ResolveResponse with resolvedFor and optional expiresAt.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValidationError: Server returned 400 (missing/invalid field).
|
|
148
|
+
NoCredentialError: Server returned 403 (no rule matched).
|
|
149
|
+
AgentIdentityError: Any other HTTP error.
|
|
150
|
+
"""
|
|
151
|
+
if "requestedAt" not in ctx:
|
|
152
|
+
ctx = {**ctx, "requestedAt": datetime.now(timezone.utc).isoformat()} # type: ignore[assignment]
|
|
153
|
+
return self._post("/api/resolve", ctx) # type: ignore[return-value]
|
|
154
|
+
|
|
155
|
+
def resolve_migration(
|
|
156
|
+
self, request: MigrateResolveRequest
|
|
157
|
+
) -> MigrateResolveResponse:
|
|
158
|
+
"""
|
|
159
|
+
Resolve source + target credentials for a migration phase.
|
|
160
|
+
|
|
161
|
+
Call once at the start of each phase. The returned expiresAt lets
|
|
162
|
+
the agent decide when to re-call before the batch loop ends.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
request: MigrateResolveRequest dict.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
MigrateResolveResponse with sourceResolvedFor, targetResolvedFor,
|
|
169
|
+
and optional expiresAt (earliest of the two credentials).
|
|
170
|
+
"""
|
|
171
|
+
if "dryRun" not in request:
|
|
172
|
+
request = {**request, "dryRun": False} # type: ignore[assignment]
|
|
173
|
+
return self._post("/api/migrate/resolve", request) # type: ignore[return-value]
|
|
174
|
+
|
|
175
|
+
def health(self) -> bool:
|
|
176
|
+
"""Returns True if the sidecar is reachable and healthy."""
|
|
177
|
+
try:
|
|
178
|
+
self._post("/api/health", {}, method="GET")
|
|
179
|
+
return True
|
|
180
|
+
except AgentIdentityError:
|
|
181
|
+
return False
|
|
182
|
+
except Exception: # noqa: BLE001
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
# ─── Internal ──────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def _post(self, path: str, body: Dict[str, Any], method: str = "POST") -> Any:
|
|
188
|
+
url = f"{self.base_url}{path}"
|
|
189
|
+
data = json.dumps(body).encode("utf-8")
|
|
190
|
+
req = _urllib.Request(
|
|
191
|
+
url,
|
|
192
|
+
data=data if method == "POST" else None,
|
|
193
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
194
|
+
method=method,
|
|
195
|
+
)
|
|
196
|
+
try:
|
|
197
|
+
with _urllib.urlopen(req, timeout=self.timeout) as resp:
|
|
198
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
199
|
+
except _urllib.HTTPError as e:
|
|
200
|
+
status = e.code
|
|
201
|
+
try:
|
|
202
|
+
error_body = json.loads(e.read().decode("utf-8"))
|
|
203
|
+
message = error_body.get("error", str(e))
|
|
204
|
+
except Exception: # noqa: BLE001
|
|
205
|
+
message = str(e)
|
|
206
|
+
|
|
207
|
+
if status == 400:
|
|
208
|
+
raise ValidationError(message, status_code=status) from e
|
|
209
|
+
if status == 403:
|
|
210
|
+
raise NoCredentialError(message, status_code=status) from e
|
|
211
|
+
raise AgentIdentityError(message, status_code=status) from e
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise AgentIdentityError(f"Request failed: {e}") from e
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datacules-agent-identity
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: Python SDK for the agent-identity credential routing sidecar — by Datacules LLC
|
|
5
|
+
Author-email: Datacules LLC <harshalrasal792@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hvrcharon1/agent-identity
|
|
8
|
+
Project-URL: Repository, https://github.com/hvrcharon1/agent-identity
|
|
9
|
+
Project-URL: Documentation, https://github.com/hvrcharon1/agent-identity/tree/main/packages/python-sdk
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# agent-identity Python SDK
|
|
17
|
+
|
|
18
|
+
Pure-Python client for the `agent-identity` sidecar. No Node.js required.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install agent-identity
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from agent_identity import AgentIdentityClient
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
|
|
32
|
+
client = AgentIdentityClient(base_url="http://localhost:3001")
|
|
33
|
+
|
|
34
|
+
# Resolve a credential for a single agent request
|
|
35
|
+
resolved = client.resolve({
|
|
36
|
+
"userId": "user-abc",
|
|
37
|
+
"resourceId": "crm-db",
|
|
38
|
+
"resourceKind": "shared",
|
|
39
|
+
"provider": "anthropic",
|
|
40
|
+
"model": "claude-sonnet-4-20250514",
|
|
41
|
+
"action": "read",
|
|
42
|
+
"traceId": "trace-xyz",
|
|
43
|
+
"requestedAt": datetime.now(timezone.utc).isoformat(),
|
|
44
|
+
})
|
|
45
|
+
print(resolved["resolvedFor"]) # 'service' or userId
|
|
46
|
+
|
|
47
|
+
# Resolve source + target credentials for a migration phase
|
|
48
|
+
pair = client.resolve_migration({
|
|
49
|
+
"migrationId": "migration-2026-q2",
|
|
50
|
+
"phase": "load",
|
|
51
|
+
"sourceResourceId": "crm-postgres-prod",
|
|
52
|
+
"targetResourceId": "crm-postgres-v2",
|
|
53
|
+
"userId": "svc-migration-bot",
|
|
54
|
+
"provider": "anthropic",
|
|
55
|
+
"model": "claude-sonnet-4-20250514",
|
|
56
|
+
"traceId": "trace-abc123",
|
|
57
|
+
"dryRun": False,
|
|
58
|
+
})
|
|
59
|
+
print(pair["expiresAt"]) # ISO 8601 or None
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## LangChain integration
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from langchain_anthropic import ChatAnthropic
|
|
66
|
+
from agent_identity import AgentIdentityClient
|
|
67
|
+
|
|
68
|
+
client = AgentIdentityClient()
|
|
69
|
+
resolved = client.resolve({...})
|
|
70
|
+
|
|
71
|
+
# resolved["resolvedFor"] is safe to log; the raw API key stays on the server
|
|
72
|
+
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
|
|
73
|
+
# The sidecar injects the API key server-side when you call /api/resolve
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Error handling
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from agent_identity import AgentIdentityClient, NoCredentialError, ValidationError
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = client.resolve(ctx)
|
|
83
|
+
except NoCredentialError:
|
|
84
|
+
# 403 — no routing rule matched this context
|
|
85
|
+
...
|
|
86
|
+
except ValidationError as e:
|
|
87
|
+
# 400 — bad request body
|
|
88
|
+
print(e)
|
|
89
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
agent_identity/__init__.py,sha256=dLLVEhHdTP2GG_fMZRcE1sMFzm-k_3n9h6DdxbT7moY,8178
|
|
2
|
+
datacules_agent_identity-0.3.3.dist-info/METADATA,sha256=p9e9yFx65vGiRmkZzZksRPfJri4KgiU5fVsCDcIsRYY,2531
|
|
3
|
+
datacules_agent_identity-0.3.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
datacules_agent_identity-0.3.3.dist-info/top_level.txt,sha256=KzARZw8H4t-44rbjTSmhOC70sSualwA5fjSPZK0Jbyo,15
|
|
5
|
+
datacules_agent_identity-0.3.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent_identity
|