getstack 1.2.0__tar.gz → 1.4.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 (89) hide show
  1. {getstack-1.2.0 → getstack-1.4.0}/.gitignore +3 -0
  2. {getstack-1.2.0 → getstack-1.4.0}/PKG-INFO +44 -40
  3. {getstack-1.2.0 → getstack-1.4.0}/README.md +43 -39
  4. {getstack-1.2.0 → getstack-1.4.0}/pyproject.toml +1 -1
  5. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/__init__.py +59 -37
  6. getstack-1.4.0/src/getstack/_async/fleets.py +82 -0
  7. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/missions.py +17 -0
  8. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/passports.py +12 -6
  9. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/proxy.py +41 -10
  10. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_authority_request.py +6 -0
  11. getstack-1.4.0/src/getstack/_fleet_key.py +44 -0
  12. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/agent_auth.py +9 -1
  13. getstack-1.4.0/src/getstack/fleets.py +82 -0
  14. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/missions.py +17 -0
  15. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/passports.py +36 -5
  16. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/proxy.py +71 -12
  17. {getstack-1.2.0 → getstack-1.4.0}/tests/test_authority.py +140 -0
  18. getstack-1.4.0/tests/test_fleets.py +94 -0
  19. {getstack-1.2.0 → getstack-1.4.0}/tests/test_parity.py +7 -1
  20. {getstack-1.2.0 → getstack-1.4.0}/CLAUDE.md +0 -0
  21. {getstack-1.2.0 → getstack-1.4.0}/LICENSE +0 -0
  22. {getstack-1.2.0 → getstack-1.4.0}/scripts/verify_sign_claim_crosslang.py +0 -0
  23. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/__init__.py +0 -0
  24. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/agents.py +0 -0
  25. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/audit.py +0 -0
  26. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/authority.py +0 -0
  27. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/credentials.py +0 -0
  28. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/delivery_methods.py +0 -0
  29. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/detector_config.py +0 -0
  30. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/dropoffs.py +0 -0
  31. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/evidence_packs.py +0 -0
  32. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/identity.py +0 -0
  33. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/inbound_webhooks.py +0 -0
  34. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/intents.py +0 -0
  35. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/llm.py +0 -0
  36. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/notifications.py +0 -0
  37. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/partner_connect.py +0 -0
  38. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/partner_tenants.py +0 -0
  39. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/passport_session.py +0 -0
  40. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/pii_config.py +0 -0
  41. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/reviews.py +0 -0
  42. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/scan.py +0 -0
  43. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/security_events.py +0 -0
  44. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/services.py +0 -0
  45. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/skills.py +0 -0
  46. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/team.py +0 -0
  47. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/usage.py +0 -0
  48. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/agents.py +0 -0
  49. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/audit.py +0 -0
  50. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/auth.py +0 -0
  51. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/authority.py +0 -0
  52. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/browser_bootstrap.py +0 -0
  53. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/client.py +0 -0
  54. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/credentials.py +0 -0
  55. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/delivery_methods.py +0 -0
  56. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/detector_config.py +0 -0
  57. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/dropoffs.py +0 -0
  58. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/errors.py +0 -0
  59. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/evidence_packs.py +0 -0
  60. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/identity.py +0 -0
  61. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/inbound_webhooks.py +0 -0
  62. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/intents.py +0 -0
  63. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/llm.py +0 -0
  64. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/notifications.py +0 -0
  65. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/partner_connect.py +0 -0
  66. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/partner_tenants.py +0 -0
  67. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/passport_header_verify.py +0 -0
  68. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/passport_session.py +0 -0
  69. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/pii_config.py +0 -0
  70. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/py.typed +0 -0
  71. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/reviews.py +0 -0
  72. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/scan.py +0 -0
  73. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/security_events.py +0 -0
  74. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/services.py +0 -0
  75. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/sign_claim.py +0 -0
  76. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/skills.py +0 -0
  77. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/team.py +0 -0
  78. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/types.py +0 -0
  79. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/usage.py +0 -0
  80. {getstack-1.2.0 → getstack-1.4.0}/src/getstack/verify_offline.py +0 -0
  81. {getstack-1.2.0 → getstack-1.4.0}/tests/test_delivery_methods.py +0 -0
  82. {getstack-1.2.0 → getstack-1.4.0}/tests/test_dropoffs.py +0 -0
  83. {getstack-1.2.0 → getstack-1.4.0}/tests/test_errors.py +0 -0
  84. {getstack-1.2.0 → getstack-1.4.0}/tests/test_evidence_packs.py +0 -0
  85. {getstack-1.2.0 → getstack-1.4.0}/tests/test_identity_requirements.py +0 -0
  86. {getstack-1.2.0 → getstack-1.4.0}/tests/test_inbound_webhooks.py +0 -0
  87. {getstack-1.2.0 → getstack-1.4.0}/tests/test_missions.py +0 -0
  88. {getstack-1.2.0 → getstack-1.4.0}/tests/test_partner_signing.py +0 -0
  89. {getstack-1.2.0 → getstack-1.4.0}/tests/test_sign_claim.py +0 -0
@@ -89,3 +89,6 @@ scratchpad/
89
89
 
90
90
  # Local venv for the AgentDojo benchmark harness (never commit)
91
91
  .venv-agentdojo/
92
+
93
+ # Isolated test-only LangChain, CrewAI and AutoGen verification environment.
94
+ .tmp/fleet-framework-venv/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: getstack
3
- Version: 1.2.0
3
+ Version: 1.4.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
@@ -44,32 +44,44 @@ npx -y @getstackrun/cli auth login
44
44
  ```
45
45
 
46
46
  ```python
47
- from getstack import Stack
48
-
49
- # Zero-arg constructor reads ~/.stack/credentials.json (OAuth refresh token).
50
- # Falls back to STACK_API_KEY env var, then constructor api_key= for CI.
51
- stack = Stack()
52
-
53
- # 1. Register an agent (one-time)
54
- agent = stack.agents.register("my-agent", accountability_mode="enforced")
55
-
56
- # 2. In your agent runtime, switch to per-agent keypair mode
57
- agent_stack = Stack(agent_id=agent.id)
58
- # First run: generates an Ed25519 keypair locally + enrolls the public
59
- # half via /v1/agents/<id>/enroll. Persisted at ~/.stack/agents/<id>.json
60
- # (mode 0600). Every subsequent call signs a fresh 60-second JWT.
61
-
62
- # 3. Run a mission with automatic checkpoints and checkout
63
- with agent_stack.passports.mission(
64
- agent_id=agent.id,
65
- intent="Process invoices from Slack",
66
- services=["slack", "stripe"],
67
- checkpoint_interval="5m",
68
- ) as mission:
69
- mission.log("slack", "read_channel", "#invoices")
70
- mission.log("stripe", "create_invoice")
71
- # Checkpoints are submitted automatically on schedule
72
- # Checkout is submitted automatically when the block exits
47
+ import hashlib
48
+ import json
49
+ from getstack.errors import StackError
50
+
51
+
52
+ def run_fleet_mission_example(stack, request, provider_url="https://slack.com/api/chat.postMessage"):
53
+ started = stack.missions.start(request)
54
+ repeated = stack.missions.start(request)
55
+ assert repeated["mission_id"] == started["mission_id"]
56
+ operation_id = hashlib.sha256(json.dumps([started["mission_id"], "support-update"]).encode()).hexdigest()
57
+ response = stack.proxy.request("slack", provider_url, "POST",
58
+ body={"channel": "C123", "text": "Support update"},
59
+ passport_token=started["passport"]["token"], authority_request_id=operation_id)
60
+ assert response.status == 200
61
+ try:
62
+ stack.proxy.request("slack", provider_url, "POST", body={"channel": "OTHER", "text": "Support update"},
63
+ passport_token=started["passport"]["token"], authority_request_id=operation_id + "_denied")
64
+ except StackError as error:
65
+ assert error.status_code == 403
66
+ else:
67
+ raise AssertionError("Expected channel restriction")
68
+ before = stack.missions.get(started["mission_id"])
69
+ renewed = stack.missions.renew(started["mission_id"])
70
+ after = stack.missions.get(renewed["mission_id"])
71
+ assert renewed["mission_id"] == started["mission_id"]
72
+ assert after["fleet"]["limits"] == before["fleet"]["limits"]
73
+ assert after["fleet"]["usage"] == before["fleet"]["usage"]
74
+ completed = stack.missions.complete(started["mission_id"], output={"done": True})
75
+ assert completed["terminal_state"] == "completed_successful"
76
+ stopped = stack.missions.start({**request, "idempotency_key": request["idempotency_key"] + "-stop"})
77
+ stack.missions.revoke(stopped["mission_id"], reason="operator_initiated_clean")
78
+ try:
79
+ stack.missions.renew(stopped["mission_id"])
80
+ except StackError:
81
+ pass
82
+ else:
83
+ raise AssertionError("Expected revoked Mission")
84
+ return {"mission_id": started["mission_id"], "operation_id": operation_id, "completed": True, "revoked": True}
73
85
  ```
74
86
 
75
87
  ## Authentication
@@ -94,23 +106,15 @@ stack = Stack()
94
106
 
95
107
  See [/docs/security/stack-auth](https://getstack.run/docs/security/stack-auth) for the full auth model and [/docs/security/agent-keys](https://getstack.run/docs/security/agent-keys) for the per-agent keypair story.
96
108
 
97
- ## Continuous missions (24/7 agents)
109
+ ## Renew a Mission's Passport
98
110
 
99
111
  ```python
100
- for mission in stack.passports.continuous_mission(
101
- agent_id=agent.id,
102
- intent="Monitor Slack channels",
103
- services=["slack"],
104
- rotation_interval="4h",
105
- checkpoint_interval="5m",
106
- ):
107
- while not mission.should_rotate:
108
- event = wait_for_event()
109
- mission.log("slack", "read_message")
110
- process(event)
111
- # Passport rotates automatically at the boundary
112
+ renewed = stack.missions.renew(mission_id)
113
+ passport_token = renewed["passport"]["token"]
112
114
  ```
113
115
 
116
+ Renewal keeps the same Mission, permission and recorded usage. Its deadline and limits still apply. Start a new Mission for the next job.
117
+
114
118
  ## Services
115
119
 
116
120
  ```python
@@ -17,32 +17,44 @@ npx -y @getstackrun/cli auth login
17
17
  ```
18
18
 
19
19
  ```python
20
- from getstack import Stack
21
-
22
- # Zero-arg constructor reads ~/.stack/credentials.json (OAuth refresh token).
23
- # Falls back to STACK_API_KEY env var, then constructor api_key= for CI.
24
- stack = Stack()
25
-
26
- # 1. Register an agent (one-time)
27
- agent = stack.agents.register("my-agent", accountability_mode="enforced")
28
-
29
- # 2. In your agent runtime, switch to per-agent keypair mode
30
- agent_stack = Stack(agent_id=agent.id)
31
- # First run: generates an Ed25519 keypair locally + enrolls the public
32
- # half via /v1/agents/<id>/enroll. Persisted at ~/.stack/agents/<id>.json
33
- # (mode 0600). Every subsequent call signs a fresh 60-second JWT.
34
-
35
- # 3. Run a mission with automatic checkpoints and checkout
36
- with agent_stack.passports.mission(
37
- agent_id=agent.id,
38
- intent="Process invoices from Slack",
39
- services=["slack", "stripe"],
40
- checkpoint_interval="5m",
41
- ) as mission:
42
- mission.log("slack", "read_channel", "#invoices")
43
- mission.log("stripe", "create_invoice")
44
- # Checkpoints are submitted automatically on schedule
45
- # Checkout is submitted automatically when the block exits
20
+ import hashlib
21
+ import json
22
+ from getstack.errors import StackError
23
+
24
+
25
+ def run_fleet_mission_example(stack, request, provider_url="https://slack.com/api/chat.postMessage"):
26
+ started = stack.missions.start(request)
27
+ repeated = stack.missions.start(request)
28
+ assert repeated["mission_id"] == started["mission_id"]
29
+ operation_id = hashlib.sha256(json.dumps([started["mission_id"], "support-update"]).encode()).hexdigest()
30
+ response = stack.proxy.request("slack", provider_url, "POST",
31
+ body={"channel": "C123", "text": "Support update"},
32
+ passport_token=started["passport"]["token"], authority_request_id=operation_id)
33
+ assert response.status == 200
34
+ try:
35
+ stack.proxy.request("slack", provider_url, "POST", body={"channel": "OTHER", "text": "Support update"},
36
+ passport_token=started["passport"]["token"], authority_request_id=operation_id + "_denied")
37
+ except StackError as error:
38
+ assert error.status_code == 403
39
+ else:
40
+ raise AssertionError("Expected channel restriction")
41
+ before = stack.missions.get(started["mission_id"])
42
+ renewed = stack.missions.renew(started["mission_id"])
43
+ after = stack.missions.get(renewed["mission_id"])
44
+ assert renewed["mission_id"] == started["mission_id"]
45
+ assert after["fleet"]["limits"] == before["fleet"]["limits"]
46
+ assert after["fleet"]["usage"] == before["fleet"]["usage"]
47
+ completed = stack.missions.complete(started["mission_id"], output={"done": True})
48
+ assert completed["terminal_state"] == "completed_successful"
49
+ stopped = stack.missions.start({**request, "idempotency_key": request["idempotency_key"] + "-stop"})
50
+ stack.missions.revoke(stopped["mission_id"], reason="operator_initiated_clean")
51
+ try:
52
+ stack.missions.renew(stopped["mission_id"])
53
+ except StackError:
54
+ pass
55
+ else:
56
+ raise AssertionError("Expected revoked Mission")
57
+ return {"mission_id": started["mission_id"], "operation_id": operation_id, "completed": True, "revoked": True}
46
58
  ```
47
59
 
48
60
  ## Authentication
@@ -67,23 +79,15 @@ stack = Stack()
67
79
 
68
80
  See [/docs/security/stack-auth](https://getstack.run/docs/security/stack-auth) for the full auth model and [/docs/security/agent-keys](https://getstack.run/docs/security/agent-keys) for the per-agent keypair story.
69
81
 
70
- ## Continuous missions (24/7 agents)
82
+ ## Renew a Mission's Passport
71
83
 
72
84
  ```python
73
- for mission in stack.passports.continuous_mission(
74
- agent_id=agent.id,
75
- intent="Monitor Slack channels",
76
- services=["slack"],
77
- rotation_interval="4h",
78
- checkpoint_interval="5m",
79
- ):
80
- while not mission.should_rotate:
81
- event = wait_for_event()
82
- mission.log("slack", "read_message")
83
- process(event)
84
- # Passport rotates automatically at the boundary
85
+ renewed = stack.missions.renew(mission_id)
86
+ passport_token = renewed["passport"]["token"]
85
87
  ```
86
88
 
89
+ Renewal keeps the same Mission, permission and recorded usage. Its deadline and limits still apply. Start a new Mission for the next job.
90
+
87
91
  ## Services
88
92
 
89
93
  ```python
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "1.2.0"
7
+ version = "1.4.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,39 +1,51 @@
1
- """STACK Python SDK — trust infrastructure for AI agents.
2
-
3
- Usage::
4
-
5
- from getstack import Stack
6
-
7
- stack = Stack(api_key="sk_live_...")
8
- agent = stack.agents.register("my-agent")
9
-
10
- with stack.passports.mission(
11
- agent_id=agent.id,
12
- intent="Process invoices",
13
- services=["slack", "stripe"],
14
- ) as mission:
15
- mission.log("slack", "read_channel", "#invoices")
16
- mission.log("stripe", "create_invoice")
17
-
18
- For OAuth auth::
19
-
20
- stack = Stack.from_oauth(
21
- client_id="...",
22
- client_secret="...",
23
- access_token="...",
24
- refresh_token="...",
25
- )
26
-
27
- For async usage::
28
-
29
- from getstack import AsyncStack
30
-
31
- stack = AsyncStack(api_key="sk_live_...")
1
+ """STACK Python SDK.
2
+
3
+ Fleet Mission example::
4
+
5
+ import hashlib
6
+ import json
7
+ from getstack.errors import StackError
8
+
9
+
10
+ def run_fleet_mission_example(stack, request, provider_url="https://slack.com/api/chat.postMessage"):
11
+ started = stack.missions.start(request)
12
+ repeated = stack.missions.start(request)
13
+ assert repeated["mission_id"] == started["mission_id"]
14
+ operation_id = hashlib.sha256(json.dumps([started["mission_id"], "support-update"]).encode()).hexdigest()
15
+ response = stack.proxy.request("slack", provider_url, "POST",
16
+ body={"channel": "C123", "text": "Support update"},
17
+ passport_token=started["passport"]["token"], authority_request_id=operation_id)
18
+ assert response.status == 200
19
+ try:
20
+ stack.proxy.request("slack", provider_url, "POST", body={"channel": "OTHER", "text": "Support update"},
21
+ passport_token=started["passport"]["token"], authority_request_id=operation_id + "_denied")
22
+ except StackError as error:
23
+ assert error.status_code == 403
24
+ else:
25
+ raise AssertionError("Expected channel restriction")
26
+ before = stack.missions.get(started["mission_id"])
27
+ renewed = stack.missions.renew(started["mission_id"])
28
+ after = stack.missions.get(renewed["mission_id"])
29
+ assert renewed["mission_id"] == started["mission_id"]
30
+ assert after["fleet"]["limits"] == before["fleet"]["limits"]
31
+ assert after["fleet"]["usage"] == before["fleet"]["usage"]
32
+ completed = stack.missions.complete(started["mission_id"], output={"done": True})
33
+ assert completed["terminal_state"] == "completed_successful"
34
+ stopped = stack.missions.start({**request, "idempotency_key": request["idempotency_key"] + "-stop"})
35
+ stack.missions.revoke(stopped["mission_id"], reason="operator_initiated_clean")
36
+ try:
37
+ stack.missions.renew(stopped["mission_id"])
38
+ except StackError:
39
+ pass
40
+ else:
41
+ raise AssertionError("Expected revoked Mission")
42
+ return {"mission_id": started["mission_id"], "operation_id": operation_id, "completed": True, "revoked": True}
32
43
  """
33
44
 
34
45
  from __future__ import annotations
35
46
 
36
47
  import os
48
+ from importlib.metadata import PackageNotFoundError, version
37
49
  from typing import Any
38
50
 
39
51
  from .auth import ApiKeyAuth, SessionAuth, OAuthAuth, CredentialsFileAuth
@@ -47,7 +59,8 @@ from .dropoffs import DropoffService
47
59
  from .reviews import ReviewService
48
60
  from .notifications import NotificationService
49
61
  from .audit import AuditService
50
- from .proxy import ProxyService as ProxySvc, ProxyResponse
62
+ from .proxy import ProxyOperation, ProxyService as ProxySvc, ProxyResponse
63
+ from ._authority_request import create_authority_request_id
51
64
  from .scan import ScanService
52
65
  from .usage import UsageService
53
66
  from .security_events import SecurityEventService as SecurityEventSvc
@@ -61,6 +74,8 @@ from .verify_offline import verify_passport_offline
61
74
  from .sign_claim import sign_claim_envelope
62
75
  from .intents import IntentsService
63
76
  from .missions import MissionsService
77
+ from .fleets import FleetsService
78
+ from ._async.fleets import AsyncFleetsService
64
79
  from .evidence_packs import EvidencePacksService
65
80
  from .llm import LlmService
66
81
  from .team import TeamService
@@ -106,7 +121,10 @@ from .errors import (
106
121
  PassportBlockedError,
107
122
  )
108
123
 
109
- __version__ = "0.2.0"
124
+ try:
125
+ __version__ = version("getstack")
126
+ except PackageNotFoundError:
127
+ __version__ = "0+unknown"
110
128
 
111
129
 
112
130
  def _build_static_auth(
@@ -169,6 +187,8 @@ __all__ = [
169
187
  "Mission",
170
188
  "ToolCall",
171
189
  "ProxyResponse",
190
+ "create_authority_request_id",
191
+ "ProxyOperation",
172
192
  "Skill",
173
193
  "SkillInvocation",
174
194
  "SkillRequest",
@@ -188,6 +208,8 @@ __all__ = [
188
208
  "DetectorConfigService",
189
209
  "PiiConfigService",
190
210
  "AuthorityService",
211
+ "FleetsService",
212
+ "AsyncFleetsService",
191
213
  "AsyncAuthorityService",
192
214
  # Offline / customer-managed crypto (parity with the JS SDK)
193
215
  "verify_passport_offline",
@@ -231,11 +253,10 @@ class Stack:
231
253
  if _auth:
232
254
  auth: ApiKeyAuth | SessionAuth | OAuthAuth | CredentialsFileAuth | AgentKeypairAuth = _auth
233
255
  elif agent_id:
234
- static_auth = _build_static_auth(api_key=api_key, base_url=base_url)
235
256
  auth = AgentKeypairAuth(
236
257
  agent_id=agent_id,
237
258
  api_base_url=base_url,
238
- bearer_provider=lambda: static_auth.get_headers()["Authorization"].split(" ", 1)[1],
259
+ bearer_provider=lambda: _build_static_auth(api_key=api_key, base_url=base_url).get_headers()["Authorization"].split(" ", 1)[1],
239
260
  )
240
261
  elif api_key or os.environ.get("STACK_API_KEY"):
241
262
  auth = ApiKeyAuth(api_key or os.environ["STACK_API_KEY"])
@@ -259,6 +280,7 @@ class Stack:
259
280
  self.identity = IdentityService(self._client)
260
281
  self.intents = IntentsService(self._client)
261
282
  self.missions = MissionsService(self._client)
283
+ self.fleets = FleetsService(self._client)
262
284
  self.evidence_packs = EvidencePacksService(self._client)
263
285
  self.llm = LlmService(self._client)
264
286
  self.inbound_webhooks = InboundWebhookService(self._client)
@@ -378,11 +400,10 @@ class AsyncStack:
378
400
  if _auth:
379
401
  auth: ApiKeyAuth | SessionAuth | OAuthAuth | CredentialsFileAuth | AgentKeypairAuth = _auth
380
402
  elif agent_id:
381
- static_auth = _build_static_auth(api_key=api_key, base_url=base_url)
382
403
  auth = AgentKeypairAuth(
383
404
  agent_id=agent_id,
384
405
  api_base_url=base_url,
385
- bearer_provider=lambda: static_auth.get_headers()["Authorization"].split(" ", 1)[1],
406
+ bearer_provider=lambda: _build_static_auth(api_key=api_key, base_url=base_url).get_headers()["Authorization"].split(" ", 1)[1],
386
407
  )
387
408
  elif api_key or os.environ.get("STACK_API_KEY"):
388
409
  auth = ApiKeyAuth(api_key or os.environ["STACK_API_KEY"])
@@ -438,6 +459,7 @@ class AsyncStack:
438
459
  self.identity = AsyncIdentityService(self._client)
439
460
  self.intents = AsyncIntentsService(self._client)
440
461
  self.missions = AsyncMissionsService(self._client)
462
+ self.fleets = AsyncFleetsService(self._client)
441
463
  self.llm = AsyncLlmService(self._client)
442
464
  self.inbound_webhooks = AsyncInboundWebhookService(self._client)
443
465
  self.partner_tenants = AsyncPartnerTenantsService(self._client)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from urllib.parse import quote
5
+ from .._fleet_key import prepare_fleet_key
6
+ from ..client import AsyncHttpClient
7
+
8
+
9
+ class AsyncFleetsService:
10
+ def __init__(self, client: AsyncHttpClient):
11
+ self._client = client
12
+
13
+ async def session(self, fleet_policy_id: str | None = None) -> dict[str, Any]:
14
+ return await self._client.request("POST", "/v1/fleets/session", json={"fleet_policy_id": fleet_policy_id} if fleet_policy_id else {})
15
+
16
+ async def workload_enrollment_challenge(self, policy_id: str) -> dict[str, Any]:
17
+ return await self._client.request("POST", f"/v1/fleets/{quote(policy_id, safe='')}/workload/enrollment-challenges", json={})
18
+
19
+ async def enroll_workload(self, policy_id: str, request: dict[str, Any]) -> dict[str, Any]:
20
+ return await self._client.request("POST", f"/v1/fleets/{quote(policy_id, safe='')}/workload/enroll", json=request)
21
+
22
+ async def enroll_workload_local(self, policy_id: str, *, name: str, idempotency_key: str) -> dict[str, Any]:
23
+ public_key, sign, persist = prepare_fleet_key(policy_id, idempotency_key)
24
+ challenge = await self.workload_enrollment_challenge(policy_id)
25
+ result = await self.enroll_workload(policy_id, {"name": name, "idempotency_key": idempotency_key,
26
+ "public_key": public_key, "challenge_id": challenge["challenge_id"], "proof": sign(challenge["challenge"])})
27
+ persist(result["agent_id"])
28
+ return result
29
+
30
+ async def enroll_local(self, policy_id: str, *, name: str, idempotency_key: str) -> dict[str, Any]:
31
+ public_key, sign, persist = prepare_fleet_key(policy_id, idempotency_key)
32
+ challenge = await self.enrollment_challenge(policy_id)
33
+ result = await self.enroll(policy_id, {"name": name, "idempotency_key": idempotency_key,
34
+ "public_key": public_key, "challenge_id": challenge["challenge_id"], "proof": sign(challenge["challenge"])})
35
+ persist(result["agent_id"])
36
+ return result
37
+
38
+ async def list(self) -> list[dict[str, Any]]:
39
+ return await self._client.request("GET", "/v1/fleets")
40
+
41
+ async def list_page(self, page: int = 1, limit: int = 20) -> dict[str, Any]:
42
+ return await self._client.request("GET", "/v1/fleets", params={"page": page, "limit": limit})
43
+
44
+ async def get(self, policy_id: str) -> dict[str, Any]:
45
+ return await self._client.request("GET", f"/v1/fleets/{quote(policy_id, safe='')}")
46
+
47
+ async def membership(self, agent_id: str) -> dict[str, Any] | None:
48
+ return await self._client.request("GET", f"/v1/fleets/membership/{quote(agent_id, safe='')}")
49
+
50
+ async def members_page(self, policy_id: str, page: int = 1, limit: int = 20) -> dict[str, Any]:
51
+ return await self._client.request("GET", f"/v1/fleets/{quote(policy_id, safe='')}/members", params={"page": page, "limit": limit})
52
+
53
+ async def activate(self, definition: dict[str, Any], *, idempotency_key: str,
54
+ governance_approval_id: str | None = None) -> dict[str, Any]:
55
+ headers = {"Idempotency-Key": idempotency_key}
56
+ if governance_approval_id is not None:
57
+ headers["X-Governance-Approval"] = governance_approval_id
58
+ return await self._client.request("POST", "/v1/fleets", json=definition, extra_headers=headers)
59
+
60
+ async def revise(self, policy_id: str, definition: dict[str, Any], *,
61
+ expected_revision: int, idempotency_key: str,
62
+ governance_approval_id: str | None = None) -> dict[str, Any]:
63
+ headers = {"If-Match": str(expected_revision), "Idempotency-Key": idempotency_key}
64
+ if governance_approval_id is not None:
65
+ headers["X-Governance-Approval"] = governance_approval_id
66
+ return await self._client.request("PUT", f"/v1/fleets/{quote(policy_id, safe='')}",
67
+ json=definition, extra_headers=headers)
68
+
69
+ async def members(self, policy_id: str) -> list[dict[str, Any]]:
70
+ return await self._client.request("GET", f"/v1/fleets/{quote(policy_id, safe='')}/members")
71
+
72
+ async def revoke_member(self, policy_id: str, agent_id: str) -> dict[str, Any]:
73
+ return await self._client.request("POST", f"/v1/fleets/{quote(policy_id, safe='')}/members/{quote(agent_id, safe='')}/revoke", json={})
74
+
75
+ async def revoke(self, policy_id: str) -> dict[str, Any]:
76
+ return await self._client.request("POST", f"/v1/fleets/{quote(policy_id, safe='')}/revoke", json={})
77
+
78
+ async def enrollment_challenge(self, policy_id: str) -> dict[str, Any]:
79
+ return await self._client.request("POST", f"/v1/fleets/{quote(policy_id, safe='')}/enrollment-challenges", json={})
80
+
81
+ async def enroll(self, policy_id: str, request: dict[str, Any]) -> dict[str, Any]:
82
+ return await self._client.request("POST", f"/v1/fleets/{quote(policy_id, safe='')}/enroll", json=request)
@@ -12,6 +12,12 @@ class AsyncMissionsService:
12
12
  def __init__(self, client: AsyncHttpClient):
13
13
  self._client = client
14
14
 
15
+ async def start(self, request: dict[str, Any]) -> dict[str, Any]:
16
+ return await self._client.request("POST", "/v1/missions/start", json=request)
17
+
18
+ async def renew(self, mission_id: str) -> dict[str, Any]:
19
+ return await self._client.request("POST", f"/v1/missions/{quote(mission_id, safe='')}/renew", json={})
20
+
15
21
  async def open(
16
22
  self,
17
23
  intent_summary: str,
@@ -29,6 +35,12 @@ class AsyncMissionsService:
29
35
  cap_behavior: dict[str, str] | None = None,
30
36
  expires_in_seconds: int | None = None,
31
37
  parent_passport_services: list[str] | None = None,
38
+ fleet_policy_id: str | None = None,
39
+ idempotency_key: str | None = None,
40
+ requested_access: list[dict[str, Any]] | None = None,
41
+ requested_operations: list[dict[str, Any]] | None = None,
42
+ limits: dict[str, Any] | None = None,
43
+ authority_binding_ids: list[str] | None = None,
32
44
  ) -> dict[str, Any]:
33
45
  body: dict[str, Any] = {"intent_summary": intent_summary}
34
46
  if intent_services is not None:
@@ -57,6 +69,11 @@ class AsyncMissionsService:
57
69
  body["expires_in_seconds"] = expires_in_seconds
58
70
  if parent_passport_services is not None:
59
71
  body["parent_passport_services"] = parent_passport_services
72
+ for key, value in {"fleet_policy_id": fleet_policy_id, "idempotency_key": idempotency_key,
73
+ "requested_access": requested_access, "requested_operations": requested_operations,
74
+ "limits": limits, "authority_binding_ids": authority_binding_ids}.items():
75
+ if value is not None:
76
+ body[key] = value
60
77
  return await self._client.request("POST", "/v1/missions", json=body)
61
78
 
62
79
  async def complete(
@@ -11,10 +11,10 @@ from __future__ import annotations
11
11
 
12
12
  import asyncio
13
13
  from contextlib import asynccontextmanager
14
- from typing import Any, AsyncGenerator
14
+ from typing import Any, AsyncGenerator, Awaitable, Callable
15
15
 
16
16
  from ..client import AsyncHttpClient
17
- from ..passports import _parse_duration, _to_passport
17
+ from ..passports import _parse_duration, _to_passport, _delegation_scopes
18
18
  from ..proxy import ProxyResponse
19
19
  from .proxy import AsyncProxyService
20
20
  from ..types import Passport, CheckpointResult, CheckoutResult, PassportReport, ToolCall
@@ -85,6 +85,8 @@ class AsyncMission:
85
85
  body: Any = None,
86
86
  query: dict[str, str] | None = None,
87
87
  approval_id: str | None = None,
88
+ authority_request_id: str | None = None,
89
+ on_operation_id: Callable[[str], None | Awaitable[None]] | None = None,
88
90
  _proxy_service: AsyncProxyService | None = None,
89
91
  ) -> ProxyResponse:
90
92
  """Async proxy call using this mission's passport.
@@ -103,6 +105,8 @@ class AsyncMission:
103
105
  headers=headers, body=body, query=query,
104
106
  passport_token=self.token,
105
107
  approval_id=approval_id,
108
+ authority_request_id=authority_request_id,
109
+ on_operation_id=on_operation_id,
106
110
  )
107
111
  self.log(service, method, url)
108
112
  return resp
@@ -213,6 +217,7 @@ class AsyncPassportService:
213
217
  ttl_seconds: int | None = None,
214
218
  scopes: list[dict[str, Any]] | None = None,
215
219
  authority_binding_ids: list[str] | None = None,
220
+ mission_ids: list[str] | None = None,
216
221
  ) -> Passport:
217
222
  """Issue a signed passport for an agent (async mirror of ``Stack``).
218
223
 
@@ -242,6 +247,8 @@ class AsyncPassportService:
242
247
  body["scopes"] = scopes
243
248
  if authority_binding_ids is not None:
244
249
  body["authority_binding_ids"] = authority_binding_ids
250
+ if mission_ids is not None:
251
+ body["mission_ids"] = mission_ids
245
252
  if ttl_seconds:
246
253
  body["ttl_seconds"] = ttl_seconds
247
254
  if intent or services:
@@ -335,12 +342,11 @@ class AsyncPassportService:
335
342
  self,
336
343
  parent_token: str,
337
344
  child_agent_id: str,
338
- scopes: list[str] | None = None,
345
+ scopes: list[dict[str, Any]] | list[str] | None = None,
339
346
  ttl_seconds: int | None = None,
340
347
  ) -> Passport:
341
- body: dict[str, Any] = {"parent_token": parent_token, "child_agent_id": child_agent_id}
342
- if scopes is not None:
343
- body["scopes"] = scopes
348
+ body: dict[str, Any] = {"parent_passport_token": parent_token, "child_agent_id": child_agent_id,
349
+ "scopes": _delegation_scopes(parent_token, scopes)}
344
350
  if ttl_seconds is not None:
345
351
  body["ttl_seconds"] = ttl_seconds
346
352
  return _to_passport(await self._client.post("/v1/passports/delegate", json=body))