getstack 2.0.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.
- {getstack-2.0.0 → getstack-3.0.0}/PKG-INFO +12 -5
- {getstack-2.0.0 → getstack-3.0.0}/README.md +11 -4
- {getstack-2.0.0 → getstack-3.0.0}/pyproject.toml +1 -1
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/__init__.py +21 -6
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/dropoffs.py +10 -2
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/missions.py +23 -3
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/partner_tenants.py +9 -1
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/passport_session.py +14 -7
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/passports.py +169 -59
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/dropoffs.py +20 -4
- getstack-3.0.0/src/getstack/errors.py +145 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/llm.py +2 -1
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/missions.py +77 -13
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/partner_tenants.py +11 -1
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/passport_session.py +19 -9
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/passports.py +297 -68
- getstack-3.0.0/tests/test_dropoffs.py +117 -0
- getstack-3.0.0/tests/test_mission_binding.py +339 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_parity.py +52 -4
- getstack-2.0.0/src/getstack/errors.py +0 -79
- getstack-2.0.0/tests/test_dropoffs.py +0 -70
- {getstack-2.0.0 → getstack-3.0.0}/.gitignore +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/CLAUDE.md +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/LICENSE +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/scripts/verify_sign_claim_crosslang.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/__init__.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/agents.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/audit.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/authority.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/credentials.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/delivery_methods.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/detector_config.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/evidence_packs.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/fleets.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/identity.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/inbound_webhooks.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/intents.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/llm.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/notifications.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/partner_connect.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/pii_config.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/proxy.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/reviews.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/scan.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/security_events.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/services.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/skills.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/team.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_async/usage.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_authority_request.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/_fleet_key.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/agent_auth.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/agents.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/audit.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/auth.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/authority.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/client.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/credentials.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/delivery_methods.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/detector_config.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/evidence_packs.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/fleets.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/identity.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/inbound_webhooks.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/intents.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/notifications.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/partner_connect.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/passport_header_verify.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/pii_config.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/proxy.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/py.typed +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/reviews.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/scan.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/security_events.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/services.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/sign_claim.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/skills.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/team.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/types.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/usage.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/src/getstack/verify_offline.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_authority.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_delivery_methods.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_errors.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_evidence_packs.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_fleets.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_identity_requirements.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_inbound_webhooks.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_missions.py +0 -0
- {getstack-2.0.0 → getstack-3.0.0}/tests/test_partner_signing.py +0 -0
- {getstack-2.0.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:
|
|
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()
|
|
@@ -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
|
-
|
|
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] = {
|
|
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
|
-
|
|
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(
|
|
16
|
-
|
|
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:
|
|
@@ -6,6 +6,7 @@ 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
12
|
|
|
@@ -87,8 +88,12 @@ class AsyncPartnerTenantsService:
|
|
|
87
88
|
services: Optional[list[dict[str, Any]]] = None,
|
|
88
89
|
named_intents: Optional[list[str]] = None,
|
|
89
90
|
accountability_mode: Optional[Literal["enforced", "logged", "standard"]] = None,
|
|
91
|
+
mission_ids: Optional[list[str]] = None,
|
|
90
92
|
) -> dict[str, Any]:
|
|
91
|
-
|
|
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)}
|
|
92
97
|
if ttl_seconds is not None:
|
|
93
98
|
body["ttl_seconds"] = ttl_seconds
|
|
94
99
|
if services is not None:
|
|
@@ -110,6 +115,8 @@ class AsyncPartnerTenantsService:
|
|
|
110
115
|
ttl_seconds: int = 900,
|
|
111
116
|
accountability_mode: Literal["enforced", "logged", "standard"] = "enforced",
|
|
112
117
|
named_intents: Optional[list[str]] = None,
|
|
118
|
+
*,
|
|
119
|
+
mission_id: str,
|
|
113
120
|
) -> dict[str, Any]:
|
|
114
121
|
"""Phase 5.7 — convenience helper for the MIME-stamping pattern.
|
|
115
122
|
|
|
@@ -123,6 +130,7 @@ class AsyncPartnerTenantsService:
|
|
|
123
130
|
accountability_mode=accountability_mode,
|
|
124
131
|
services=services,
|
|
125
132
|
named_intents=named_intents,
|
|
133
|
+
mission_ids=[mission_id],
|
|
126
134
|
)
|
|
127
135
|
token = issued.get("token") or issued.get("passport")
|
|
128
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
@@ -13,8 +13,20 @@ import asyncio
|
|
|
13
13
|
from contextlib import asynccontextmanager
|
|
14
14
|
from typing import Any, AsyncGenerator, Awaitable, Callable
|
|
15
15
|
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
16
18
|
from ..client import AsyncHttpClient
|
|
17
|
-
from ..
|
|
19
|
+
from ..errors import StackError, mission_ended_error
|
|
20
|
+
from ..passports import (
|
|
21
|
+
_delegation_scopes,
|
|
22
|
+
_intent_body,
|
|
23
|
+
_parse_duration,
|
|
24
|
+
_require_one_mission,
|
|
25
|
+
_revoke_notes,
|
|
26
|
+
_signed_accountability,
|
|
27
|
+
_start_mission_body,
|
|
28
|
+
_to_passport,
|
|
29
|
+
)
|
|
18
30
|
from ..proxy import ProxyResponse
|
|
19
31
|
from .proxy import AsyncProxyService
|
|
20
32
|
from ..types import Passport, CheckpointResult, CheckoutResult, PassportReport, ToolCall
|
|
@@ -35,6 +47,8 @@ class AsyncMission:
|
|
|
35
47
|
will_delegate: bool = False,
|
|
36
48
|
estimated_duration: str | None = None,
|
|
37
49
|
checkpoint_interval: str | None = None,
|
|
50
|
+
mission_id: str | None = None,
|
|
51
|
+
expires_in: str | None = None,
|
|
38
52
|
):
|
|
39
53
|
self._service = service
|
|
40
54
|
self._agent_id = agent_id
|
|
@@ -43,6 +57,10 @@ class AsyncMission:
|
|
|
43
57
|
self._will_delegate = will_delegate
|
|
44
58
|
self._estimated_duration = estimated_duration
|
|
45
59
|
self._checkpoint_interval = checkpoint_interval
|
|
60
|
+
self._expires_in = expires_in
|
|
61
|
+
|
|
62
|
+
self.mission_id: str | None = mission_id
|
|
63
|
+
self._owns_mission: bool = False
|
|
46
64
|
|
|
47
65
|
self.passport: Passport | None = None
|
|
48
66
|
self.review_status: str | None = None
|
|
@@ -112,14 +130,30 @@ class AsyncMission:
|
|
|
112
130
|
return resp
|
|
113
131
|
|
|
114
132
|
async def _enter(self) -> None:
|
|
115
|
-
self.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
if self.mission_id is None:
|
|
134
|
+
# One call opens the Mission and issues its passport; the API
|
|
135
|
+
# revokes the Mission if issuance fails.
|
|
136
|
+
started = await self._service._start_mission(_start_mission_body(
|
|
137
|
+
self._agent_id, self._intent, self._declared_services,
|
|
138
|
+
self._will_delegate, self._estimated_duration,
|
|
139
|
+
self._checkpoint_interval, self._expires_in,
|
|
140
|
+
))
|
|
141
|
+
self.mission_id = started["mission_id"]
|
|
142
|
+
self._owns_mission = True
|
|
143
|
+
passport = _to_passport(started["passport"])
|
|
144
|
+
if passport.accountability is None:
|
|
145
|
+
passport.accountability = _signed_accountability(passport.token)
|
|
146
|
+
self.passport = passport
|
|
147
|
+
else:
|
|
148
|
+
self.passport = await self._service.issue(
|
|
149
|
+
agent_id=self._agent_id,
|
|
150
|
+
intent=self._intent,
|
|
151
|
+
services=self._declared_services,
|
|
152
|
+
will_delegate=self._will_delegate,
|
|
153
|
+
estimated_duration=self._estimated_duration,
|
|
154
|
+
checkpoint_interval=self._checkpoint_interval,
|
|
155
|
+
mission_ids=[self.mission_id],
|
|
156
|
+
)
|
|
123
157
|
self._rotation_start = asyncio.get_event_loop().time()
|
|
124
158
|
|
|
125
159
|
if self._checkpoint_interval:
|
|
@@ -162,7 +196,9 @@ class AsyncMission:
|
|
|
162
196
|
)
|
|
163
197
|
self._last_checkpoint_index = len(self._tool_calls)
|
|
164
198
|
|
|
165
|
-
async def _exit(self, error:
|
|
199
|
+
async def _exit(self, error: BaseException | None = None) -> None:
|
|
200
|
+
"""Checkout first (closing the Mission revokes its passports), then
|
|
201
|
+
close the Mission this object opened."""
|
|
166
202
|
if self._task and not self._task.done():
|
|
167
203
|
self._task.cancel()
|
|
168
204
|
try:
|
|
@@ -171,32 +207,33 @@ class AsyncMission:
|
|
|
171
207
|
pass
|
|
172
208
|
self._task = None
|
|
173
209
|
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if error:
|
|
179
|
-
summary = f"Mission failed: {type(error).__name__}: {error}"
|
|
210
|
+
if self.passport:
|
|
211
|
+
summary = None
|
|
212
|
+
if error:
|
|
213
|
+
summary = f"Mission failed: {type(error).__name__}: {error}"
|
|
180
214
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
215
|
+
try:
|
|
216
|
+
self.result = await self._service.checkout(
|
|
217
|
+
self.passport.jti,
|
|
218
|
+
services_used=list(self._services_used),
|
|
219
|
+
actions_count=len(self._tool_calls),
|
|
220
|
+
tool_calls=[tc.to_dict() for tc in self._tool_calls] if self._tool_calls else None,
|
|
221
|
+
summary=summary,
|
|
222
|
+
delegated_to=self._delegated_to if self._delegated_to else None,
|
|
223
|
+
)
|
|
224
|
+
self.review_status = self.result.review_status
|
|
225
|
+
except Exception:
|
|
226
|
+
pass # Best-effort checkout
|
|
227
|
+
|
|
228
|
+
if self._owns_mission and self.mission_id:
|
|
229
|
+
await self._service._finish_mission(self.mission_id, error)
|
|
193
230
|
|
|
194
231
|
async def __aenter__(self) -> AsyncMission:
|
|
195
232
|
await self._enter()
|
|
196
233
|
return self
|
|
197
234
|
|
|
198
235
|
async def __aexit__(
|
|
199
|
-
self, exc_type: type | None, exc_val:
|
|
236
|
+
self, exc_type: type | None, exc_val: BaseException | None, exc_tb: Any
|
|
200
237
|
) -> bool:
|
|
201
238
|
await self._exit(error=exc_val if exc_type else None)
|
|
202
239
|
return False
|
|
@@ -221,6 +258,17 @@ class AsyncPassportService:
|
|
|
221
258
|
) -> Passport:
|
|
222
259
|
"""Issue a signed passport for an agent (async mirror of ``Stack``).
|
|
223
260
|
|
|
261
|
+
``mission_ids`` must hold exactly one live Mission id (``ms_*``);
|
|
262
|
+
anything else raises ``ValueError`` before any request::
|
|
263
|
+
|
|
264
|
+
mission = await stack.missions.open("Send the weekly report", agent_id="agt_x")
|
|
265
|
+
passport = await stack.passports.issue(
|
|
266
|
+
agent_id="agt_x",
|
|
267
|
+
mission_ids=[mission["mission_id"]],
|
|
268
|
+
intent="Send the weekly report",
|
|
269
|
+
services=["resend"],
|
|
270
|
+
)
|
|
271
|
+
|
|
224
272
|
``scopes`` is a list of per-connection override dicts. Each recognises:
|
|
225
273
|
|
|
226
274
|
- ``service_connection_id`` (str) — the connection to scope.
|
|
@@ -235,6 +283,7 @@ class AsyncPassportService:
|
|
|
235
283
|
|
|
236
284
|
await stack.passports.issue(
|
|
237
285
|
agent_id="agt_x",
|
|
286
|
+
mission_ids=[mission_id],
|
|
238
287
|
scopes=[{
|
|
239
288
|
"service_connection_id": "scon_resend",
|
|
240
289
|
"scopes": ["email:send"],
|
|
@@ -242,26 +291,44 @@ class AsyncPassportService:
|
|
|
242
291
|
}],
|
|
243
292
|
)
|
|
244
293
|
"""
|
|
245
|
-
body: dict[str, Any] = {"agent_id": agent_id}
|
|
294
|
+
body: dict[str, Any] = {"agent_id": agent_id, "mission_ids": _require_one_mission(mission_ids)}
|
|
246
295
|
if scopes:
|
|
247
296
|
body["scopes"] = scopes
|
|
248
297
|
if authority_binding_ids is not None:
|
|
249
298
|
body["authority_binding_ids"] = authority_binding_ids
|
|
250
|
-
if mission_ids is not None:
|
|
251
|
-
body["mission_ids"] = mission_ids
|
|
252
299
|
if ttl_seconds:
|
|
253
300
|
body["ttl_seconds"] = ttl_seconds
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"services": services or [],
|
|
258
|
-
"will_delegate": will_delegate,
|
|
259
|
-
}
|
|
260
|
-
if estimated_duration:
|
|
261
|
-
body["intent"]["estimated_duration_seconds"] = _parse_duration(estimated_duration)
|
|
301
|
+
intent_body = _intent_body(intent, services, will_delegate, estimated_duration)
|
|
302
|
+
if intent_body is not None:
|
|
303
|
+
body["intent"] = intent_body
|
|
262
304
|
if checkpoint_interval:
|
|
263
305
|
body["checkpoint_interval_seconds"] = _parse_duration(checkpoint_interval)
|
|
264
|
-
|
|
306
|
+
try:
|
|
307
|
+
data = await self._client.post("/v1/passports/issue", json=body)
|
|
308
|
+
except StackError as exc:
|
|
309
|
+
ended = mission_ended_error(exc)
|
|
310
|
+
if ended is not None:
|
|
311
|
+
raise ended from exc
|
|
312
|
+
raise
|
|
313
|
+
return _to_passport(data)
|
|
314
|
+
|
|
315
|
+
async def _start_mission(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
316
|
+
return await self._client.post("/v1/missions/start", json=body)
|
|
317
|
+
|
|
318
|
+
async def _finish_mission(self, mission_id: str, error: BaseException | None) -> None:
|
|
319
|
+
"""Complete the Mission, or revoke it when the block raised. Mirror of
|
|
320
|
+
the sync PassportService._finish_mission."""
|
|
321
|
+
path_id = quote(mission_id, safe="")
|
|
322
|
+
if error is None:
|
|
323
|
+
await self._client.post(f"/v1/missions/{path_id}/complete", json={})
|
|
324
|
+
return
|
|
325
|
+
try:
|
|
326
|
+
await self._client.post(
|
|
327
|
+
f"/v1/missions/{path_id}/revoke",
|
|
328
|
+
json={"reason": "operator_initiated_clean", "notes": _revoke_notes(error)},
|
|
329
|
+
)
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
265
332
|
|
|
266
333
|
async def checkpoint(
|
|
267
334
|
self,
|
|
@@ -362,10 +429,18 @@ class AsyncPassportService:
|
|
|
362
429
|
return await self._client.get("/v1/passports/active", params=params or None)
|
|
363
430
|
|
|
364
431
|
async def refresh(self, token: str, ttl_seconds: int | None = None) -> Passport:
|
|
432
|
+
"""Refresh within the Mission; raises MissionEndedError once it ends."""
|
|
365
433
|
body: dict[str, Any] = {"token": token}
|
|
366
434
|
if ttl_seconds:
|
|
367
435
|
body["ttl_seconds"] = ttl_seconds
|
|
368
|
-
|
|
436
|
+
try:
|
|
437
|
+
data = await self._client.post("/v1/passports/refresh", json=body)
|
|
438
|
+
except StackError as exc:
|
|
439
|
+
ended = mission_ended_error(exc)
|
|
440
|
+
if ended is not None:
|
|
441
|
+
raise ended from exc
|
|
442
|
+
raise
|
|
443
|
+
return _to_passport(data)
|
|
369
444
|
|
|
370
445
|
async def list_active(
|
|
371
446
|
self, agent_id: str | None = None, session_id: str | None = None
|
|
@@ -386,18 +461,26 @@ class AsyncPassportService:
|
|
|
386
461
|
will_delegate: bool = False,
|
|
387
462
|
estimated_duration: str | None = None,
|
|
388
463
|
checkpoint_interval: str | None = None,
|
|
464
|
+
mission_id: str | None = None,
|
|
465
|
+
expires_in: str | None = None,
|
|
389
466
|
) -> AsyncGenerator[AsyncMission, None]:
|
|
390
|
-
"""Async-context-manager mission. Mirror of sync mission()
|
|
467
|
+
"""Async-context-manager mission. Mirror of sync mission(): opens the
|
|
468
|
+
Mission and its passport in one call (POST /v1/missions/start),
|
|
469
|
+
checks out, then completes the Mission on exit (revokes it if the
|
|
470
|
+
block raised). ``mission_id`` binds an existing Mission, which is
|
|
471
|
+
then left open on exit."""
|
|
391
472
|
m = AsyncMission(
|
|
392
473
|
self, agent_id, intent, services,
|
|
393
474
|
will_delegate=will_delegate,
|
|
394
475
|
estimated_duration=estimated_duration,
|
|
395
476
|
checkpoint_interval=checkpoint_interval,
|
|
477
|
+
mission_id=mission_id,
|
|
478
|
+
expires_in=expires_in,
|
|
396
479
|
)
|
|
397
480
|
await m._enter()
|
|
398
481
|
try:
|
|
399
482
|
yield m
|
|
400
|
-
except
|
|
483
|
+
except BaseException as e:
|
|
401
484
|
await m._exit(error=e)
|
|
402
485
|
raise
|
|
403
486
|
else:
|
|
@@ -411,21 +494,48 @@ class AsyncPassportService:
|
|
|
411
494
|
rotation_interval: str = "4h",
|
|
412
495
|
will_delegate: bool = False,
|
|
413
496
|
checkpoint_interval: str | None = None,
|
|
497
|
+
mission_id: str | None = None,
|
|
498
|
+
expires_in: str | None = None,
|
|
414
499
|
) -> AsyncGenerator[AsyncMission, None]:
|
|
415
|
-
"""Async generator yielding successive AsyncMission objects.
|
|
500
|
+
"""Async generator yielding successive AsyncMission objects. Mirror of
|
|
501
|
+
sync continuous_mission(): every rotation shares one Mission, and
|
|
502
|
+
closing the generator (``aclose()``) completes the Mission it opened."""
|
|
416
503
|
rotation_secs = float(_parse_duration(rotation_interval))
|
|
417
504
|
rotation_count = 0
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
505
|
+
bound_mission_id = mission_id
|
|
506
|
+
owned_mission_id: str | None = None
|
|
507
|
+
error: BaseException | None = None
|
|
508
|
+
try:
|
|
509
|
+
while True:
|
|
510
|
+
m = AsyncMission(
|
|
511
|
+
self, agent_id, intent, services,
|
|
512
|
+
will_delegate=will_delegate,
|
|
513
|
+
checkpoint_interval=checkpoint_interval,
|
|
514
|
+
mission_id=bound_mission_id,
|
|
515
|
+
expires_in=expires_in,
|
|
516
|
+
)
|
|
517
|
+
m.rotation_count = rotation_count
|
|
518
|
+
m._rotation_interval = rotation_secs
|
|
519
|
+
await m._enter()
|
|
520
|
+
if m._owns_mission:
|
|
521
|
+
owned_mission_id = bound_mission_id = m.mission_id
|
|
522
|
+
m._owns_mission = False
|
|
523
|
+
try:
|
|
524
|
+
yield m
|
|
525
|
+
except GeneratorExit:
|
|
526
|
+
await m._exit()
|
|
527
|
+
raise
|
|
528
|
+
except BaseException as e:
|
|
529
|
+
await m._exit(error=e)
|
|
530
|
+
raise
|
|
531
|
+
else:
|
|
532
|
+
await m._exit()
|
|
533
|
+
rotation_count += 1
|
|
534
|
+
except GeneratorExit:
|
|
535
|
+
raise
|
|
536
|
+
except BaseException as e:
|
|
537
|
+
error = e
|
|
538
|
+
raise
|
|
539
|
+
finally:
|
|
540
|
+
if owned_mission_id is not None:
|
|
541
|
+
await self._finish_mission(owned_mission_id, error)
|