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.
- {getstack-1.2.0 → getstack-1.4.0}/.gitignore +3 -0
- {getstack-1.2.0 → getstack-1.4.0}/PKG-INFO +44 -40
- {getstack-1.2.0 → getstack-1.4.0}/README.md +43 -39
- {getstack-1.2.0 → getstack-1.4.0}/pyproject.toml +1 -1
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/__init__.py +59 -37
- getstack-1.4.0/src/getstack/_async/fleets.py +82 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/missions.py +17 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/passports.py +12 -6
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/proxy.py +41 -10
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_authority_request.py +6 -0
- getstack-1.4.0/src/getstack/_fleet_key.py +44 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/agent_auth.py +9 -1
- getstack-1.4.0/src/getstack/fleets.py +82 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/missions.py +17 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/passports.py +36 -5
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/proxy.py +71 -12
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_authority.py +140 -0
- getstack-1.4.0/tests/test_fleets.py +94 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_parity.py +7 -1
- {getstack-1.2.0 → getstack-1.4.0}/CLAUDE.md +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/LICENSE +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/scripts/verify_sign_claim_crosslang.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/__init__.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/agents.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/audit.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/authority.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/credentials.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/delivery_methods.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/detector_config.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/dropoffs.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/evidence_packs.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/identity.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/inbound_webhooks.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/intents.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/llm.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/notifications.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/partner_connect.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/partner_tenants.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/passport_session.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/pii_config.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/reviews.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/scan.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/security_events.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/services.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/skills.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/team.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/_async/usage.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/agents.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/audit.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/auth.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/authority.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/client.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/credentials.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/delivery_methods.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/detector_config.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/dropoffs.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/errors.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/evidence_packs.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/identity.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/inbound_webhooks.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/intents.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/llm.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/notifications.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/partner_connect.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/partner_tenants.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/passport_header_verify.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/passport_session.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/pii_config.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/py.typed +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/reviews.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/scan.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/security_events.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/services.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/sign_claim.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/skills.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/team.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/types.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/usage.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/src/getstack/verify_offline.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_delivery_methods.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_dropoffs.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_errors.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_evidence_packs.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_identity_requirements.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_inbound_webhooks.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_missions.py +0 -0
- {getstack-1.2.0 → getstack-1.4.0}/tests/test_partner_signing.py +0 -0
- {getstack-1.2.0 → getstack-1.4.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.
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
##
|
|
109
|
+
## Renew a Mission's Passport
|
|
98
110
|
|
|
99
111
|
```python
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
##
|
|
82
|
+
## Renew a Mission's Passport
|
|
71
83
|
|
|
72
84
|
```python
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
@@ -1,39 +1,51 @@
|
|
|
1
|
-
"""STACK Python SDK
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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] = {"
|
|
342
|
-
|
|
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))
|