getstack 0.5.1__tar.gz → 0.7.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.
Files changed (45) hide show
  1. {getstack-0.5.1 → getstack-0.7.0}/.gitignore +5 -0
  2. {getstack-0.5.1 → getstack-0.7.0}/PKG-INFO +1 -1
  3. {getstack-0.5.1 → getstack-0.7.0}/pyproject.toml +1 -1
  4. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/__init__.py +57 -4
  5. getstack-0.7.0/src/getstack/_async/__init__.py +14 -0
  6. getstack-0.7.0/src/getstack/_async/agents.py +53 -0
  7. getstack-0.7.0/src/getstack/_async/audit.py +46 -0
  8. getstack-0.7.0/src/getstack/_async/credentials.py +43 -0
  9. getstack-0.7.0/src/getstack/_async/dropoffs.py +49 -0
  10. getstack-0.7.0/src/getstack/_async/identity.py +92 -0
  11. getstack-0.7.0/src/getstack/_async/intents.py +128 -0
  12. getstack-0.7.0/src/getstack/_async/missions.py +110 -0
  13. getstack-0.7.0/src/getstack/_async/notifications.py +49 -0
  14. getstack-0.7.0/src/getstack/_async/passports.py +364 -0
  15. getstack-0.7.0/src/getstack/_async/proxy.py +73 -0
  16. getstack-0.7.0/src/getstack/_async/reviews.py +53 -0
  17. getstack-0.7.0/src/getstack/_async/scan.py +40 -0
  18. getstack-0.7.0/src/getstack/_async/security_events.py +26 -0
  19. getstack-0.7.0/src/getstack/_async/services.py +41 -0
  20. getstack-0.7.0/src/getstack/_async/skills.py +162 -0
  21. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/client.py +47 -13
  22. getstack-0.7.0/src/getstack/missions.py +130 -0
  23. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/passports.py +7 -3
  24. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/proxy.py +7 -1
  25. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/skills.py +21 -1
  26. {getstack-0.5.1 → getstack-0.7.0}/CLAUDE.md +0 -0
  27. {getstack-0.5.1 → getstack-0.7.0}/LICENSE +0 -0
  28. {getstack-0.5.1 → getstack-0.7.0}/README.md +0 -0
  29. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/agent_auth.py +0 -0
  30. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/agents.py +0 -0
  31. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/audit.py +0 -0
  32. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/auth.py +0 -0
  33. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/browser_bootstrap.py +0 -0
  34. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/credentials.py +0 -0
  35. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/dropoffs.py +0 -0
  36. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/errors.py +0 -0
  37. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/identity.py +0 -0
  38. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/intents.py +0 -0
  39. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/notifications.py +0 -0
  40. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/py.typed +0 -0
  41. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/reviews.py +0 -0
  42. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/scan.py +0 -0
  43. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/security_events.py +0 -0
  44. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/services.py +0 -0
  45. {getstack-0.5.1 → getstack-0.7.0}/src/getstack/types.py +0 -0
@@ -60,6 +60,11 @@ scripts/.openapi-cache/
60
60
  # Local dev screenshots
61
61
  *-stripe.png
62
62
 
63
+ # Root-level scratch images (founder workspace — never commit)
64
+ /*.png
65
+ /*.jpg
66
+ /*.jpeg
67
+
63
68
  # YC application session exports (founder voice + redacted creds — never commit)
64
69
  docs/yc-q18/
65
70
  docs/yc-application-session-preface.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.5.1
3
+ Version: 0.7.0
4
4
  Summary: Python SDK for STACK — trust infrastructure for AI agents
5
5
  Project-URL: Homepage, https://getstack.run
6
6
  Project-URL: Documentation, https://getstack.run/docs/sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "0.5.1"
7
+ version = "0.7.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -52,6 +52,7 @@ from .security_events import SecurityEventService as SecurityEventSvc
52
52
  from .skills import SkillService
53
53
  from .identity import IdentityService
54
54
  from .intents import IntentsService
55
+ from .missions import MissionsService
55
56
  from .types import (
56
57
  Agent,
57
58
  Passport,
@@ -216,6 +217,7 @@ class Stack:
216
217
  self.skills = SkillService(self._client)
217
218
  self.identity = IdentityService(self._client)
218
219
  self.intents = IntentsService(self._client)
220
+ self.missions = MissionsService(self._client)
219
221
 
220
222
  @classmethod
221
223
  def from_session(cls, session_token: str, **kwargs) -> Stack:
@@ -256,15 +258,31 @@ class Stack:
256
258
  class AsyncStack:
257
259
  """Asynchronous STACK client.
258
260
 
259
- Uses httpx.AsyncClient under the hood. All service methods are sync
260
- (they use the sync HttpClient wrapper) — for true async, the async
261
- client needs async service modules. This provides the async HTTP
262
- transport layer for future async service implementations.
261
+ Sub-chunk 2.4 full parity with Stack. All 14 typed services are
262
+ async-native, plus AsyncMission for the bounded-mission context
263
+ manager. Uses httpx.AsyncClient under the hood.
263
264
 
264
265
  Args:
265
266
  api_key: A STACK API key (``sk_live_...``).
266
267
  base_url: Override the API base URL.
267
268
  timeout: HTTP timeout in seconds.
269
+
270
+ Example::
271
+
272
+ from getstack import AsyncStack
273
+
274
+ async with AsyncStack(api_key="sk_live_...") as stack:
275
+ agents = await stack.agents.list()
276
+ async with stack.passports.mission(
277
+ agent_id=agents[0].id,
278
+ intent="Process invoices",
279
+ services=["slack", "stripe"],
280
+ ) as mission:
281
+ resp = await mission.proxy(
282
+ "stripe", "https://api.stripe.com/v1/charges",
283
+ method="POST", body={"amount": 50},
284
+ _proxy_service=stack.proxy,
285
+ )
268
286
  """
269
287
 
270
288
  def __init__(
@@ -293,6 +311,41 @@ class AsyncStack:
293
311
 
294
312
  self._client = AsyncHttpClient(auth, base_url=base_url, timeout=timeout)
295
313
 
314
+ # Sub-chunk 2.4 — 14 typed async services attached eagerly,
315
+ # matching Stack's posture. Footprint is small; lazy init
316
+ # would only buy us memory we don't need to save.
317
+ from ._async.agents import AsyncAgentService
318
+ from ._async.passports import AsyncPassportService
319
+ from ._async.credentials import AsyncCredentialService
320
+ from ._async.services import AsyncServiceService
321
+ from ._async.dropoffs import AsyncDropoffService
322
+ from ._async.reviews import AsyncReviewService
323
+ from ._async.notifications import AsyncNotificationService
324
+ from ._async.audit import AsyncAuditService
325
+ from ._async.proxy import AsyncProxyService
326
+ from ._async.scan import AsyncScanService
327
+ from ._async.security_events import AsyncSecurityEventService
328
+ from ._async.skills import AsyncSkillService
329
+ from ._async.identity import AsyncIdentityService
330
+ from ._async.intents import AsyncIntentsService
331
+ from ._async.missions import AsyncMissionsService
332
+
333
+ self.agents = AsyncAgentService(self._client)
334
+ self.passports = AsyncPassportService(self._client)
335
+ self.credentials = AsyncCredentialService(self._client)
336
+ self.services = AsyncServiceService(self._client)
337
+ self.dropoffs = AsyncDropoffService(self._client)
338
+ self.reviews = AsyncReviewService(self._client)
339
+ self.notifications = AsyncNotificationService(self._client)
340
+ self.audit = AsyncAuditService(self._client)
341
+ self.proxy = AsyncProxyService(self._client)
342
+ self.scan = AsyncScanService(self._client)
343
+ self.security_events = AsyncSecurityEventService(self._client)
344
+ self.skills = AsyncSkillService(self._client)
345
+ self.identity = AsyncIdentityService(self._client)
346
+ self.intents = AsyncIntentsService(self._client)
347
+ self.missions = AsyncMissionsService(self._client)
348
+
296
349
  @classmethod
297
350
  def from_session(cls, session_token: str, **kwargs) -> AsyncStack:
298
351
  """Create an async client authenticated with a session JWT."""
@@ -0,0 +1,14 @@
1
+ """Async service modules (Sub-chunk 2.4 — full parity with sync).
2
+
3
+ Each AsyncXxxService mirrors the sync XxxService surface exactly:
4
+ same method names, same kwargs, same return types. The only
5
+ difference is `async def` and `await self._client.x` instead of
6
+ `self._client.x`. The Mission ergonomic gets an AsyncMission
7
+ context manager that uses asyncio.create_task + asyncio.sleep
8
+ instead of threading.Timer.
9
+
10
+ Import via AsyncStack:
11
+ from getstack import AsyncStack
12
+ async with AsyncStack(api_key="sk_live_...") as stack:
13
+ agents = await stack.agents.list()
14
+ """
@@ -0,0 +1,53 @@
1
+ """Async mirror of AgentService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+ from ..types import Agent
9
+ from ..agents import _to_agent
10
+
11
+
12
+ class AsyncAgentService:
13
+ def __init__(self, client: AsyncHttpClient):
14
+ self._client = client
15
+
16
+ async def register(
17
+ self,
18
+ name: str,
19
+ description: str | None = None,
20
+ accountability_mode: str = "enforced",
21
+ on_warning: str = "notify",
22
+ on_critical: str = "notify",
23
+ ) -> Agent:
24
+ body: dict[str, Any] = {"name": name, "accountability_mode": accountability_mode}
25
+ if description:
26
+ body["description"] = description
27
+ if on_warning != "notify":
28
+ body["on_warning"] = on_warning
29
+ if on_critical != "notify":
30
+ body["on_critical"] = on_critical
31
+ return _to_agent(await self._client.post("/v1/agents/register", json=body))
32
+
33
+ async def get(self, agent_id: str) -> Agent:
34
+ return _to_agent(await self._client.get(f"/v1/agents/{agent_id}"))
35
+
36
+ async def list(self) -> list[Agent]:
37
+ data = await self._client.get("/v1/agents")
38
+ return [_to_agent(a) for a in data]
39
+
40
+ async def update(self, agent_id: str, **kwargs: Any) -> Agent:
41
+ return _to_agent(await self._client.patch(f"/v1/agents/{agent_id}", json=kwargs))
42
+
43
+ async def suspend(self, agent_id: str) -> Agent:
44
+ return await self.update(agent_id, status="suspended")
45
+
46
+ async def activate(self, agent_id: str) -> Agent:
47
+ return await self.update(agent_id, status="active")
48
+
49
+ async def delete(self, agent_id: str) -> dict[str, Any]:
50
+ return await self._client.delete(f"/v1/agents/{agent_id}")
51
+
52
+ async def unblock(self, agent_id: str) -> dict[str, Any]:
53
+ return await self._client.post(f"/v1/agents/{agent_id}/unblock")
@@ -0,0 +1,46 @@
1
+ """Async mirror of AuditService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncAuditService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def list(
15
+ self,
16
+ limit: int = 20,
17
+ since: int | None = None,
18
+ agent_id: str | None = None,
19
+ passport_jti: str | None = None,
20
+ ) -> dict[str, Any]:
21
+ params: dict[str, str] = {"limit": str(limit)}
22
+ if since is not None:
23
+ params["since"] = str(since)
24
+ if agent_id is not None:
25
+ params["agent_id"] = agent_id
26
+ if passport_jti is not None:
27
+ params["passport_jti"] = passport_jti
28
+ return await self._client.get("/v1/audit", params=params)
29
+
30
+ async def chain_head(self) -> dict[str, Any]:
31
+ return await self._client.get("/v1/audit/chain-head")
32
+
33
+ async def verify_chain(
34
+ self,
35
+ from_date: str | None = None,
36
+ to_date: str | None = None,
37
+ limit: int | None = None,
38
+ ) -> dict[str, Any]:
39
+ params: dict[str, str] = {}
40
+ if from_date is not None:
41
+ params["from"] = from_date
42
+ if to_date is not None:
43
+ params["to"] = to_date
44
+ if limit is not None:
45
+ params["limit"] = str(limit)
46
+ return await self._client.get("/v1/audit/verify-chain", params=params)
@@ -0,0 +1,43 @@
1
+ """Async mirror of CredentialService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import quote
6
+
7
+ from ..client import AsyncHttpClient
8
+ from ..types import Credential
9
+
10
+
11
+ class AsyncCredentialService:
12
+ def __init__(self, client: AsyncHttpClient):
13
+ self._client = client
14
+
15
+ async def get(self, provider: str, passport_token: str | None = None) -> Credential:
16
+ extra_headers: dict[str, str] = {}
17
+ if passport_token:
18
+ extra_headers["X-Passport-Token"] = passport_token
19
+ data = await self._client.get(
20
+ f"/v1/credentials/{quote(provider)}",
21
+ extra_headers=extra_headers if extra_headers else None,
22
+ )
23
+ return Credential(
24
+ provider=data["provider"],
25
+ credential=data.get("credential"),
26
+ credentials=data.get("credentials"),
27
+ )
28
+
29
+ async def get_by_connection(
30
+ self, connection_id: str, passport_token: str | None = None
31
+ ) -> Credential:
32
+ extra_headers: dict[str, str] = {}
33
+ if passport_token:
34
+ extra_headers["X-Passport-Token"] = passport_token
35
+ data = await self._client.get(
36
+ f"/v1/credentials/by-connection/{connection_id}",
37
+ extra_headers=extra_headers if extra_headers else None,
38
+ )
39
+ return Credential(
40
+ provider=data["provider"],
41
+ credential=data.get("credential"),
42
+ credentials=data.get("credentials"),
43
+ )
@@ -0,0 +1,49 @@
1
+ """Async mirror of DropoffService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncDropoffService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def create(
15
+ self,
16
+ from_agent_id: str,
17
+ to_agent_id: str,
18
+ schema: dict[str, Any],
19
+ ttl_seconds: int | None = None,
20
+ on_expire: str | None = None,
21
+ ) -> dict[str, Any]:
22
+ body: dict[str, Any] = {
23
+ "from_agent": from_agent_id,
24
+ "to_agent": to_agent_id,
25
+ "schema": schema,
26
+ }
27
+ if ttl_seconds:
28
+ body["ttl_seconds"] = ttl_seconds
29
+ if on_expire:
30
+ body["on_expire"] = on_expire
31
+ return await self._client.post("/v1/dropoffs", json=body)
32
+
33
+ async def deposit(self, dropoff_id: str, data: dict[str, Any], agent_id: str) -> dict[str, Any]:
34
+ return await self._client.post(
35
+ f"/v1/dropoffs/{dropoff_id}/deposit",
36
+ json={"agent_id": agent_id, "payload": data},
37
+ )
38
+
39
+ async def collect(self, dropoff_id: str, agent_id: str) -> dict[str, Any]:
40
+ return await self._client.post(
41
+ f"/v1/dropoffs/{dropoff_id}/collect",
42
+ json={"agent_id": agent_id},
43
+ )
44
+
45
+ async def get(self, dropoff_id: str) -> dict[str, Any]:
46
+ return await self._client.get(f"/v1/dropoffs/{dropoff_id}")
47
+
48
+ async def list(self) -> list[dict[str, Any]]:
49
+ return await self._client.get("/v1/dropoffs")
@@ -0,0 +1,92 @@
1
+ """Async mirror of IdentityService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+ from ..types import IdentityProvider, IdentityClaim, IdentitySettings, VerificationSession
9
+ from ..identity import _to_provider, _to_claim
10
+
11
+
12
+ class AsyncIdentityService:
13
+ def __init__(self, client: AsyncHttpClient):
14
+ self._client = client
15
+
16
+ async def get_settings(self) -> IdentitySettings:
17
+ data = await self._client.get("/v1/operators/me/identity-settings")
18
+ return IdentitySettings(
19
+ identity_claim_ttl=data["identity_claim_ttl"],
20
+ identity_claim_inheritance=data["identity_claim_inheritance"],
21
+ identity_auto_revoke=data["identity_auto_revoke"],
22
+ )
23
+
24
+ async def update_settings(
25
+ self,
26
+ *,
27
+ identity_claim_ttl: str | None = None,
28
+ identity_claim_inheritance: str | None = None,
29
+ identity_auto_revoke: bool | None = None,
30
+ ) -> IdentitySettings:
31
+ body: dict[str, Any] = {}
32
+ if identity_claim_ttl is not None:
33
+ body["identity_claim_ttl"] = identity_claim_ttl
34
+ if identity_claim_inheritance is not None:
35
+ body["identity_claim_inheritance"] = identity_claim_inheritance
36
+ if identity_auto_revoke is not None:
37
+ body["identity_auto_revoke"] = identity_auto_revoke
38
+ data = await self._client.patch("/v1/operators/me/identity-settings", json=body)
39
+ return IdentitySettings(
40
+ identity_claim_ttl=data["identity_claim_ttl"],
41
+ identity_claim_inheritance=data["identity_claim_inheritance"],
42
+ identity_auto_revoke=data["identity_auto_revoke"],
43
+ )
44
+
45
+ async def list_providers(self) -> list[IdentityProvider]:
46
+ data = await self._client.get("/v1/identity/providers")
47
+ return [_to_provider(p) for p in data]
48
+
49
+ async def initiate_verification(
50
+ self,
51
+ provider_key: str,
52
+ *,
53
+ requested_claims: list[str] | None = None,
54
+ return_url: str | None = None,
55
+ ) -> VerificationSession:
56
+ body: dict[str, Any] = {"provider_key": provider_key}
57
+ if requested_claims:
58
+ body["requested_claims"] = requested_claims
59
+ if return_url:
60
+ body["return_url"] = return_url
61
+ data = await self._client.post("/v1/identity/verify/initiate", json=body)
62
+ return VerificationSession(
63
+ session_ref=data["session_ref"],
64
+ authorization_url=data.get("authorization_url"),
65
+ status=data.get("status", "pending"),
66
+ )
67
+
68
+ async def complete_verification(self, provider_key: str, session_ref: str) -> dict[str, Any]:
69
+ return await self._client.post(
70
+ "/v1/identity/verify/complete",
71
+ json={"provider_key": provider_key, "session_ref": session_ref},
72
+ )
73
+
74
+ async def grant_delegation(self, delegated_scopes: list[str]) -> IdentityClaim:
75
+ data = await self._client.post(
76
+ "/v1/identity/delegate",
77
+ json={"delegated_scopes": delegated_scopes},
78
+ )
79
+ return _to_claim(data)
80
+
81
+ async def list_claims(self) -> list[IdentityClaim]:
82
+ data = await self._client.get("/v1/identity/claims")
83
+ return [_to_claim(c) for c in data]
84
+
85
+ async def revoke_claim(self, claim_id: str) -> dict[str, Any]:
86
+ return await self._client.delete(f"/v1/identity/claims/{claim_id}")
87
+
88
+ async def get_service_requirements(self, service_id: str) -> dict[str, Any]:
89
+ return await self._client.get(f"/v1/services/{service_id}/requirements")
90
+
91
+ async def set_service_requirement(self, service_id: str, **kwargs: Any) -> dict[str, Any]:
92
+ return await self._client.post(f"/v1/services/{service_id}/requirements", json=kwargs)
@@ -0,0 +1,128 @@
1
+ """Async mirror of IntentsService. asyncio.sleep for the poll loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ from ..client import AsyncHttpClient
9
+
10
+
11
+ class AsyncIntentsService:
12
+ def __init__(self, client: AsyncHttpClient):
13
+ self._client = client
14
+
15
+ async def simulate(
16
+ self,
17
+ intent: dict[str, Any],
18
+ passport_token: str,
19
+ *,
20
+ intent_claim_ref: str | None = None,
21
+ persist: bool | None = None,
22
+ ) -> dict[str, Any]:
23
+ body: dict[str, Any] = {"intent": intent}
24
+ if intent_claim_ref is not None:
25
+ body["intent_claim_ref"] = intent_claim_ref
26
+ if persist is not None:
27
+ body["persist"] = persist
28
+ return await self._client.request(
29
+ "POST",
30
+ "/v1/intents/simulate",
31
+ json=body,
32
+ extra_headers={"X-Passport-Token": passport_token},
33
+ )
34
+
35
+ async def submit(
36
+ self,
37
+ intent: dict[str, Any],
38
+ passport_token: str,
39
+ *,
40
+ expires_in_seconds: int | None = None,
41
+ skip_simulation: bool = False,
42
+ ) -> dict[str, Any]:
43
+ body: dict[str, Any] = {"intent": intent}
44
+ if expires_in_seconds is not None:
45
+ body["expires_in_seconds"] = expires_in_seconds
46
+ if skip_simulation:
47
+ body["skip_simulation"] = True
48
+ return await self._client.request(
49
+ "POST",
50
+ "/v1/intents/submit",
51
+ json=body,
52
+ extra_headers={"X-Passport-Token": passport_token},
53
+ )
54
+
55
+ async def submit_and_wait(
56
+ self,
57
+ intent: dict[str, Any],
58
+ passport_token: str,
59
+ *,
60
+ expires_in_seconds: int | None = None,
61
+ skip_simulation: bool = False,
62
+ timeout_seconds: float = 60.0,
63
+ poll_interval_seconds: float = 1.0,
64
+ ) -> dict[str, Any]:
65
+ """Async submit + poll. Uses asyncio.sleep instead of time.sleep
66
+ so the event loop stays free during the wait."""
67
+ initial = await self.submit(
68
+ intent,
69
+ passport_token,
70
+ expires_in_seconds=expires_in_seconds,
71
+ skip_simulation=skip_simulation,
72
+ )
73
+ approval_id = initial["approval_id"]
74
+ loop = asyncio.get_event_loop()
75
+ deadline = loop.time() + timeout_seconds
76
+ last_row: dict[str, Any] | None = None
77
+ while loop.time() < deadline:
78
+ row = await self.get(approval_id)
79
+ last_row = row
80
+ if row.get("status") != "pending":
81
+ return {"initial": initial, "final": row, "timed_out": False}
82
+ await asyncio.sleep(poll_interval_seconds)
83
+ return {
84
+ "initial": initial,
85
+ "final": last_row or {"id": approval_id, "status": "pending"},
86
+ "timed_out": True,
87
+ }
88
+
89
+ async def get(self, approval_id: str) -> dict[str, Any]:
90
+ return await self._client.request("GET", f"/v1/intents/{approval_id}")
91
+
92
+ async def list_pending(
93
+ self, *, page: int | None = None, limit: int | None = None
94
+ ) -> dict[str, Any]:
95
+ params: dict[str, Any] = {}
96
+ if page is not None:
97
+ params["page"] = page
98
+ if limit is not None:
99
+ params["limit"] = limit
100
+ return await self._client.request("GET", "/v1/intents/pending", params=params or None)
101
+
102
+ async def approve(self, approval_id: str, *, notes: str | None = None) -> dict[str, Any]:
103
+ body: dict[str, Any] = {}
104
+ if notes is not None:
105
+ body["notes"] = notes
106
+ return await self._client.request(
107
+ "POST",
108
+ f"/v1/intents/{approval_id}/approve",
109
+ json=body,
110
+ )
111
+
112
+ async def reject(
113
+ self,
114
+ approval_id: str,
115
+ *,
116
+ notes: str | None = None,
117
+ block_future: bool = False,
118
+ ) -> dict[str, Any]:
119
+ body: dict[str, Any] = {}
120
+ if notes is not None:
121
+ body["notes"] = notes
122
+ if block_future:
123
+ body["block_future"] = True
124
+ return await self._client.request(
125
+ "POST",
126
+ f"/v1/intents/{approval_id}/reject",
127
+ json=body,
128
+ )
@@ -0,0 +1,110 @@
1
+ """Async mirror of MissionsService. Same surface, async/await."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncMissionsService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def open(
15
+ self,
16
+ intent_summary: str,
17
+ *,
18
+ intent_services: list[str] | None = None,
19
+ expected_counterparties: list[str] | None = None,
20
+ baseline_profile: dict[str, Any] | None = None,
21
+ expected_output_shape: dict[str, Any] | None = None,
22
+ max_actions: int | None = None,
23
+ max_dropoff_bytes: int | None = None,
24
+ coverage_cap_cents: int | None = None,
25
+ max_duration_seconds: int | None = None,
26
+ max_concurrent_intents: int | None = None,
27
+ max_actions_per_service: dict[str, int] | None = None,
28
+ cap_behavior: dict[str, str] | None = None,
29
+ expires_in_seconds: int | None = None,
30
+ parent_passport_services: list[str] | None = None,
31
+ ) -> dict[str, Any]:
32
+ body: dict[str, Any] = {"intent_summary": intent_summary}
33
+ if intent_services is not None:
34
+ body["intent_services"] = intent_services
35
+ if expected_counterparties is not None:
36
+ body["expected_counterparties"] = expected_counterparties
37
+ if baseline_profile is not None:
38
+ body["baseline_profile"] = baseline_profile
39
+ if expected_output_shape is not None:
40
+ body["expected_output_shape"] = expected_output_shape
41
+ if max_actions is not None:
42
+ body["max_actions"] = max_actions
43
+ if max_dropoff_bytes is not None:
44
+ body["max_dropoff_bytes"] = max_dropoff_bytes
45
+ if coverage_cap_cents is not None:
46
+ body["coverage_cap_cents"] = coverage_cap_cents
47
+ if max_duration_seconds is not None:
48
+ body["max_duration_seconds"] = max_duration_seconds
49
+ if max_concurrent_intents is not None:
50
+ body["max_concurrent_intents"] = max_concurrent_intents
51
+ if max_actions_per_service is not None:
52
+ body["max_actions_per_service"] = max_actions_per_service
53
+ if cap_behavior is not None:
54
+ body["cap_behavior"] = cap_behavior
55
+ if expires_in_seconds is not None:
56
+ body["expires_in_seconds"] = expires_in_seconds
57
+ if parent_passport_services is not None:
58
+ body["parent_passport_services"] = parent_passport_services
59
+ return await self._client.request("POST", "/v1/missions", json=body)
60
+
61
+ async def complete(
62
+ self,
63
+ mission_id: str,
64
+ *,
65
+ output: dict[str, Any] | None = None,
66
+ ) -> dict[str, Any]:
67
+ body: dict[str, Any] = {}
68
+ if output is not None:
69
+ body["output"] = output
70
+ return await self._client.request(
71
+ "POST", f"/v1/missions/{mission_id}/complete", json=body
72
+ )
73
+
74
+ async def revoke(
75
+ self,
76
+ mission_id: str,
77
+ *,
78
+ reason: str,
79
+ notes: str | None = None,
80
+ ) -> dict[str, Any]:
81
+ body: dict[str, Any] = {"reason": reason}
82
+ if notes is not None:
83
+ body["notes"] = notes
84
+ return await self._client.request(
85
+ "POST", f"/v1/missions/{mission_id}/revoke", json=body
86
+ )
87
+
88
+ async def get(self, mission_id: str) -> dict[str, Any]:
89
+ return await self._client.request("GET", f"/v1/missions/{mission_id}")
90
+
91
+ async def list(
92
+ self,
93
+ *,
94
+ status: str | None = None,
95
+ page: int | None = None,
96
+ limit: int | None = None,
97
+ ) -> dict[str, Any]:
98
+ params: dict[str, Any] = {}
99
+ if status is not None:
100
+ params["status"] = status
101
+ if page is not None:
102
+ params["page"] = page
103
+ if limit is not None:
104
+ params["limit"] = limit
105
+ path = "/v1/missions"
106
+ if params:
107
+ from urllib.parse import urlencode
108
+
109
+ path = f"{path}?{urlencode(params)}"
110
+ return await self._client.request("GET", path)