getstack 0.1.0__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.
- getstack/__init__.py +233 -0
- getstack/agents.py +70 -0
- getstack/audit.py +26 -0
- getstack/auth.py +78 -0
- getstack/client.py +102 -0
- getstack/credentials.py +33 -0
- getstack/dropoffs.py +43 -0
- getstack/errors.py +56 -0
- getstack/notifications.py +57 -0
- getstack/passports.py +359 -0
- getstack/py.typed +0 -0
- getstack/reviews.py +54 -0
- getstack/services.py +41 -0
- getstack/types.py +156 -0
- getstack-0.1.0.dist-info/METADATA +143 -0
- getstack-0.1.0.dist-info/RECORD +18 -0
- getstack-0.1.0.dist-info/WHEEL +4 -0
- getstack-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Notification channel management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
from .types import NotificationChannel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _to_channel(data: dict[str, Any]) -> NotificationChannel:
|
|
12
|
+
return NotificationChannel(
|
|
13
|
+
id=data["id"],
|
|
14
|
+
channel_type=data["channel_type"],
|
|
15
|
+
destination=data["destination"],
|
|
16
|
+
events=data.get("events", []),
|
|
17
|
+
min_severity=data.get("min_severity", "warning"),
|
|
18
|
+
active=data.get("active", True),
|
|
19
|
+
verified=data.get("verified", False),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NotificationService:
|
|
24
|
+
def __init__(self, client: HttpClient):
|
|
25
|
+
self._client = client
|
|
26
|
+
|
|
27
|
+
def list(self) -> list[NotificationChannel]:
|
|
28
|
+
data = self._client.get("/v1/notifications/channels")
|
|
29
|
+
return [_to_channel(ch) for ch in data]
|
|
30
|
+
|
|
31
|
+
def create(
|
|
32
|
+
self,
|
|
33
|
+
channel_type: str,
|
|
34
|
+
destination: str,
|
|
35
|
+
events: list[str] | None = None,
|
|
36
|
+
min_severity: str = "warning",
|
|
37
|
+
) -> NotificationChannel:
|
|
38
|
+
body: dict[str, Any] = {
|
|
39
|
+
"channel_type": channel_type,
|
|
40
|
+
"destination": destination,
|
|
41
|
+
"min_severity": min_severity,
|
|
42
|
+
}
|
|
43
|
+
if events:
|
|
44
|
+
body["events"] = events
|
|
45
|
+
return _to_channel(self._client.post("/v1/notifications/channels", json=body))
|
|
46
|
+
|
|
47
|
+
def verify(self, channel_id: str, code: str | None = None) -> dict[str, Any]:
|
|
48
|
+
body: dict[str, Any] = {}
|
|
49
|
+
if code:
|
|
50
|
+
body["code"] = code
|
|
51
|
+
return self._client.post(f"/v1/notifications/channels/{channel_id}/verify", json=body)
|
|
52
|
+
|
|
53
|
+
def test(self, channel_id: str) -> dict[str, Any]:
|
|
54
|
+
return self._client.post(f"/v1/notifications/channels/{channel_id}/test")
|
|
55
|
+
|
|
56
|
+
def delete(self, channel_id: str) -> dict[str, Any]:
|
|
57
|
+
return self._client.delete(f"/v1/notifications/channels/{channel_id}")
|
getstack/passports.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Passport management with mission context manager and continuous rotation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
import threading
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any, Generator
|
|
10
|
+
|
|
11
|
+
from .client import HttpClient
|
|
12
|
+
from .types import Passport, CheckpointResult, CheckoutResult, PassportReport, ToolCall
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_duration(s: str) -> int:
|
|
16
|
+
"""Parse a duration string like '30m', '2h', '300' to seconds."""
|
|
17
|
+
match = re.match(r"^(\d+)(s|m|h)$", s)
|
|
18
|
+
if match:
|
|
19
|
+
num, unit = int(match.group(1)), match.group(2)
|
|
20
|
+
return num * {"s": 1, "m": 60, "h": 3600}[unit]
|
|
21
|
+
return int(s)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _to_passport(data: dict[str, Any]) -> Passport:
|
|
25
|
+
return Passport(
|
|
26
|
+
token=data["token"],
|
|
27
|
+
jti=data["jti"],
|
|
28
|
+
expires_at=data["expires_at"],
|
|
29
|
+
accountability=data.get("accountability"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Mission:
|
|
34
|
+
"""A bounded accountability window for an agent mission.
|
|
35
|
+
|
|
36
|
+
Tracks tool calls, submits checkpoints on schedule, and checks out
|
|
37
|
+
automatically when the context manager exits.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
service: PassportService,
|
|
43
|
+
agent_id: str,
|
|
44
|
+
intent: str,
|
|
45
|
+
services: list[str],
|
|
46
|
+
will_delegate: bool = False,
|
|
47
|
+
estimated_duration: str | None = None,
|
|
48
|
+
checkpoint_interval: str | None = None,
|
|
49
|
+
):
|
|
50
|
+
self._service = service
|
|
51
|
+
self._agent_id = agent_id
|
|
52
|
+
self._intent = intent
|
|
53
|
+
self._declared_services = services
|
|
54
|
+
self._will_delegate = will_delegate
|
|
55
|
+
self._estimated_duration = estimated_duration
|
|
56
|
+
self._checkpoint_interval = checkpoint_interval
|
|
57
|
+
|
|
58
|
+
self.passport: Passport | None = None
|
|
59
|
+
self.review_status: str | None = None
|
|
60
|
+
self.result: CheckoutResult | None = None
|
|
61
|
+
self.rotation_count: int = 0
|
|
62
|
+
|
|
63
|
+
self._tool_calls: list[ToolCall] = []
|
|
64
|
+
self._services_used: set[str] = set()
|
|
65
|
+
self._delegated_to: list[str] = []
|
|
66
|
+
self._last_checkpoint_index: int = 0
|
|
67
|
+
self._timer: threading.Timer | None = None
|
|
68
|
+
self._timer_interval: float | None = None
|
|
69
|
+
self._rotation_start: float = 0
|
|
70
|
+
self._rotation_interval: float | None = None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def should_rotate(self) -> bool:
|
|
74
|
+
"""True when the rotation interval has elapsed. Check at natural breakpoints."""
|
|
75
|
+
if self._rotation_interval is None:
|
|
76
|
+
return False
|
|
77
|
+
return time.monotonic() - self._rotation_start >= self._rotation_interval
|
|
78
|
+
|
|
79
|
+
def log(self, service: str, method: str, target: str | None = None) -> None:
|
|
80
|
+
"""Log a tool call. Call this for each action the agent takes."""
|
|
81
|
+
self._tool_calls.append(ToolCall(service=service, method=method, target=target))
|
|
82
|
+
self._services_used.add(service)
|
|
83
|
+
|
|
84
|
+
def delegate(self, child_agent_id: str) -> None:
|
|
85
|
+
"""Record that delegation occurred to a child agent."""
|
|
86
|
+
self._delegated_to.append(child_agent_id)
|
|
87
|
+
|
|
88
|
+
def _enter(self) -> None:
|
|
89
|
+
"""Issue passport and start checkpoint timer."""
|
|
90
|
+
self.passport = self._service.issue(
|
|
91
|
+
agent_id=self._agent_id,
|
|
92
|
+
intent=self._intent,
|
|
93
|
+
services=self._declared_services,
|
|
94
|
+
will_delegate=self._will_delegate,
|
|
95
|
+
estimated_duration=self._estimated_duration,
|
|
96
|
+
checkpoint_interval=self._checkpoint_interval,
|
|
97
|
+
)
|
|
98
|
+
self._rotation_start = time.monotonic()
|
|
99
|
+
|
|
100
|
+
# Start checkpoint timer for enforced mode
|
|
101
|
+
if self._checkpoint_interval:
|
|
102
|
+
self._timer_interval = _parse_duration(self._checkpoint_interval)
|
|
103
|
+
elif self.passport.accountability == "enforced":
|
|
104
|
+
self._timer_interval = 300 # 5 min default
|
|
105
|
+
else:
|
|
106
|
+
self._timer_interval = None
|
|
107
|
+
|
|
108
|
+
if self._timer_interval:
|
|
109
|
+
self._schedule_checkpoint()
|
|
110
|
+
|
|
111
|
+
def _schedule_checkpoint(self) -> None:
|
|
112
|
+
"""Schedule the next automatic checkpoint."""
|
|
113
|
+
if self._timer_interval:
|
|
114
|
+
# Submit slightly before the interval to ensure we don't miss it
|
|
115
|
+
delay = max(self._timer_interval - 5, 10)
|
|
116
|
+
self._timer = threading.Timer(delay, self._auto_checkpoint)
|
|
117
|
+
self._timer.daemon = True
|
|
118
|
+
self._timer.start()
|
|
119
|
+
|
|
120
|
+
def _auto_checkpoint(self) -> None:
|
|
121
|
+
"""Submit checkpoint and reschedule."""
|
|
122
|
+
try:
|
|
123
|
+
self._submit_checkpoint()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass # Don't crash the agent on checkpoint failure
|
|
126
|
+
self._schedule_checkpoint()
|
|
127
|
+
|
|
128
|
+
def _submit_checkpoint(self) -> None:
|
|
129
|
+
"""Submit accumulated activity as a checkpoint."""
|
|
130
|
+
if not self.passport:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
new_calls = self._tool_calls[self._last_checkpoint_index:]
|
|
134
|
+
self._service.checkpoint(
|
|
135
|
+
self.passport.jti,
|
|
136
|
+
services_used=list(self._services_used),
|
|
137
|
+
actions_count=len(new_calls),
|
|
138
|
+
tool_calls=[tc.to_dict() for tc in new_calls] if new_calls else None,
|
|
139
|
+
delegated_to=self._delegated_to if self._delegated_to else None,
|
|
140
|
+
)
|
|
141
|
+
self._last_checkpoint_index = len(self._tool_calls)
|
|
142
|
+
|
|
143
|
+
def _exit(self, error: Exception | None = None) -> None:
|
|
144
|
+
"""Stop timer and submit checkout."""
|
|
145
|
+
if self._timer:
|
|
146
|
+
self._timer.cancel()
|
|
147
|
+
self._timer = None
|
|
148
|
+
|
|
149
|
+
if not self.passport:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
summary = None
|
|
153
|
+
if error:
|
|
154
|
+
summary = f"Mission failed: {type(error).__name__}: {error}"
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
self.result = self._service.checkout(
|
|
158
|
+
self.passport.jti,
|
|
159
|
+
services_used=list(self._services_used),
|
|
160
|
+
actions_count=len(self._tool_calls),
|
|
161
|
+
tool_calls=[tc.to_dict() for tc in self._tool_calls] if self._tool_calls else None,
|
|
162
|
+
summary=summary,
|
|
163
|
+
delegated_to=self._delegated_to if self._delegated_to else None,
|
|
164
|
+
)
|
|
165
|
+
self.review_status = self.result.review_status
|
|
166
|
+
except Exception:
|
|
167
|
+
pass # Best-effort checkout
|
|
168
|
+
|
|
169
|
+
def __enter__(self) -> Mission:
|
|
170
|
+
self._enter()
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def __exit__(self, exc_type: type | None, exc_val: Exception | None, exc_tb: Any) -> bool:
|
|
174
|
+
self._exit(error=exc_val if exc_type else None)
|
|
175
|
+
return False # Don't suppress exceptions
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class PassportService:
|
|
179
|
+
def __init__(self, client: HttpClient):
|
|
180
|
+
self._client = client
|
|
181
|
+
|
|
182
|
+
def issue(
|
|
183
|
+
self,
|
|
184
|
+
agent_id: str,
|
|
185
|
+
intent: str | None = None,
|
|
186
|
+
services: list[str] | None = None,
|
|
187
|
+
will_delegate: bool = False,
|
|
188
|
+
estimated_duration: str | None = None,
|
|
189
|
+
checkpoint_interval: str | None = None,
|
|
190
|
+
ttl_seconds: int | None = None,
|
|
191
|
+
scopes: list[dict[str, Any]] | None = None,
|
|
192
|
+
) -> Passport:
|
|
193
|
+
body: dict[str, Any] = {"agent_id": agent_id}
|
|
194
|
+
if scopes:
|
|
195
|
+
body["scopes"] = scopes
|
|
196
|
+
if ttl_seconds:
|
|
197
|
+
body["ttl_seconds"] = ttl_seconds
|
|
198
|
+
if intent or services:
|
|
199
|
+
body["intent"] = {
|
|
200
|
+
"summary": intent or "",
|
|
201
|
+
"services": services or [],
|
|
202
|
+
"will_delegate": will_delegate,
|
|
203
|
+
}
|
|
204
|
+
if estimated_duration:
|
|
205
|
+
body["intent"]["estimated_duration_seconds"] = _parse_duration(estimated_duration)
|
|
206
|
+
if checkpoint_interval:
|
|
207
|
+
body["checkpoint_interval_seconds"] = _parse_duration(checkpoint_interval)
|
|
208
|
+
return _to_passport(self._client.post("/v1/passports/issue", json=body))
|
|
209
|
+
|
|
210
|
+
def checkpoint(
|
|
211
|
+
self,
|
|
212
|
+
passport_jti: str,
|
|
213
|
+
services_used: list[str],
|
|
214
|
+
actions_count: int,
|
|
215
|
+
tool_calls: list[dict[str, Any]] | None = None,
|
|
216
|
+
summary: str | None = None,
|
|
217
|
+
delegated_to: list[str] | None = None,
|
|
218
|
+
) -> CheckpointResult:
|
|
219
|
+
body: dict[str, Any] = {
|
|
220
|
+
"services_used": services_used,
|
|
221
|
+
"actions_count": actions_count,
|
|
222
|
+
}
|
|
223
|
+
if tool_calls:
|
|
224
|
+
body["tool_calls"] = tool_calls
|
|
225
|
+
if summary:
|
|
226
|
+
body["summary"] = summary
|
|
227
|
+
if delegated_to:
|
|
228
|
+
body["delegated_to"] = delegated_to
|
|
229
|
+
data = self._client.post(f"/v1/passports/{passport_jti}/checkpoint", json=body)
|
|
230
|
+
return CheckpointResult(
|
|
231
|
+
checkpoint_id=data["checkpoint_id"],
|
|
232
|
+
passport_jti=data["passport_jti"],
|
|
233
|
+
new_expires_at=data.get("new_expires_at"),
|
|
234
|
+
flags=data.get("flags"),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def checkout(
|
|
238
|
+
self,
|
|
239
|
+
passport_jti: str,
|
|
240
|
+
services_used: list[str],
|
|
241
|
+
actions_count: int,
|
|
242
|
+
tool_calls: list[dict[str, Any]] | None = None,
|
|
243
|
+
summary: str | None = None,
|
|
244
|
+
delegated_to: list[str] | None = None,
|
|
245
|
+
) -> CheckoutResult:
|
|
246
|
+
body: dict[str, Any] = {
|
|
247
|
+
"services_used": services_used,
|
|
248
|
+
"actions_count": actions_count,
|
|
249
|
+
}
|
|
250
|
+
if tool_calls:
|
|
251
|
+
body["tool_calls"] = tool_calls
|
|
252
|
+
if summary:
|
|
253
|
+
body["summary"] = summary
|
|
254
|
+
if delegated_to:
|
|
255
|
+
body["delegated_to"] = delegated_to
|
|
256
|
+
data = self._client.post(f"/v1/passports/{passport_jti}/checkout", json=body)
|
|
257
|
+
return CheckoutResult(
|
|
258
|
+
checkout_id=data["checkout_id"],
|
|
259
|
+
passport_jti=data["passport_jti"],
|
|
260
|
+
review_status=data["review_status"],
|
|
261
|
+
flags=data.get("flags"),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def report(self, passport_jti: str) -> PassportReport:
|
|
265
|
+
data = self._client.get(f"/v1/passports/{passport_jti}/report")
|
|
266
|
+
return PassportReport(**data)
|
|
267
|
+
|
|
268
|
+
def verify(self, token: str) -> dict[str, Any]:
|
|
269
|
+
return self._client.post("/v1/passports/verify", json={"token": token})
|
|
270
|
+
|
|
271
|
+
def revoke(self, jti: str, reason: str | None = None) -> dict[str, Any]:
|
|
272
|
+
return self._client.post("/v1/passports/revoke", json={"jti": jti, "reason": reason})
|
|
273
|
+
|
|
274
|
+
def refresh(self, token: str, ttl_seconds: int | None = None) -> Passport:
|
|
275
|
+
body: dict[str, Any] = {"token": token}
|
|
276
|
+
if ttl_seconds:
|
|
277
|
+
body["ttl_seconds"] = ttl_seconds
|
|
278
|
+
return _to_passport(self._client.post("/v1/passports/refresh", json=body))
|
|
279
|
+
|
|
280
|
+
def list_active(self, agent_id: str | None = None, session_id: str | None = None) -> list[dict[str, Any]]:
|
|
281
|
+
params: dict[str, str] = {}
|
|
282
|
+
if agent_id:
|
|
283
|
+
params["agent_id"] = agent_id
|
|
284
|
+
if session_id:
|
|
285
|
+
params["session_id"] = session_id
|
|
286
|
+
return self._client.get("/v1/passports/active", params=params or None)
|
|
287
|
+
|
|
288
|
+
@contextmanager
|
|
289
|
+
def mission(
|
|
290
|
+
self,
|
|
291
|
+
agent_id: str,
|
|
292
|
+
intent: str,
|
|
293
|
+
services: list[str],
|
|
294
|
+
will_delegate: bool = False,
|
|
295
|
+
estimated_duration: str | None = None,
|
|
296
|
+
checkpoint_interval: str | None = None,
|
|
297
|
+
) -> Generator[Mission, None, None]:
|
|
298
|
+
"""Context manager for a single bounded mission."""
|
|
299
|
+
m = Mission(
|
|
300
|
+
self, agent_id, intent, services,
|
|
301
|
+
will_delegate=will_delegate,
|
|
302
|
+
estimated_duration=estimated_duration,
|
|
303
|
+
checkpoint_interval=checkpoint_interval,
|
|
304
|
+
)
|
|
305
|
+
m._enter()
|
|
306
|
+
try:
|
|
307
|
+
yield m
|
|
308
|
+
except Exception as e:
|
|
309
|
+
m._exit(error=e)
|
|
310
|
+
raise
|
|
311
|
+
else:
|
|
312
|
+
m._exit()
|
|
313
|
+
|
|
314
|
+
def continuous_mission(
|
|
315
|
+
self,
|
|
316
|
+
agent_id: str,
|
|
317
|
+
intent: str,
|
|
318
|
+
services: list[str],
|
|
319
|
+
rotation_interval: str = "4h",
|
|
320
|
+
will_delegate: bool = False,
|
|
321
|
+
checkpoint_interval: str | None = None,
|
|
322
|
+
) -> Generator[Mission, None, None]:
|
|
323
|
+
"""Generator yielding successive Mission objects with automatic rotation.
|
|
324
|
+
|
|
325
|
+
Each iteration issues a new passport, runs checkpoints, and checks out
|
|
326
|
+
when the rotation interval elapses (signaled via mission.should_rotate).
|
|
327
|
+
|
|
328
|
+
Usage::
|
|
329
|
+
|
|
330
|
+
for mission in stack.passports.continuous_mission(
|
|
331
|
+
agent_id="agt_...",
|
|
332
|
+
intent="Monitor Slack",
|
|
333
|
+
services=["slack"],
|
|
334
|
+
rotation_interval="4h",
|
|
335
|
+
):
|
|
336
|
+
while not mission.should_rotate:
|
|
337
|
+
event = wait_for_event()
|
|
338
|
+
mission.log("slack", "read_message")
|
|
339
|
+
process(event)
|
|
340
|
+
"""
|
|
341
|
+
rotation_secs = _parse_duration(rotation_interval)
|
|
342
|
+
rotation_count = 0
|
|
343
|
+
|
|
344
|
+
while True:
|
|
345
|
+
m = Mission(
|
|
346
|
+
self, agent_id, intent, services,
|
|
347
|
+
will_delegate=will_delegate,
|
|
348
|
+
checkpoint_interval=checkpoint_interval,
|
|
349
|
+
)
|
|
350
|
+
m.rotation_count = rotation_count
|
|
351
|
+
m._rotation_interval = rotation_secs
|
|
352
|
+
|
|
353
|
+
m._enter()
|
|
354
|
+
try:
|
|
355
|
+
yield m
|
|
356
|
+
finally:
|
|
357
|
+
m._exit()
|
|
358
|
+
|
|
359
|
+
rotation_count += 1
|
getstack/py.typed
ADDED
|
File without changes
|
getstack/reviews.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Review queue management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
from .types import ReviewQueue, ReviewQueueItem, ReviewDecisionResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReviewService:
|
|
12
|
+
def __init__(self, client: HttpClient):
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
def list(self, status: str = "flagged", page: int = 1, limit: int = 20) -> ReviewQueue:
|
|
16
|
+
data = self._client.get("/v1/passports/reviews", params={
|
|
17
|
+
"status": status,
|
|
18
|
+
"page": str(page),
|
|
19
|
+
"limit": str(limit),
|
|
20
|
+
})
|
|
21
|
+
items = [
|
|
22
|
+
ReviewQueueItem(
|
|
23
|
+
checkout_id=item["checkout_id"],
|
|
24
|
+
passport_jti=item["passport_jti"],
|
|
25
|
+
agent_id=item["agent_id"],
|
|
26
|
+
agent_name=item["agent_name"],
|
|
27
|
+
intent_summary=item["intent_summary"],
|
|
28
|
+
review_status=item["review_status"],
|
|
29
|
+
flags=item.get("flags", []),
|
|
30
|
+
submitted_at=item["submitted_at"],
|
|
31
|
+
)
|
|
32
|
+
for item in data["items"]
|
|
33
|
+
]
|
|
34
|
+
return ReviewQueue(items=items, total=data["total"])
|
|
35
|
+
|
|
36
|
+
def decide(
|
|
37
|
+
self,
|
|
38
|
+
checkout_id: str,
|
|
39
|
+
decision: str,
|
|
40
|
+
notes: str | None = None,
|
|
41
|
+
block_future: bool = False,
|
|
42
|
+
) -> ReviewDecisionResult:
|
|
43
|
+
body: dict[str, Any] = {"decision": decision}
|
|
44
|
+
if notes:
|
|
45
|
+
body["notes"] = notes
|
|
46
|
+
if block_future:
|
|
47
|
+
body["block_future"] = True
|
|
48
|
+
data = self._client.post(f"/v1/passports/reviews/{checkout_id}/decide", json=body)
|
|
49
|
+
return ReviewDecisionResult(
|
|
50
|
+
review_id=data["review_id"],
|
|
51
|
+
agent_id=data["agent_id"],
|
|
52
|
+
decision=data["decision"],
|
|
53
|
+
passport_blocked=data["passport_blocked"],
|
|
54
|
+
)
|
getstack/services.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Service management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServiceService:
|
|
11
|
+
def __init__(self, client: HttpClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def list(self) -> list[dict[str, Any]]:
|
|
15
|
+
return self._client.get("/v1/services/connections")
|
|
16
|
+
|
|
17
|
+
def list_available(self) -> list[dict[str, Any]]:
|
|
18
|
+
return self._client.get("/v1/services")
|
|
19
|
+
|
|
20
|
+
def list_templates(self) -> list[dict[str, Any]]:
|
|
21
|
+
return self._client.get("/v1/services/templates")
|
|
22
|
+
|
|
23
|
+
def connect_custom(
|
|
24
|
+
self,
|
|
25
|
+
name: str,
|
|
26
|
+
credential: str | dict[str, str],
|
|
27
|
+
description: str | None = None,
|
|
28
|
+
scopes: list[str] | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
body: dict[str, Any] = {"name": name, "credential": credential}
|
|
31
|
+
if description:
|
|
32
|
+
body["description"] = description
|
|
33
|
+
if scopes:
|
|
34
|
+
body["scopes"] = scopes
|
|
35
|
+
return self._client.post("/v1/services/custom", json=body)
|
|
36
|
+
|
|
37
|
+
def verify(self, connection_id: str) -> dict[str, Any]:
|
|
38
|
+
return self._client.post(f"/v1/services/{connection_id}/verify")
|
|
39
|
+
|
|
40
|
+
def disconnect(self, connection_id: str) -> dict[str, Any]:
|
|
41
|
+
return self._client.delete(f"/v1/services/connections/{connection_id}")
|
getstack/types.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""STACK SDK type definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Agent:
|
|
10
|
+
id: str
|
|
11
|
+
operator_id: str
|
|
12
|
+
name: str
|
|
13
|
+
description: str | None
|
|
14
|
+
status: str
|
|
15
|
+
accountability_mode: str
|
|
16
|
+
on_warning: str
|
|
17
|
+
on_critical: str
|
|
18
|
+
passport_blocked: bool
|
|
19
|
+
passport_blocked_reason: str | None
|
|
20
|
+
passport_blocked_at: str | None
|
|
21
|
+
created_at: str
|
|
22
|
+
updated_at: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Passport:
|
|
27
|
+
token: str
|
|
28
|
+
jti: str
|
|
29
|
+
expires_at: str
|
|
30
|
+
accountability: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class CheckpointResult:
|
|
35
|
+
checkpoint_id: str
|
|
36
|
+
passport_jti: str
|
|
37
|
+
new_expires_at: str | None = None
|
|
38
|
+
flags: list[str] | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class CheckoutResult:
|
|
43
|
+
checkout_id: str
|
|
44
|
+
passport_jti: str
|
|
45
|
+
review_status: str # clean | flagged | blocked
|
|
46
|
+
flags: list[dict[str, str]] | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ReviewFlag:
|
|
51
|
+
type: str
|
|
52
|
+
severity: str
|
|
53
|
+
message: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class PassportReport:
|
|
58
|
+
passport: dict[str, Any]
|
|
59
|
+
intent: dict[str, Any] | None
|
|
60
|
+
checkpoints: list[dict[str, Any]]
|
|
61
|
+
checkout: dict[str, Any] | None
|
|
62
|
+
review: dict[str, Any] | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ReviewQueueItem:
|
|
67
|
+
checkout_id: str
|
|
68
|
+
passport_jti: str
|
|
69
|
+
agent_id: str
|
|
70
|
+
agent_name: str
|
|
71
|
+
intent_summary: str
|
|
72
|
+
review_status: str
|
|
73
|
+
flags: list[dict[str, str]]
|
|
74
|
+
submitted_at: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ReviewQueue:
|
|
79
|
+
items: list[ReviewQueueItem]
|
|
80
|
+
total: int
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ReviewDecisionResult:
|
|
85
|
+
review_id: str
|
|
86
|
+
agent_id: str
|
|
87
|
+
decision: str
|
|
88
|
+
passport_blocked: bool
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Credential:
|
|
93
|
+
provider: str
|
|
94
|
+
credential: str | None = None
|
|
95
|
+
credentials: dict[str, str] | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Service:
|
|
100
|
+
id: str
|
|
101
|
+
provider: str
|
|
102
|
+
name: str
|
|
103
|
+
description: str
|
|
104
|
+
available: bool = True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class ServiceConnection:
|
|
109
|
+
id: str
|
|
110
|
+
provider: str
|
|
111
|
+
status: str
|
|
112
|
+
connected_at: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class Dropoff:
|
|
117
|
+
id: str
|
|
118
|
+
from_agent_id: str
|
|
119
|
+
to_agent_id: str
|
|
120
|
+
status: str
|
|
121
|
+
created_at: str
|
|
122
|
+
expires_at: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class NotificationChannel:
|
|
127
|
+
id: str
|
|
128
|
+
channel_type: str
|
|
129
|
+
destination: str
|
|
130
|
+
events: list[str]
|
|
131
|
+
min_severity: str
|
|
132
|
+
active: bool
|
|
133
|
+
verified: bool
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class AuditEntry:
|
|
138
|
+
entry_id: str
|
|
139
|
+
action: str
|
|
140
|
+
outcome: str
|
|
141
|
+
timestamp: int
|
|
142
|
+
agent_id: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class ToolCall:
|
|
147
|
+
"""A single tool call logged during a mission."""
|
|
148
|
+
service: str
|
|
149
|
+
method: str
|
|
150
|
+
target: str | None = None
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> dict[str, Any]:
|
|
153
|
+
d: dict[str, Any] = {"service": self.service, "method": self.method}
|
|
154
|
+
if self.target:
|
|
155
|
+
d["target"] = self.target
|
|
156
|
+
return d
|