nxtid-sdk 0.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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: nxtid-sdk
3
+ Version: 0.1.0
4
+ Summary: nxtID Python SDK — AIT-gated AI agent tool permissions
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
@@ -0,0 +1,319 @@
1
+ # nxtID Python SDK
2
+
3
+ Python SDK for integrating **MetaKeep wallet**, **AuthID biometric IDV**, and **nxtlinq AIT on-chain permissions** into AI agents.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install nxtid-sdk
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ from nxtid_sdk import NxtidSDK
19
+
20
+ sdk = NxtidSDK(
21
+ api_key="your-api-key",
22
+ api_secret="your-api-secret",
23
+ env="production", # "production" (default) or "staging"
24
+ service_id="your-service-id", # required for log_chat
25
+ )
26
+
27
+ # 1. Register your agent's tools on startup
28
+ await sdk.register_tools([
29
+ {"name": "get_account_balance", "description": "Read-only finance access"},
30
+ {"name": "transfer_funds", "description": "Move money between accounts"},
31
+ ])
32
+
33
+ # 2. Connect wallet by email
34
+ wallet = await sdk.connect_wallet(email="user@example.com")
35
+ # → WalletResult(wallet_address="0x...", email="user@example.com")
36
+
37
+ # 3. Start AuthID identity verification
38
+ verify = await sdk.start_verification(wallet.wallet_address)
39
+ # → VerifyStartResult(flow="login"|"onboarding"|"choose_document", widget_url="...", ...)
40
+
41
+ # 4. After user completes the widget
42
+ result = await sdk.complete_verification(
43
+ wallet.wallet_address,
44
+ operation_id=verify.operation_id, # for onboarding flow
45
+ transaction_id=verify.transaction_id, # for login flow
46
+ )
47
+ # → VerifyCompleteResult(is_verified=True)
48
+
49
+ # 5. Check admin-granted permissions
50
+ perms = await sdk.get_permissions(wallet.wallet_address)
51
+ if perms.has_permission("transfer_funds"):
52
+ ...
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Environments
58
+
59
+ | env | AIT Service | Dashboard |
60
+ |---------------|----------------------------------------|-------------------------------------|
61
+ | `production` | `https://ait-service.nxtlinq.ai` | `https://dashboard.nxtlinq.ai` |
62
+ | `staging` | `https://staging-ait-service.nxtlinq.ai` | `https://dashboard-staging.nxtlinq.ai` |
63
+
64
+ ---
65
+
66
+ ## API Reference
67
+
68
+ ### `NxtidSDK`
69
+
70
+ ```python
71
+ NxtidSDK(
72
+ api_key: str,
73
+ api_secret: str,
74
+ env: str = "production",
75
+ service_id: Optional[str] = None,
76
+ )
77
+ ```
78
+
79
+ Credentials (`api_key`, `api_secret`, `service_id`) are obtained from nxtID Dashboard → **Services → Register Third-party Agent**.
80
+
81
+ ---
82
+
83
+ #### `connect_wallet(email) → WalletResult`
84
+
85
+ Gets or creates a MetaKeep non-custodial wallet for the given email address.
86
+
87
+ ```python
88
+ wallet = await sdk.connect_wallet(email="user@example.com")
89
+ wallet.wallet_address # "0xABCDEF..."
90
+ wallet.email # "user@example.com"
91
+ ```
92
+
93
+ ---
94
+
95
+ #### `start_verification(wallet_address, id_type=None) → VerifyStartResult`
96
+
97
+ Starts an AuthID identity verification session.
98
+
99
+ | `flow` | Condition |
100
+ |---------------------|------------------------------------------------|
101
+ | `"login"` | Returning user (face scan only) |
102
+ | `"onboarding"` | New user with `id_type` provided |
103
+ | `"choose_document"` | New user without `id_type` (user selects type) |
104
+
105
+ `id_type` is the AuthID document type code (e.g. `"40"` for passport).
106
+
107
+ ```python
108
+ result = await sdk.start_verification(wallet.wallet_address, id_type="40")
109
+ result.flow # "onboarding"
110
+ result.widget_url # URL to open in browser/iframe
111
+ result.operation_id # pass to complete_verification (onboarding)
112
+ result.transaction_id # pass to complete_verification (login)
113
+ ```
114
+
115
+ ---
116
+
117
+ #### `complete_verification(wallet_address, operation_id=None, transaction_id=None) → VerifyCompleteResult`
118
+
119
+ Finalises the verification session after the user completes the widget. Polls AuthID for up to 60 s.
120
+
121
+ ```python
122
+ done = await sdk.complete_verification(
123
+ wallet.wallet_address,
124
+ operation_id=result.operation_id,
125
+ )
126
+ done.is_verified # True | False
127
+ ```
128
+
129
+ ---
130
+
131
+ #### `get_permissions(wallet_address) → PermissionsResult`
132
+
133
+ Queries the AIT on-chain record for this wallet to determine which tools the admin has granted.
134
+
135
+ ```python
136
+ perms = await sdk.get_permissions(wallet.wallet_address)
137
+ perms.allowed # True if any permission grant exists
138
+ perms.allowed_tools # ["transfer_funds", "get_account_balance", ...]
139
+ perms.denied_tools # tools explicitly blocked
140
+ perms.reason # optional explanation from nxtlinq
141
+
142
+ perms.has_permission("transfer_funds") # True | False
143
+ ```
144
+
145
+ ---
146
+
147
+ #### `get_admin_granted_tools(wallet_address) → list[str]`
148
+
149
+ Returns the list of tools the admin has pre-approved for this user to self-service request. Empty list means the admin hasn't granted any tools yet.
150
+
151
+ ```python
152
+ tools = await sdk.get_admin_granted_tools(wallet.wallet_address)
153
+ # ["calculate", "get_company_info", "get_account_balance"]
154
+ ```
155
+
156
+ ---
157
+
158
+ #### `set_permissions(wallet_address, permissions, granted_by=None) → dict`
159
+
160
+ Writes an AIT on-chain with the given tool list for `wallet_address`. Used for self-service permission requests (user selects a subset of admin-granted tools).
161
+
162
+ ```python
163
+ await sdk.set_permissions(
164
+ wallet_address="0x...",
165
+ permissions=["calculate", "get_company_info"],
166
+ granted_by="user@example.com",
167
+ )
168
+ ```
169
+
170
+ ---
171
+
172
+ #### `register_tools(tools) → int`
173
+
174
+ Registers the agent's tool catalogue with nxtID so admins can grant permissions per tool. Call once at agent startup.
175
+
176
+ ```python
177
+ count = await sdk.register_tools([
178
+ {"name": "transfer_funds", "description": "Move money between accounts"},
179
+ {"name": "get_account_balance", "description": "Read-only finance access"},
180
+ ])
181
+ # count = 2
182
+ ```
183
+
184
+ ---
185
+
186
+ #### `log_chat(...) → None`
187
+
188
+ Sends one conversation turn to nxtID Dashboard for monitoring. Fails silently — logging is non-fatal. Requires `service_id` at SDK init time.
189
+
190
+ ```python
191
+ await sdk.log_chat(
192
+ session_id="abc123",
193
+ user_message="What's my balance?",
194
+ assistant_message="Your balance is $500.",
195
+ external_user_id="user@example.com",
196
+ user_name="Alice",
197
+ model="meta/llama-3.3-70b-instruct",
198
+ has_tool_call=True,
199
+ tool_name="get_account_balance",
200
+ turn_index=0,
201
+ )
202
+ ```
203
+
204
+ ---
205
+
206
+ ### `EntraAuth`
207
+
208
+ Optional module for adding Microsoft Entra SSO login to a FastAPI agent. When enabled, mounts two routes onto the host app that the browser MSAL popup calls.
209
+
210
+ ```python
211
+ from nxtid_sdk import EntraAuth
212
+
213
+ entra = EntraAuth(
214
+ client_id=os.getenv("AZURE_AD_CLIENT_ID"),
215
+ tenant_id=os.getenv("AZURE_AD_TENANT_ID"),
216
+ get_session=get_or_create_session, # callable: session_id → Session object
217
+ )
218
+ entra.mount(app) # app is a FastAPI instance
219
+ ```
220
+
221
+ If `client_id` or `tenant_id` is empty/`None`, `entra.enabled` returns `False` and the routes respond accordingly — no error is raised.
222
+
223
+ **Routes mounted:**
224
+
225
+ | Method | Path | Description |
226
+ |--------|---------------------|--------------------------------------------------|
227
+ | `GET` | `/auth/entra/config`| Returns `{ enabled, client_id, tenant_id }` for MSAL initialisation |
228
+ | `POST` | `/auth/entra` | Receives MSAL token claims, writes session state |
229
+
230
+ **POST `/auth/entra` body:**
231
+
232
+ ```json
233
+ {
234
+ "session_id": "string",
235
+ "name": "string",
236
+ "email": "string",
237
+ "tenant_id": "string (optional)",
238
+ "roles": ["string"]
239
+ }
240
+ ```
241
+
242
+ The `get_session` callable receives a `session_id` string and must return a session object with writable attributes: `entra_authenticated`, `entra_name`, `entra_email`, `entra_tenant_id`, `entra_roles`.
243
+
244
+ ---
245
+
246
+ ### `ToolCallBlockedError`
247
+
248
+ Exception raised to block a tool execution before it reaches the LLM tool handler.
249
+
250
+ ```python
251
+ from nxtid_sdk import ToolCallBlockedError
252
+
253
+ try:
254
+ await guard_tool_call(tool_name, session_id)
255
+ except ToolCallBlockedError as exc:
256
+ exc.reason # "verification_required" | "permission_denied"
257
+ exc.message # user-facing explanation
258
+ ```
259
+
260
+ **Factory method:**
261
+
262
+ ```python
263
+ raise ToolCallBlockedError.permission_denied(
264
+ tool_name="transfer_funds",
265
+ allowed_tools=["calculate", "get_company_info"],
266
+ )
267
+ # message: "You don't have permission to use 'transfer_funds'.
268
+ # Your current permissions: [calculate, get_company_info].
269
+ # Contact your administrator."
270
+ ```
271
+
272
+ ---
273
+
274
+ ### Return Types
275
+
276
+ | Class | Fields |
277
+ |----------------------|------------------------------------------------------------------------|
278
+ | `WalletResult` | `wallet_address: str`, `email: str` |
279
+ | `VerifyStartResult` | `flow: str`, `widget_url: str\|None`, `operation_id: str\|None`, `transaction_id: str\|None` |
280
+ | `VerifyCompleteResult` | `is_verified: bool` |
281
+ | `PermissionsResult` | `allowed: bool`, `allowed_tools: list[str]`, `denied_tools: list[str]`, `reason: str\|None`, `has_permission(tool_name) → bool` |
282
+
283
+ ---
284
+
285
+ ## Typical Agent Integration Pattern
286
+
287
+ ```python
288
+ # main.py (FastAPI)
289
+ from nxtid_sdk import NxtidSDK, EntraAuth, ToolCallBlockedError
290
+
291
+ sdk = NxtidSDK(api_key=..., api_secret=..., env="staging", service_id=...)
292
+
293
+ entra = EntraAuth(
294
+ client_id=os.getenv("AZURE_AD_CLIENT_ID"),
295
+ tenant_id=os.getenv("AZURE_AD_TENANT_ID"),
296
+ get_session=get_or_create_session,
297
+ )
298
+ entra.mount(app)
299
+
300
+ @app.on_event("startup")
301
+ async def startup():
302
+ await sdk.register_tools(TOOL_DEFINITIONS)
303
+
304
+ # In your tool dispatcher:
305
+ async def guard_tool_call(tool_name: str, session_id: str) -> None:
306
+ ctx = get_identity_context(session_id)
307
+ if not ctx.is_verified:
308
+ raise ToolCallBlockedError("verification_required", "Complete identity verification first.")
309
+ if tool_name not in ctx.allowed_tools:
310
+ raise ToolCallBlockedError.permission_denied(tool_name, ctx.allowed_tools)
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Requirements
316
+
317
+ - Python ≥ 3.10
318
+ - `httpx >= 0.27`
319
+ - FastAPI (only required for `EntraAuth`)
@@ -0,0 +1,5 @@
1
+ from .client import NxtidSDK
2
+ from .entra import EntraAuth
3
+ from .exceptions import ToolCallBlockedError
4
+
5
+ __all__ = ["NxtidSDK", "EntraAuth", "ToolCallBlockedError"]
@@ -0,0 +1,355 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+
6
+ import httpx
7
+
8
+
9
+ def _raise_for_status(resp: httpx.Response) -> None:
10
+ """Like raise_for_status() but includes the response body in the exception message."""
11
+ if resp.is_error:
12
+ try:
13
+ detail = resp.json()
14
+ except Exception:
15
+ detail = resp.text
16
+ raise httpx.HTTPStatusError(
17
+ f"Client error '{resp.status_code} {resp.reason_phrase}' for url '{resp.url}' — {detail}",
18
+ request=resp.request,
19
+ response=resp,
20
+ )
21
+
22
+
23
+ @dataclass
24
+ class WalletResult:
25
+ wallet_address: str
26
+ email: str
27
+
28
+
29
+ @dataclass
30
+ class VerifyStartResult:
31
+ flow: str # 'onboarding' | 'login' | 'choose_document'
32
+ widget_url: Optional[str] = None
33
+ operation_id: Optional[str] = None
34
+ transaction_id: Optional[str] = None
35
+
36
+
37
+ @dataclass
38
+ class VerifyCompleteResult:
39
+ is_verified: bool
40
+
41
+
42
+ @dataclass
43
+ class PermissionsResult:
44
+ allowed: bool
45
+ allowed_tools: list[str] = field(default_factory=list)
46
+ denied_tools: list[str] = field(default_factory=list)
47
+ reason: Optional[str] = None
48
+
49
+ def has_permission(self, tool_name: str) -> bool:
50
+ if not self.allowed:
51
+ return False
52
+ if not self.allowed_tools or "ALL" in self.allowed_tools:
53
+ return True
54
+ return tool_name in self.allowed_tools
55
+
56
+
57
+ _ENV_URLS: dict[str, dict[str, str]] = {
58
+ "production": {
59
+ "ait": "https://ait-service.nxtlinq.ai",
60
+ "dashboard": "https://dashboard.nxtlinq.ai",
61
+ },
62
+ "staging": {
63
+ "ait": "https://staging-ait-service.nxtlinq.ai",
64
+ "dashboard": "https://dashboard-staging.nxtlinq.ai",
65
+ },
66
+ }
67
+
68
+
69
+ class NxtidSDK:
70
+ """
71
+ nxtID Python SDK.
72
+
73
+ Provides wallet connection, AuthID identity verification, and AIT
74
+ permission checks.
75
+
76
+ Usage::
77
+
78
+ sdk = NxtidSDK(
79
+ api_key="...",
80
+ api_secret="...",
81
+ env="production", # "production" (default) or "staging"
82
+ service_id="your-service-id", # optional, for log_chat
83
+ )
84
+
85
+ # Register tools on startup
86
+ await sdk.register_tools([{"name": "transfer_funds", "description": "..."}])
87
+
88
+ # Connect wallet
89
+ wallet = await sdk.connect_wallet(email="user@example.com")
90
+
91
+ # Start IDV (returns widget URL for the user to complete)
92
+ verify = await sdk.start_verification(wallet.wallet_address, id_type="40")
93
+
94
+ # After user completes the widget:
95
+ result = await sdk.complete_verification(
96
+ wallet.wallet_address,
97
+ operation_id=verify.operation_id,
98
+ )
99
+
100
+ # Check AIT permissions
101
+ perms = await sdk.get_permissions(wallet.wallet_address)
102
+ if perms.has_permission("transfer_funds"):
103
+ ...
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ api_key: str,
109
+ api_secret: str,
110
+ env: str = "production",
111
+ service_id: Optional[str] = None,
112
+ ) -> None:
113
+ if env not in _ENV_URLS:
114
+ raise ValueError(f"env must be 'production' or 'staging', got '{env}'")
115
+ urls = _ENV_URLS[env]
116
+ self._base_url = urls["ait"]
117
+ self._dashboard_url = urls["dashboard"]
118
+ self._service_id = service_id
119
+ self._headers = {
120
+ "x-api-key": api_key,
121
+ "x-api-secret": api_secret,
122
+ "Content-Type": "application/json",
123
+ }
124
+
125
+ def _url(self, path: str) -> str:
126
+ return f"{self._base_url}{path}"
127
+
128
+ def _dashboard_api_url(self, path: str) -> str:
129
+ if not self._dashboard_url:
130
+ raise ValueError("dashboard_url is required for this operation")
131
+ return f"{self._dashboard_url}{path}"
132
+
133
+ # ── Wallet ────────────────────────────────────────────────────────────────
134
+
135
+ async def connect_wallet(self, email: str) -> WalletResult:
136
+ """Get (or create) a MetaKeep-managed wallet for the given email."""
137
+ async with httpx.AsyncClient(timeout=30.0) as client:
138
+ resp = await client.post(
139
+ self._url("/api/wallet/connect"),
140
+ json={"email": email},
141
+ headers=self._headers,
142
+ )
143
+ _raise_for_status(resp)
144
+ data = resp.json()
145
+ return WalletResult(wallet_address=data["walletAddress"], email=data["email"])
146
+
147
+ # ── Verification ──────────────────────────────────────────────────────────
148
+
149
+ async def start_verification(
150
+ self,
151
+ wallet_address: str,
152
+ id_type: Optional[str] = None,
153
+ ) -> VerifyStartResult:
154
+ """
155
+ Start AuthID identity verification.
156
+
157
+ Returns a widget URL for the user to open in a browser or iframe.
158
+ - Returning users: flow='login' (face scan)
159
+ - New users: flow='onboarding' (document capture, id_type required)
160
+ - New users w/o id_type: flow='choose_document' (prompt user to pick)
161
+ """
162
+ body: dict = {"walletAddress": wallet_address}
163
+ if id_type:
164
+ body["idType"] = id_type
165
+
166
+ async with httpx.AsyncClient(timeout=30.0) as client:
167
+ resp = await client.post(
168
+ self._url("/api/verify/start"),
169
+ json=body,
170
+ headers=self._headers,
171
+ )
172
+ _raise_for_status(resp)
173
+ data = resp.json()
174
+ return VerifyStartResult(
175
+ flow=data["flow"],
176
+ widget_url=data.get("widgetUrl"),
177
+ operation_id=data.get("operationId"),
178
+ transaction_id=data.get("transactionId"),
179
+ )
180
+
181
+ async def complete_verification(
182
+ self,
183
+ wallet_address: str,
184
+ operation_id: Optional[str] = None,
185
+ transaction_id: Optional[str] = None,
186
+ ) -> VerifyCompleteResult:
187
+ """
188
+ Call after the user completes the AuthID widget.
189
+
190
+ Pass operation_id (onboarding flow) or transaction_id (login flow)
191
+ from the VerifyStartResult.
192
+ """
193
+ body: dict = {"walletAddress": wallet_address}
194
+ if operation_id:
195
+ body["operationId"] = operation_id
196
+ if transaction_id:
197
+ body["transactionId"] = transaction_id
198
+
199
+ # Generous timeout: complete_verification polls AuthID for up to 60s
200
+ async with httpx.AsyncClient(timeout=90.0) as client:
201
+ resp = await client.post(
202
+ self._url("/api/verify/complete"),
203
+ json=body,
204
+ headers=self._headers,
205
+ )
206
+ _raise_for_status(resp)
207
+ data = resp.json()
208
+ return VerifyCompleteResult(is_verified=data["isVerified"])
209
+
210
+ # ── Permissions ───────────────────────────────────────────────────────────
211
+
212
+ async def set_permissions(
213
+ self,
214
+ wallet_address: str,
215
+ permissions: list[str],
216
+ granted_by: Optional[str] = None,
217
+ ) -> dict:
218
+ """
219
+ Create an AIT on-chain with the given permissions for wallet_address.
220
+
221
+ requested_by: email or identifier of whoever triggered this request —
222
+ stored in the AIT record for audit purposes. Optional;
223
+ existing callers that omit it are unaffected.
224
+ """
225
+ body: dict = {"address": wallet_address, "permissions": permissions}
226
+ if granted_by:
227
+ body["grantedBy"] = granted_by
228
+
229
+ async with httpx.AsyncClient(timeout=60.0) as client:
230
+ resp = await client.post(
231
+ self._url("/api/ait/set-permissions"),
232
+ json=body,
233
+ headers=self._headers,
234
+ )
235
+ _raise_for_status(resp)
236
+ return resp.json()
237
+
238
+ async def get_admin_granted_tools(self, wallet_address: str) -> list[str]:
239
+ """
240
+ Return the tools admin has pre-approved for this user to self-service request.
241
+
242
+ Empty list means admin hasn't granted this user any tools yet — the caller
243
+ should show "waiting for administrator" rather than checkboxes.
244
+ """
245
+ async with httpx.AsyncClient(timeout=15.0) as client:
246
+ resp = await client.get(
247
+ self._url("/api/agent/permissions"),
248
+ params={"walletAddress": wallet_address},
249
+ headers=self._headers,
250
+ )
251
+ _raise_for_status(resp)
252
+ return resp.json().get("allowedTools", [])
253
+
254
+ async def get_permissions(self, wallet_address: str) -> PermissionsResult:
255
+ """Query admin-granted AIT tool permissions for the given wallet address."""
256
+ async with httpx.AsyncClient(timeout=15.0) as client:
257
+ resp = await client.get(
258
+ self._url("/api/agent/user-permission"),
259
+ params={"walletAddress": wallet_address},
260
+ headers=self._headers,
261
+ )
262
+ _raise_for_status(resp)
263
+ data = resp.json()
264
+ return PermissionsResult(
265
+ allowed=data.get("allowed", False),
266
+ allowed_tools=data.get("allowedTools", []),
267
+ denied_tools=data.get("deniedTools", []),
268
+ reason=data.get("reason"),
269
+ )
270
+
271
+ # ── Agent management ─────────────────────────────────────────────────────
272
+
273
+ async def register_tools(self, tools: list) -> int:
274
+ """
275
+ Register this agent's tool list with nxtlinq so admins can grant permissions.
276
+
277
+ Call once at agent startup. Each tool should be a dict with 'name' and
278
+ optionally 'description'.
279
+
280
+ Returns the number of tools registered.
281
+
282
+ Usage::
283
+
284
+ tool_count = await sdk.register_tools([
285
+ {"name": "transfer_funds", "description": "Move money between accounts"},
286
+ {"name": "get_account_balance", "description": "Read-only finance access"},
287
+ ])
288
+ """
289
+ async with httpx.AsyncClient(timeout=15.0) as client:
290
+ resp = await client.post(
291
+ self._url("/api/agent/tools"),
292
+ json={"tools": tools},
293
+ headers=self._headers,
294
+ )
295
+ _raise_for_status(resp)
296
+ return resp.json().get("toolCount", len(tools))
297
+
298
+ async def log_chat(
299
+ self,
300
+ session_id: str,
301
+ user_message: str,
302
+ assistant_message: str,
303
+ external_user_id: str,
304
+ user_name: str,
305
+ model: str = "",
306
+ has_tool_call: bool = False,
307
+ tool_name: str = "",
308
+ turn_index: Optional[int] = None,
309
+ ) -> None:
310
+ """
311
+ Send one conversation turn to the nxtlinq Dashboard for monitoring.
312
+
313
+ Requires dashboard_url and service_id to be set at SDK init time.
314
+ Fails silently — chat logging is non-fatal.
315
+
316
+ Usage::
317
+
318
+ await sdk.log_chat(
319
+ session_id="abc123",
320
+ user_message="What's my balance?",
321
+ assistant_message="Your balance is $500.",
322
+ external_user_id="user@example.com",
323
+ user_name="Alice",
324
+ has_tool_call=True,
325
+ tool_name="get_account_balance",
326
+ )
327
+ """
328
+ if not self._dashboard_url or not self._service_id:
329
+ return
330
+ payload: dict = {
331
+ "aiType": model,
332
+ "hasToolCall": has_tool_call,
333
+ "toolName": tool_name,
334
+ "userInfo": {
335
+ "externalUserId": external_user_id,
336
+ "userName": user_name,
337
+ },
338
+ "chatHistory": {
339
+ "externalChatHistoryId": session_id,
340
+ "userMessage": user_message,
341
+ "assistantMessage": assistant_message,
342
+ },
343
+ }
344
+ if turn_index is not None:
345
+ payload["chatHistory"]["externalTurnIndex"] = turn_index
346
+ try:
347
+ async with httpx.AsyncClient(timeout=10.0) as client:
348
+ await client.post(
349
+ self._dashboard_api_url(f"/api/service/{self._service_id}/external-service-call-log"),
350
+ json=payload,
351
+ headers=self._headers,
352
+ )
353
+ except Exception:
354
+ pass
355
+
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional
4
+
5
+ from fastapi import APIRouter
6
+ from fastapi.responses import JSONResponse
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class EntraAuth:
11
+ """
12
+ Optional Microsoft Entra SSO module for the nxtID SDK.
13
+
14
+ When enabled, mounts two FastAPI routes onto the host app:
15
+ GET /auth/entra/config — returns client_id/tenant_id for MSAL
16
+ POST /auth/entra — receives MSAL token claims, writes session state
17
+
18
+ Usage::
19
+
20
+ from nxtid_sdk import EntraAuth
21
+
22
+ entra = EntraAuth(
23
+ client_id="...",
24
+ tenant_id="...",
25
+ get_session=get_or_create_session, # callable: session_id -> Session
26
+ )
27
+ entra.mount(app) # app is a FastAPI instance
28
+
29
+ # Check if enabled
30
+ if entra.enabled:
31
+ ...
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ client_id: Optional[str],
37
+ tenant_id: Optional[str],
38
+ get_session,
39
+ ) -> None:
40
+ self._client_id = client_id or ""
41
+ self._tenant_id = tenant_id or ""
42
+ self._get_session = get_session
43
+
44
+ @property
45
+ def enabled(self) -> bool:
46
+ return bool(self._client_id and self._tenant_id)
47
+
48
+ def mount(self, app) -> None:
49
+ """Mount /auth/entra/config and /auth/entra routes onto a FastAPI app."""
50
+ router = self._build_router()
51
+ app.include_router(router)
52
+
53
+ def _build_router(self) -> APIRouter:
54
+ router = APIRouter()
55
+ client_id = self._client_id
56
+ tenant_id = self._tenant_id
57
+ enabled = self.enabled
58
+ get_session = self._get_session
59
+
60
+ class _EntraAuthRequest(BaseModel):
61
+ session_id: str
62
+ name: str
63
+ email: str
64
+ tenant_id: Optional[str] = None
65
+ roles: List[str] = []
66
+
67
+ @router.get("/auth/entra/config")
68
+ async def entra_config() -> dict:
69
+ return {
70
+ "enabled": enabled,
71
+ "client_id": client_id,
72
+ "tenant_id": tenant_id,
73
+ }
74
+
75
+ @router.post("/auth/entra")
76
+ async def auth_entra(req: _EntraAuthRequest) -> dict:
77
+ session = get_session(req.session_id)
78
+ session.entra_authenticated = True
79
+ session.entra_name = req.name
80
+ session.entra_email = req.email
81
+ session.entra_tenant_id = req.tenant_id
82
+ session.entra_roles = req.roles
83
+ return {
84
+ "session_id": session.session_id,
85
+ "entra_authenticated": True,
86
+ "name": req.name,
87
+ "email": req.email,
88
+ "roles": req.roles,
89
+ }
90
+
91
+ return router
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ToolCallBlockedError(Exception):
5
+ """
6
+ Raised to block a tool execution before it reaches the LLM tool handler.
7
+
8
+ reason: 'verification_required' | 'permission_denied'
9
+ message: user-facing explanation
10
+ """
11
+
12
+ def __init__(self, reason: str, message: str) -> None:
13
+ self.reason = reason
14
+ self.message = message
15
+
16
+ @classmethod
17
+ def permission_denied(cls, tool_name: str, allowed_tools: list[str]) -> "ToolCallBlockedError":
18
+ tools_str = ", ".join(allowed_tools) if allowed_tools else "none"
19
+ return cls(
20
+ "permission_denied",
21
+ f"You don't have permission to use '{tool_name}'. "
22
+ f"Your current permissions: [{tools_str}]. "
23
+ "Contact your administrator.",
24
+ )
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: nxtid-sdk
3
+ Version: 0.1.0
4
+ Summary: nxtID Python SDK — AIT-gated AI agent tool permissions
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ nxtid_sdk/__init__.py
4
+ nxtid_sdk/client.py
5
+ nxtid_sdk/entra.py
6
+ nxtid_sdk/exceptions.py
7
+ nxtid_sdk.egg-info/PKG-INFO
8
+ nxtid_sdk.egg-info/SOURCES.txt
9
+ nxtid_sdk.egg-info/dependency_links.txt
10
+ nxtid_sdk.egg-info/requires.txt
11
+ nxtid_sdk.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ httpx>=0.27
@@ -0,0 +1 @@
1
+ nxtid_sdk
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nxtid-sdk"
7
+ version = "0.1.0"
8
+ description = "nxtID Python SDK — AIT-gated AI agent tool permissions"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "httpx>=0.27",
12
+ ]
13
+
14
+ [tool.setuptools.packages.find]
15
+ where = ["."]
16
+ include = ["nxtid_sdk*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+