swiftapi-python 1.2.2__py3-none-any.whl → 1.3.1__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.
- swiftapi/__init__.py +75 -96
- swiftapi/__main__.py +2 -0
- swiftapi/anthropic.py +236 -222
- swiftapi/brake/__init__.py +45 -0
- swiftapi/brake/benchmarks.py +111 -0
- swiftapi/brake/broker.py +274 -0
- swiftapi/brake/core.py +420 -0
- swiftapi/brake/install.py +268 -0
- swiftapi/brake/mcp.py +160 -0
- swiftapi/brake/observations.py +61 -0
- swiftapi/brake/qualification.py +116 -0
- swiftapi/brake/remote.py +90 -0
- swiftapi/cli.py +150 -0
- swiftapi/client.py +328 -309
- swiftapi/enforcement.py +281 -280
- swiftapi/openai.py +243 -225
- swiftapi/py.typed +0 -0
- swiftapi/verifier.py +182 -130
- swiftapi_python-1.3.1.dist-info/METADATA +144 -0
- swiftapi_python-1.3.1.dist-info/RECORD +26 -0
- {swiftapi_python-1.2.2.dist-info → swiftapi_python-1.3.1.dist-info}/WHEEL +1 -1
- swiftapi_python-1.3.1.dist-info/entry_points.txt +2 -0
- swiftapi_python-1.2.2.dist-info/METADATA +0 -255
- swiftapi_python-1.2.2.dist-info/RECORD +0 -13
- {swiftapi_python-1.2.2.dist-info → swiftapi_python-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {swiftapi_python-1.2.2.dist-info → swiftapi_python-1.3.1.dist-info}/top_level.txt +0 -0
swiftapi/client.py
CHANGED
|
@@ -1,309 +1,328 @@
|
|
|
1
|
-
"""
|
|
2
|
-
SwiftAPI SDK - HTTP Client
|
|
3
|
-
|
|
4
|
-
This module provides the HTTP client for communicating with SwiftAPI.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import requests
|
|
8
|
-
from typing import Dict, Any, Optional, List
|
|
9
|
-
|
|
10
|
-
from . import __version__
|
|
11
|
-
from .exceptions import (
|
|
12
|
-
SwiftAPIError,
|
|
13
|
-
AuthenticationError,
|
|
14
|
-
PolicyViolation,
|
|
15
|
-
RateLimitError,
|
|
16
|
-
NetworkError,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
DEFAULT_BASE_URL = "https://swiftapi.ai"
|
|
20
|
-
DEFAULT_TIMEOUT = 30
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class SwiftAPI:
|
|
24
|
-
"""
|
|
25
|
-
SwiftAPI HTTP Client.
|
|
26
|
-
|
|
27
|
-
Usage:
|
|
28
|
-
api = SwiftAPI(key="swiftapi_live_...")
|
|
29
|
-
result = api.verify("file_write", "Save config", {"path": "/etc/config"})
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(
|
|
33
|
-
self,
|
|
34
|
-
key: str,
|
|
35
|
-
base_url: str = DEFAULT_BASE_URL,
|
|
36
|
-
timeout: int = DEFAULT_TIMEOUT,
|
|
37
|
-
):
|
|
38
|
-
"""
|
|
39
|
-
Initialize SwiftAPI client.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
key: SwiftAPI authority key (swiftapi_live_...)
|
|
43
|
-
base_url: API base URL (default: https://swiftapi.ai)
|
|
44
|
-
timeout: Request timeout in seconds
|
|
45
|
-
"""
|
|
46
|
-
if not key:
|
|
47
|
-
raise AuthenticationError("API key is required")
|
|
48
|
-
if not key.startswith("swiftapi_live_"):
|
|
49
|
-
raise AuthenticationError("Invalid key format: must start with 'swiftapi_live_'")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
raise
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Returns:
|
|
195
|
-
|
|
196
|
-
"""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
1
|
+
"""
|
|
2
|
+
SwiftAPI SDK - HTTP Client
|
|
3
|
+
|
|
4
|
+
This module provides the HTTP client for communicating with SwiftAPI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
SwiftAPIError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
PolicyViolation,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://swiftapi.ai"
|
|
20
|
+
DEFAULT_TIMEOUT = 30
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SwiftAPI:
|
|
24
|
+
"""
|
|
25
|
+
SwiftAPI HTTP Client.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
api = SwiftAPI(key="swiftapi_live_...")
|
|
29
|
+
result = api.verify("file_write", "Save config", {"path": "/etc/config"})
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
key: str,
|
|
35
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
36
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Initialize SwiftAPI client.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
key: SwiftAPI authority key (swiftapi_live_...)
|
|
43
|
+
base_url: API base URL (default: https://swiftapi.ai)
|
|
44
|
+
timeout: Request timeout in seconds
|
|
45
|
+
"""
|
|
46
|
+
if not key:
|
|
47
|
+
raise AuthenticationError("API key is required")
|
|
48
|
+
if not key.startswith("swiftapi_live_"):
|
|
49
|
+
raise AuthenticationError("Invalid key format: must start with 'swiftapi_live_'")
|
|
50
|
+
|
|
51
|
+
from urllib.parse import urlparse
|
|
52
|
+
parsed = urlparse(base_url)
|
|
53
|
+
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
54
|
+
raise SwiftAPIError("Authority URL must use HTTPS without embedded credentials")
|
|
55
|
+
if not isinstance(timeout, (int, float)) or not 0 < timeout <= 300:
|
|
56
|
+
raise SwiftAPIError("Timeout must be positive and at most 300 seconds")
|
|
57
|
+
self.key = key
|
|
58
|
+
self.base_url = base_url.rstrip("/")
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
self._session = requests.Session()
|
|
61
|
+
self._session.headers.update({
|
|
62
|
+
"X-SwiftAPI-Authority": key,
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"User-Agent": f"swiftapi-python/{__version__}",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
def _request(
|
|
68
|
+
self,
|
|
69
|
+
method: str,
|
|
70
|
+
endpoint: str,
|
|
71
|
+
json: Dict = None,
|
|
72
|
+
params: Dict = None,
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
"""Make HTTP request to SwiftAPI."""
|
|
75
|
+
url = f"{self.base_url}{endpoint}"
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
response = self._session.request(
|
|
79
|
+
method=method,
|
|
80
|
+
url=url,
|
|
81
|
+
json=json,
|
|
82
|
+
params=params,
|
|
83
|
+
timeout=self.timeout,
|
|
84
|
+
allow_redirects=False,
|
|
85
|
+
)
|
|
86
|
+
except requests.exceptions.ConnectionError:
|
|
87
|
+
raise NetworkError("Failed to connect to SwiftAPI") from None
|
|
88
|
+
except requests.exceptions.Timeout:
|
|
89
|
+
raise NetworkError(f"Request timed out after {self.timeout}s")
|
|
90
|
+
except requests.exceptions.RequestException:
|
|
91
|
+
raise NetworkError("SwiftAPI request failed") from None
|
|
92
|
+
|
|
93
|
+
# Handle response
|
|
94
|
+
return self._handle_response(response)
|
|
95
|
+
|
|
96
|
+
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
97
|
+
"""Handle API response and raise appropriate exceptions."""
|
|
98
|
+
if 300 <= response.status_code < 400:
|
|
99
|
+
raise SwiftAPIError("Authority redirect refused", response.status_code)
|
|
100
|
+
# Rate limit
|
|
101
|
+
if response.status_code == 429:
|
|
102
|
+
retry_after = response.headers.get("Retry-After")
|
|
103
|
+
raise RateLimitError(int(retry_after) if retry_after and retry_after.isdigit() else None)
|
|
104
|
+
|
|
105
|
+
# Auth errors
|
|
106
|
+
if response.status_code == 401:
|
|
107
|
+
raise AuthenticationError("Invalid API key")
|
|
108
|
+
if response.status_code == 403:
|
|
109
|
+
try:
|
|
110
|
+
data = response.json()
|
|
111
|
+
detail = data.get("detail", {}) if isinstance(data, dict) else {}
|
|
112
|
+
raise AuthenticationError(detail.get("message", "Forbidden") if isinstance(detail, dict) else "Forbidden")
|
|
113
|
+
except ValueError:
|
|
114
|
+
raise AuthenticationError("Forbidden")
|
|
115
|
+
|
|
116
|
+
# Parse JSON
|
|
117
|
+
try:
|
|
118
|
+
data = response.json()
|
|
119
|
+
except ValueError:
|
|
120
|
+
raise SwiftAPIError("Invalid JSON response")
|
|
121
|
+
|
|
122
|
+
if not isinstance(data, dict):
|
|
123
|
+
raise SwiftAPIError("Expected JSON object response")
|
|
124
|
+
|
|
125
|
+
# Policy violations (denied actions)
|
|
126
|
+
if response.status_code == 200 and data.get("approved") is False:
|
|
127
|
+
reason = data.get("denial_reason") or data.get("reason", "Action denied by policy")
|
|
128
|
+
action_type = data.get("action_type") or data.get("action", {}).get("type")
|
|
129
|
+
raise PolicyViolation(
|
|
130
|
+
message=reason,
|
|
131
|
+
action_type=action_type,
|
|
132
|
+
denial_reason=reason,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Other errors
|
|
136
|
+
if response.status_code >= 400:
|
|
137
|
+
detail = data.get("detail", {})
|
|
138
|
+
if isinstance(detail, dict):
|
|
139
|
+
message = detail.get("message", str(detail))
|
|
140
|
+
else:
|
|
141
|
+
message = str(detail)
|
|
142
|
+
raise SwiftAPIError(message, response.status_code, data)
|
|
143
|
+
|
|
144
|
+
return data
|
|
145
|
+
|
|
146
|
+
# =========================================================================
|
|
147
|
+
# Core Endpoints
|
|
148
|
+
# =========================================================================
|
|
149
|
+
|
|
150
|
+
def verify(
|
|
151
|
+
self,
|
|
152
|
+
action_type: str,
|
|
153
|
+
intent: str,
|
|
154
|
+
params: Optional[Dict[str, Any]] = None,
|
|
155
|
+
actor: str = "sdk",
|
|
156
|
+
app_id: str = "swiftapi-python",
|
|
157
|
+
) -> Dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Submit an action for verification and get an execution attestation.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
action_type: Type of action (e.g., "file_write", "api_call")
|
|
163
|
+
intent: Human-readable description of intent
|
|
164
|
+
params: Optional action parameters
|
|
165
|
+
actor: Identifier for the requesting agent
|
|
166
|
+
app_id: Application identifier
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Full verification response including execution_attestation
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
PolicyViolation: If action is denied
|
|
173
|
+
AuthenticationError: If key is invalid
|
|
174
|
+
SwiftAPIError: For other errors
|
|
175
|
+
"""
|
|
176
|
+
payload = {
|
|
177
|
+
"action": {
|
|
178
|
+
"type": action_type,
|
|
179
|
+
"intent": intent,
|
|
180
|
+
"params": params or {},
|
|
181
|
+
},
|
|
182
|
+
"actor": actor,
|
|
183
|
+
"app_id": app_id,
|
|
184
|
+
}
|
|
185
|
+
return self._request("POST", "/verify", json=payload)
|
|
186
|
+
|
|
187
|
+
def verify_attestation(self, attestation: Dict[str, Any]) -> Dict[str, Any]:
|
|
188
|
+
"""
|
|
189
|
+
Verify an existing attestation with the server.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
attestation: The execution_attestation to verify
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Verification result
|
|
196
|
+
"""
|
|
197
|
+
return self._request("POST", "/attestation/verify", json=attestation)
|
|
198
|
+
|
|
199
|
+
def revoke(self, jti: str, reason: str = "SDK revocation") -> Dict[str, Any]:
|
|
200
|
+
"""
|
|
201
|
+
Revoke an attestation by JTI.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
jti: The attestation's unique identifier
|
|
205
|
+
reason: Reason for revocation
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Revocation confirmation
|
|
209
|
+
"""
|
|
210
|
+
payload = {"jti": jti, "reason": reason}
|
|
211
|
+
return self._request("POST", "/attestation/revoke", json=payload)
|
|
212
|
+
|
|
213
|
+
def check_revocation(self, jti: str) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Check if an attestation has been revoked.
|
|
216
|
+
|
|
217
|
+
This is a fast check - use for real-time revocation verification.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
jti: The attestation's unique identifier
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if revoked, False if valid
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
SwiftAPIError: If revocation status cannot be established, including
|
|
227
|
+
network errors or malformed responses. Execution must not proceed.
|
|
228
|
+
"""
|
|
229
|
+
result = self._request("GET", f"/attestation/revoked/{jti}")
|
|
230
|
+
if (
|
|
231
|
+
not isinstance(result, dict)
|
|
232
|
+
or result.get("jti") != jti
|
|
233
|
+
or type(result.get("revoked")) is not bool
|
|
234
|
+
):
|
|
235
|
+
raise SwiftAPIError("Invalid revocation response")
|
|
236
|
+
return result["revoked"]
|
|
237
|
+
|
|
238
|
+
def attest(
|
|
239
|
+
self,
|
|
240
|
+
action_type: str,
|
|
241
|
+
action_data: Dict[str, Any],
|
|
242
|
+
) -> Dict[str, Any]:
|
|
243
|
+
"""
|
|
244
|
+
Request an execution attestation from the authority.
|
|
245
|
+
|
|
246
|
+
Calls /attest to get a signed attestation for the given action.
|
|
247
|
+
The attestation can then be passed to gated endpoints like /chat/vibe.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
action_type: Type of action (e.g., "chat_completion")
|
|
251
|
+
action_data: Action payload to be attested
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Signed attestation dict with jti, signature, action_fingerprint, etc.
|
|
255
|
+
"""
|
|
256
|
+
payload = {
|
|
257
|
+
"action_type": action_type,
|
|
258
|
+
"action_data": action_data,
|
|
259
|
+
}
|
|
260
|
+
return self._request("POST", "/attest", json=payload)
|
|
261
|
+
|
|
262
|
+
# =========================================================================
|
|
263
|
+
# Info Endpoints
|
|
264
|
+
# =========================================================================
|
|
265
|
+
|
|
266
|
+
def get_info(self) -> Dict[str, Any]:
|
|
267
|
+
"""Get API info and public key."""
|
|
268
|
+
return self._request("GET", "/")
|
|
269
|
+
|
|
270
|
+
def health(self) -> bool:
|
|
271
|
+
"""Check API health."""
|
|
272
|
+
try:
|
|
273
|
+
result = self._request("GET", "/health")
|
|
274
|
+
return result.get("status") == "healthy"
|
|
275
|
+
except Exception:
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def get_policies(self) -> List[Dict[str, Any]]:
|
|
279
|
+
"""Get active policies."""
|
|
280
|
+
result = self._request("GET", "/policies")
|
|
281
|
+
return result.get("policies", [])
|
|
282
|
+
|
|
283
|
+
def get_scopes(self) -> List[str]:
|
|
284
|
+
"""Get available authority scopes."""
|
|
285
|
+
result = self._request("GET", "/authority/scopes")
|
|
286
|
+
return result.get("scopes", [])
|
|
287
|
+
|
|
288
|
+
# =========================================================================
|
|
289
|
+
# Key Management (Admin Only)
|
|
290
|
+
# =========================================================================
|
|
291
|
+
|
|
292
|
+
def list_keys(self) -> List[Dict[str, Any]]:
|
|
293
|
+
"""List authority keys (requires admin scope)."""
|
|
294
|
+
result = self._request("GET", "/authority/keys")
|
|
295
|
+
return result.get("keys", [])
|
|
296
|
+
|
|
297
|
+
def create_key(
|
|
298
|
+
self,
|
|
299
|
+
name: str,
|
|
300
|
+
scopes: List[str],
|
|
301
|
+
) -> Dict[str, Any]:
|
|
302
|
+
"""
|
|
303
|
+
Create a new authority key (requires admin scope).
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
name: Key name
|
|
307
|
+
scopes: List of scopes to grant
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
New key details (including the key itself - shown only once!)
|
|
311
|
+
"""
|
|
312
|
+
payload = {"name": name, "scopes": scopes}
|
|
313
|
+
return self._request("POST", "/authority/keys", json=payload)
|
|
314
|
+
|
|
315
|
+
def revoke_key(self, key_hash: str) -> Dict[str, Any]:
|
|
316
|
+
"""Revoke an authority key by hash."""
|
|
317
|
+
return self._request("DELETE", f"/authority/keys/{key_hash}")
|
|
318
|
+
|
|
319
|
+
# =========================================================================
|
|
320
|
+
# Context Manager
|
|
321
|
+
# =========================================================================
|
|
322
|
+
|
|
323
|
+
def __enter__(self):
|
|
324
|
+
return self
|
|
325
|
+
|
|
326
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
327
|
+
self._session.close()
|
|
328
|
+
return False
|