getstack 1.4.0__tar.gz → 3.0.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 (93) hide show
  1. {getstack-1.4.0 → getstack-3.0.0}/PKG-INFO +12 -5
  2. {getstack-1.4.0 → getstack-3.0.0}/README.md +11 -4
  3. {getstack-1.4.0 → getstack-3.0.0}/pyproject.toml +1 -1
  4. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/__init__.py +21 -6
  5. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/dropoffs.py +10 -2
  6. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/missions.py +23 -3
  7. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/partner_tenants.py +26 -7
  8. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/passport_session.py +14 -7
  9. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/passports.py +169 -59
  10. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/dropoffs.py +20 -4
  11. getstack-3.0.0/src/getstack/errors.py +145 -0
  12. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/llm.py +2 -1
  13. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/missions.py +77 -13
  14. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/partner_connect.py +3 -2
  15. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/partner_tenants.py +44 -10
  16. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/passport_session.py +19 -9
  17. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/passports.py +297 -68
  18. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/proxy.py +3 -1
  19. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/scan.py +3 -1
  20. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/usage.py +4 -0
  21. getstack-3.0.0/tests/test_dropoffs.py +117 -0
  22. getstack-3.0.0/tests/test_mission_binding.py +339 -0
  23. {getstack-1.4.0 → getstack-3.0.0}/tests/test_parity.py +52 -4
  24. getstack-3.0.0/tests/test_partner_signing.py +69 -0
  25. getstack-1.4.0/src/getstack/errors.py +0 -79
  26. getstack-1.4.0/tests/test_dropoffs.py +0 -70
  27. getstack-1.4.0/tests/test_partner_signing.py +0 -36
  28. {getstack-1.4.0 → getstack-3.0.0}/.gitignore +0 -0
  29. {getstack-1.4.0 → getstack-3.0.0}/CLAUDE.md +0 -0
  30. {getstack-1.4.0 → getstack-3.0.0}/LICENSE +0 -0
  31. {getstack-1.4.0 → getstack-3.0.0}/scripts/verify_sign_claim_crosslang.py +0 -0
  32. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/__init__.py +0 -0
  33. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/agents.py +0 -0
  34. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/audit.py +0 -0
  35. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/authority.py +0 -0
  36. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/credentials.py +0 -0
  37. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/delivery_methods.py +0 -0
  38. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/detector_config.py +0 -0
  39. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/evidence_packs.py +0 -0
  40. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/fleets.py +0 -0
  41. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/identity.py +0 -0
  42. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/inbound_webhooks.py +0 -0
  43. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/intents.py +0 -0
  44. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/llm.py +0 -0
  45. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/notifications.py +0 -0
  46. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/partner_connect.py +0 -0
  47. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/pii_config.py +0 -0
  48. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/proxy.py +0 -0
  49. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/reviews.py +0 -0
  50. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/scan.py +0 -0
  51. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/security_events.py +0 -0
  52. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/services.py +0 -0
  53. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/skills.py +0 -0
  54. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/team.py +0 -0
  55. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_async/usage.py +0 -0
  56. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_authority_request.py +0 -0
  57. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/_fleet_key.py +0 -0
  58. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/agent_auth.py +0 -0
  59. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/agents.py +0 -0
  60. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/audit.py +0 -0
  61. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/auth.py +0 -0
  62. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/authority.py +0 -0
  63. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/browser_bootstrap.py +0 -0
  64. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/client.py +0 -0
  65. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/credentials.py +0 -0
  66. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/delivery_methods.py +0 -0
  67. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/detector_config.py +0 -0
  68. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/evidence_packs.py +0 -0
  69. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/fleets.py +0 -0
  70. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/identity.py +0 -0
  71. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/inbound_webhooks.py +0 -0
  72. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/intents.py +0 -0
  73. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/notifications.py +0 -0
  74. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/passport_header_verify.py +0 -0
  75. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/pii_config.py +0 -0
  76. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/py.typed +0 -0
  77. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/reviews.py +0 -0
  78. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/security_events.py +0 -0
  79. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/services.py +0 -0
  80. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/sign_claim.py +0 -0
  81. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/skills.py +0 -0
  82. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/team.py +0 -0
  83. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/types.py +0 -0
  84. {getstack-1.4.0 → getstack-3.0.0}/src/getstack/verify_offline.py +0 -0
  85. {getstack-1.4.0 → getstack-3.0.0}/tests/test_authority.py +0 -0
  86. {getstack-1.4.0 → getstack-3.0.0}/tests/test_delivery_methods.py +0 -0
  87. {getstack-1.4.0 → getstack-3.0.0}/tests/test_errors.py +0 -0
  88. {getstack-1.4.0 → getstack-3.0.0}/tests/test_evidence_packs.py +0 -0
  89. {getstack-1.4.0 → getstack-3.0.0}/tests/test_fleets.py +0 -0
  90. {getstack-1.4.0 → getstack-3.0.0}/tests/test_identity_requirements.py +0 -0
  91. {getstack-1.4.0 → getstack-3.0.0}/tests/test_inbound_webhooks.py +0 -0
  92. {getstack-1.4.0 → getstack-3.0.0}/tests/test_missions.py +0 -0
  93. {getstack-1.4.0 → getstack-3.0.0}/tests/test_sign_claim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: getstack
3
- Version: 1.4.0
3
+ Version: 3.0.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
@@ -125,8 +125,15 @@ stack.agents.list()
125
125
  stack.agents.update(agent_id, **fields)
126
126
  stack.agents.unblock(agent_id)
127
127
 
128
+ # Missions (every passport is issued for exactly one Mission)
129
+ stack.missions.open(intent_summary, agent_id=None, intent_services=None, ...)
130
+ stack.missions.start(agent_id=agent_id, mission={"intent_summary": "..."}, passport=None)
131
+ stack.missions.complete(mission_id) # also revokes the Mission's passports
132
+ stack.missions.revoke(mission_id, reason="operator_initiated_clean")
133
+
128
134
  # Passports
129
- stack.passports.issue(agent_id, intent=None, services=None, ...)
135
+ stack.passports.issue(agent_id, mission_ids=[mission_id], intent=None, services=None, ...)
136
+ stack.passports.mission(agent_id, intent, services, ...) # context manager: Mission + passport, closed on exit
130
137
  stack.passports.verify(token)
131
138
  stack.passports.revoke(jti, reason=None)
132
139
  stack.passports.checkpoint(jti, services_used, actions_count, ...)
@@ -148,9 +155,9 @@ stack.reviews.list(status="flagged")
148
155
  stack.reviews.decide(checkout_id, decision, notes=None, block_future=False)
149
156
 
150
157
  # Drop-offs
151
- stack.dropoffs.create(from_agent_id, to_agent_id, schema, ...)
152
- stack.dropoffs.deposit(dropoff_id, data, agent_id)
153
- stack.dropoffs.collect(dropoff_id, agent_id)
158
+ stack.dropoffs.create(from_agent_id, to_agent_id, schema, ..., passport_token=None)
159
+ stack.dropoffs.deposit(dropoff_id, data, agent_id, passport_token=None)
160
+ stack.dropoffs.collect(dropoff_id, agent_id, passport_token=None)
154
161
 
155
162
  # Notifications
156
163
  stack.notifications.list()
@@ -98,8 +98,15 @@ stack.agents.list()
98
98
  stack.agents.update(agent_id, **fields)
99
99
  stack.agents.unblock(agent_id)
100
100
 
101
+ # Missions (every passport is issued for exactly one Mission)
102
+ stack.missions.open(intent_summary, agent_id=None, intent_services=None, ...)
103
+ stack.missions.start(agent_id=agent_id, mission={"intent_summary": "..."}, passport=None)
104
+ stack.missions.complete(mission_id) # also revokes the Mission's passports
105
+ stack.missions.revoke(mission_id, reason="operator_initiated_clean")
106
+
101
107
  # Passports
102
- stack.passports.issue(agent_id, intent=None, services=None, ...)
108
+ stack.passports.issue(agent_id, mission_ids=[mission_id], intent=None, services=None, ...)
109
+ stack.passports.mission(agent_id, intent, services, ...) # context manager: Mission + passport, closed on exit
103
110
  stack.passports.verify(token)
104
111
  stack.passports.revoke(jti, reason=None)
105
112
  stack.passports.checkpoint(jti, services_used, actions_count, ...)
@@ -121,9 +128,9 @@ stack.reviews.list(status="flagged")
121
128
  stack.reviews.decide(checkout_id, decision, notes=None, block_future=False)
122
129
 
123
130
  # Drop-offs
124
- stack.dropoffs.create(from_agent_id, to_agent_id, schema, ...)
125
- stack.dropoffs.deposit(dropoff_id, data, agent_id)
126
- stack.dropoffs.collect(dropoff_id, agent_id)
131
+ stack.dropoffs.create(from_agent_id, to_agent_id, schema, ..., passport_token=None)
132
+ stack.dropoffs.deposit(dropoff_id, data, agent_id, passport_token=None)
133
+ stack.dropoffs.collect(dropoff_id, agent_id, passport_token=None)
127
134
 
128
135
  # Notifications
129
136
  stack.notifications.list()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "1.4.0"
7
+ version = "3.0.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -52,7 +52,7 @@ from .auth import ApiKeyAuth, SessionAuth, OAuthAuth, CredentialsFileAuth
52
52
  from .agent_auth import AgentKeypairAuth
53
53
  from .client import HttpClient, AsyncHttpClient, DEFAULT_BASE_URL
54
54
  from .agents import AgentService
55
- from .passports import PassportService, Mission
55
+ from .passports import PassportService, Mission, _require_one_mission
56
56
  from .credentials import CredentialService
57
57
  from .services import ServiceService
58
58
  from .dropoffs import DropoffService
@@ -119,6 +119,7 @@ from .errors import (
119
119
  ForbiddenError,
120
120
  ValidationError,
121
121
  PassportBlockedError,
122
+ MissionEndedError,
122
123
  )
123
124
 
124
125
  try:
@@ -222,6 +223,7 @@ __all__ = [
222
223
  "ForbiddenError",
223
224
  "ValidationError",
224
225
  "PassportBlockedError",
226
+ "MissionEndedError",
225
227
  ]
226
228
 
227
229
 
@@ -301,18 +303,25 @@ class Stack:
301
303
  ttl_seconds: int | None = None,
302
304
  accountability_mode: str | None = None,
303
305
  authority_binding_ids: list[str] | None = None,
306
+ mission_ids: list[str] | None = None,
304
307
  **kwargs: Any,
305
308
  ) -> PassportSession:
306
309
  """Phase 5.2.6 — issue a passport and return a PassportSession
307
310
  that holds the token + auto-refreshes ~60s before expiry. Pass
308
311
  the returned session to LLM calls instead of threading a raw
309
- token per call::
312
+ token per call. ``mission_ids`` must hold exactly one Mission id;
313
+ once that Mission ends, refresh raises ``MissionEndedError``::
310
314
 
311
- session = stack.issue_passport(agent_id="agt_x", services=[...])
315
+ mission = stack.missions.open("Answer support tickets", agent_id="agt_x")
316
+ session = stack.issue_passport(agent_id="agt_x", mission_ids=[mission["mission_id"]])
312
317
  for i in range(1000):
313
318
  stack.llm.anthropic.messages.create(body, passport=session)
314
319
  """
315
- issue_input: dict[str, Any] = {"agent_id": agent_id, **kwargs}
320
+ issue_input: dict[str, Any] = {
321
+ "agent_id": agent_id,
322
+ **kwargs,
323
+ "mission_ids": _require_one_mission(mission_ids),
324
+ }
316
325
  if services is not None: issue_input["services"] = services
317
326
  if intents is not None: issue_input["intents"] = intents
318
327
  if ttl_seconds is not None: issue_input["ttl_seconds"] = ttl_seconds
@@ -483,10 +492,16 @@ class AsyncStack:
483
492
  ttl_seconds: int | None = None,
484
493
  accountability_mode: str | None = None,
485
494
  authority_binding_ids: list[str] | None = None,
495
+ mission_ids: list[str] | None = None,
486
496
  **kwargs: Any,
487
497
  ):
488
- """Phase 5.2.6 — async passport issue + session wrapper."""
489
- issue_input: dict[str, Any] = {"agent_id": agent_id, **kwargs}
498
+ """Phase 5.2.6 — async passport issue + session wrapper.
499
+ ``mission_ids`` must hold exactly one Mission id."""
500
+ issue_input: dict[str, Any] = {
501
+ "agent_id": agent_id,
502
+ **kwargs,
503
+ "mission_ids": _require_one_mission(mission_ids),
504
+ }
490
505
  if services is not None: issue_input["services"] = services
491
506
  if intents is not None: issue_input["intents"] = intents
492
507
  if ttl_seconds is not None: issue_input["ttl_seconds"] = ttl_seconds
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from typing import Any
6
6
 
7
7
  from ..client import AsyncHttpClient
8
+ from ..dropoffs import _passport_header
8
9
 
9
10
 
10
11
  class AsyncDropoffService:
@@ -18,7 +19,9 @@ class AsyncDropoffService:
18
19
  schema: dict[str, Any],
19
20
  ttl_seconds: int | None = None,
20
21
  on_expire: str | None = None,
22
+ passport_token: str | None = None,
21
23
  ) -> dict[str, Any]:
24
+ """Create a drop-off. An agent caller passes the producer's Mission Passport as passport_token."""
22
25
  body: dict[str, Any] = {
23
26
  "from_agent": from_agent_id,
24
27
  "to_agent": to_agent_id,
@@ -28,7 +31,7 @@ class AsyncDropoffService:
28
31
  body["ttl_seconds"] = ttl_seconds
29
32
  if on_expire:
30
33
  body["on_expire"] = on_expire
31
- return await self._client.post("/v1/dropoffs", json=body)
34
+ return await self._client.post("/v1/dropoffs", json=body, extra_headers=_passport_header(passport_token))
32
35
 
33
36
  async def deposit(
34
37
  self,
@@ -37,7 +40,9 @@ class AsyncDropoffService:
37
40
  agent_id: str,
38
41
  passport_jti: str | None = None,
39
42
  mission_id: str | None = None,
43
+ passport_token: str | None = None,
40
44
  ) -> dict[str, Any]:
45
+ """Deposit as the producer. An agent caller passes its Mission Passport as passport_token (or passport_jti)."""
41
46
  body: dict[str, Any] = {"agent_id": agent_id, "payload": data}
42
47
  if passport_jti is not None:
43
48
  body["passport_jti"] = passport_jti
@@ -46,12 +51,15 @@ class AsyncDropoffService:
46
51
  return await self._client.post(
47
52
  f"/v1/dropoffs/{dropoff_id}/deposit",
48
53
  json=body,
54
+ extra_headers=_passport_header(passport_token),
49
55
  )
50
56
 
51
- async def collect(self, dropoff_id: str, agent_id: str) -> dict[str, Any]:
57
+ async def collect(self, dropoff_id: str, agent_id: str, passport_token: str | None = None) -> dict[str, Any]:
58
+ """Collect as the consumer. An agent caller passes its Mission Passport as passport_token."""
52
59
  return await self._client.post(
53
60
  f"/v1/dropoffs/{dropoff_id}/collect",
54
61
  json={"agent_id": agent_id},
62
+ extra_headers=_passport_header(passport_token),
55
63
  )
56
64
 
57
65
  async def expire(self, dropoff_id: str) -> dict[str, Any]:
@@ -1,4 +1,8 @@
1
- """Async mirror of MissionsService. Same surface, async/await."""
1
+ """Async mirror of MissionsService. Same surface, async/await.
2
+
3
+ Completing or revoking a Mission (any reason) revokes every passport
4
+ bound to it.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -6,14 +10,26 @@ from typing import Any
6
10
  from urllib.parse import quote
7
11
 
8
12
  from ..client import AsyncHttpClient
13
+ from ..missions import _start_body
9
14
 
10
15
 
11
16
  class AsyncMissionsService:
12
17
  def __init__(self, client: AsyncHttpClient):
13
18
  self._client = client
14
19
 
15
- async def start(self, request: dict[str, Any]) -> dict[str, Any]:
16
- return await self._client.request("POST", "/v1/missions/start", json=request)
20
+ async def start(
21
+ self,
22
+ request: dict[str, Any] | None = None,
23
+ *,
24
+ agent_id: str | None = None,
25
+ mission: dict[str, Any] | None = None,
26
+ passport: dict[str, Any] | None = None,
27
+ ) -> dict[str, Any]:
28
+ """Open a Mission and issue its passport in one call. Operator form:
29
+ ``await stack.missions.start(agent_id=..., mission={...}, passport={...})``.
30
+ Fleet form (unchanged): ``await stack.missions.start({"fleet_policy_id": ...})``."""
31
+ body = _start_body(request, agent_id, mission, passport)
32
+ return await self._client.request("POST", "/v1/missions/start", json=body)
17
33
 
18
34
  async def renew(self, mission_id: str) -> dict[str, Any]:
19
35
  return await self._client.request("POST", f"/v1/missions/{quote(mission_id, safe='')}/renew", json={})
@@ -41,8 +57,12 @@ class AsyncMissionsService:
41
57
  requested_operations: list[dict[str, Any]] | None = None,
42
58
  limits: dict[str, Any] | None = None,
43
59
  authority_binding_ids: list[str] | None = None,
60
+ agent_id: str | None = None,
44
61
  ) -> dict[str, Any]:
62
+ """Open a new mission. ``agent_id`` assigns it to that agent."""
45
63
  body: dict[str, Any] = {"intent_summary": intent_summary}
64
+ if agent_id is not None:
65
+ body["agent_id"] = agent_id
46
66
  if intent_services is not None:
47
67
  body["intent_services"] = intent_services
48
68
  if expected_counterparties is not None:
@@ -1,4 +1,4 @@
1
- """Async mirror of PartnerTenantsService (Phase 5.6)."""
1
+ """Async mirror of PartnerTenantsService (white-label tenants)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -6,10 +6,9 @@ from typing import Any, Literal, Optional
6
6
  from urllib.parse import quote
7
7
 
8
8
  from ..client import AsyncHttpClient
9
+ from ..passports import _require_one_mission
9
10
 
10
11
 
11
- Tier = Literal["free", "developer", "pro", "business", "enterprise"]
12
-
13
12
 
14
13
  class AsyncPartnerTenantsService:
15
14
  def __init__(self, client: AsyncHttpClient):
@@ -18,23 +17,36 @@ class AsyncPartnerTenantsService:
18
17
  async def create(
19
18
  self,
20
19
  external_customer_id: Optional[str] = None,
21
- tier: Optional[Tier] = None,
22
20
  ) -> dict[str, Any]:
21
+ """Create a white-label tenant.
22
+
23
+ Tenants have no plan of their own; they draw from your allowance
24
+ and wallet. Returns operator_id, api_key (shown once — store it),
25
+ external_customer_id, tier ('partner'), billing ('partner') and
26
+ created_at. Raises with code TENANT_LIMIT_REACHED (409) when your
27
+ deal caps the tenant count.
28
+ """
23
29
  body: dict[str, Any] = {}
24
30
  if external_customer_id is not None:
25
31
  body["external_customer_id"] = external_customer_id
26
- if tier is not None:
27
- body["tier"] = tier
28
32
  return await self._client.post("/v1/partner/tenants", json=body)
29
33
 
30
34
  async def list(self) -> list[dict[str, Any]]:
35
+ """List your tenants. Each carries ``usage`` ({period, actions}):
36
+ its month-to-date share of your action pool, when available."""
31
37
  res = await self._client.get("/v1/partner/tenants")
32
38
  return list(res.get("tenants", []))
33
39
 
34
40
  async def get(self, tenant_id: str) -> dict[str, Any]:
41
+ """Fetch one tenant (same shape as a ``list`` entry)."""
35
42
  return await self._client.get(f"/v1/partner/tenants/{tenant_id}")
36
43
 
37
44
  async def revoke(self, tenant_id: str) -> dict[str, Any]:
45
+ """Revoke a tenant. Cascades to its inbound webhooks and passports.
46
+
47
+ Returns {transitioned, webhooks_revoked, passports_revoked}.
48
+ Idempotent: a retry finishes a cascade that failed part-way.
49
+ """
38
50
  return await self._client.delete(f"/v1/partner/tenants/{tenant_id}")
39
51
 
40
52
  async def create_inbound_webhook(
@@ -76,8 +88,12 @@ class AsyncPartnerTenantsService:
76
88
  services: Optional[list[dict[str, Any]]] = None,
77
89
  named_intents: Optional[list[str]] = None,
78
90
  accountability_mode: Optional[Literal["enforced", "logged", "standard"]] = None,
91
+ mission_ids: Optional[list[str]] = None,
79
92
  ) -> dict[str, Any]:
80
- body: dict[str, Any] = {"agent_id": agent_id}
93
+ """Issue a passport under the tenant. ``mission_ids`` must hold exactly
94
+ one Mission id the tenant's agent may use; anything else raises
95
+ ``ValueError`` before any request."""
96
+ body: dict[str, Any] = {"agent_id": agent_id, "mission_ids": _require_one_mission(mission_ids)}
81
97
  if ttl_seconds is not None:
82
98
  body["ttl_seconds"] = ttl_seconds
83
99
  if services is not None:
@@ -99,6 +115,8 @@ class AsyncPartnerTenantsService:
99
115
  ttl_seconds: int = 900,
100
116
  accountability_mode: Literal["enforced", "logged", "standard"] = "enforced",
101
117
  named_intents: Optional[list[str]] = None,
118
+ *,
119
+ mission_id: str,
102
120
  ) -> dict[str, Any]:
103
121
  """Phase 5.7 — convenience helper for the MIME-stamping pattern.
104
122
 
@@ -112,6 +130,7 @@ class AsyncPartnerTenantsService:
112
130
  accountability_mode=accountability_mode,
113
131
  services=services,
114
132
  named_intents=named_intents,
133
+ mission_ids=[mission_id],
115
134
  )
116
135
  token = issued.get("token") or issued.get("passport")
117
136
  passport_id = issued.get("passport_id") or issued.get("jti")
@@ -6,6 +6,7 @@ import time
6
6
  from typing import Any
7
7
 
8
8
  from ..client import AsyncHttpClient
9
+ from ..errors import StackError, mission_ended_error
9
10
  from ..passport_session import _decode_jwt_exp
10
11
 
11
12
 
@@ -25,13 +26,19 @@ class AsyncPassportSession:
25
26
  now_sec = int(time.time())
26
27
  if self._exp_seconds - now_sec > grace_seconds:
27
28
  return self._current_token
28
- refreshed = await self._client.post(
29
- "/v1/passports/refresh",
30
- json={
31
- "token": self._current_token,
32
- "ttl_seconds": self._issue_input.get("ttl_seconds"),
33
- },
34
- )
29
+ try:
30
+ refreshed = await self._client.post(
31
+ "/v1/passports/refresh",
32
+ json={
33
+ "token": self._current_token,
34
+ "ttl_seconds": self._issue_input.get("ttl_seconds"),
35
+ },
36
+ )
37
+ except StackError as exc:
38
+ ended = mission_ended_error(exc)
39
+ if ended is not None:
40
+ raise ended from exc
41
+ raise
35
42
  self._current_token = refreshed["token"]
36
43
  self._exp_seconds = _decode_jwt_exp(self._current_token)
37
44
  return self._current_token