getstack 0.4.0__tar.gz → 0.5.1__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-0.4.0 → getstack-0.5.1}/.gitignore +7 -1
- {getstack-0.4.0 → getstack-0.5.1}/PKG-INFO +1 -1
- {getstack-0.4.0 → getstack-0.5.1}/pyproject.toml +1 -1
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/__init__.py +2 -0
- getstack-0.5.1/src/getstack/intents.py +206 -0
- {getstack-0.4.0 → getstack-0.5.1}/CLAUDE.md +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/LICENSE +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/README.md +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/agent_auth.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/agents.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/audit.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/auth.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/client.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/credentials.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/dropoffs.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/errors.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/identity.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/notifications.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/passports.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/proxy.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/py.typed +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/reviews.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/scan.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/security_events.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/services.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/skills.py +0 -0
- {getstack-0.4.0 → getstack-0.5.1}/src/getstack/types.py +0 -0
|
@@ -9,9 +9,15 @@ __pycache__/
|
|
|
9
9
|
.env
|
|
10
10
|
.env.*
|
|
11
11
|
*.env
|
|
12
|
-
.npmrc
|
|
13
12
|
scripts/.env.cto
|
|
14
13
|
|
|
14
|
+
# Repo .npmrc files carry supply-chain policy (minimum-release-age,
|
|
15
|
+
# onlyBuiltDependencies hints, auto-install-peers) and MUST be tracked
|
|
16
|
+
# so CI + Docker + every developer inherits the same gate. Block only
|
|
17
|
+
# user-local override files that conventionally carry auth tokens.
|
|
18
|
+
.npmrc.local
|
|
19
|
+
~/.npmrc
|
|
20
|
+
|
|
15
21
|
# Strategy docs (not sensitive, but private)
|
|
16
22
|
stack-claude-code-spec-v3.md
|
|
17
23
|
stack-gtm-distribution.md
|
|
@@ -51,6 +51,7 @@ from .scan import ScanService
|
|
|
51
51
|
from .security_events import SecurityEventService as SecurityEventSvc
|
|
52
52
|
from .skills import SkillService
|
|
53
53
|
from .identity import IdentityService
|
|
54
|
+
from .intents import IntentsService
|
|
54
55
|
from .types import (
|
|
55
56
|
Agent,
|
|
56
57
|
Passport,
|
|
@@ -214,6 +215,7 @@ class Stack:
|
|
|
214
215
|
self.security_events = SecurityEventSvc(self._client)
|
|
215
216
|
self.skills = SkillService(self._client)
|
|
216
217
|
self.identity = IdentityService(self._client)
|
|
218
|
+
self.intents = IntentsService(self._client)
|
|
217
219
|
|
|
218
220
|
@classmethod
|
|
219
221
|
def from_session(cls, session_token: str, **kwargs) -> Stack:
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Intent simulation + pre-execution approval gate.
|
|
2
|
+
|
|
3
|
+
Phase 2.5a Macro 2 Sub-chunks 2.1 + 2.2.
|
|
4
|
+
|
|
5
|
+
simulate() Dry-run a candidate IntentClaim against a passport.
|
|
6
|
+
Returns a signed simulation result.
|
|
7
|
+
submit() Register the Intent + run simulation + persist a
|
|
8
|
+
pending intent_approvals row. Returns an approval id;
|
|
9
|
+
operators decide via approve() / reject().
|
|
10
|
+
submit_and_wait()
|
|
11
|
+
Submit then poll until the approval reaches a terminal
|
|
12
|
+
state or the timeout elapses.
|
|
13
|
+
list_pending()
|
|
14
|
+
Operator-scope queue of pending approvals.
|
|
15
|
+
approve() / reject()
|
|
16
|
+
Operator transitions a pending row to approved or
|
|
17
|
+
rejected. Rejection auto-revokes the passport;
|
|
18
|
+
block_future=True additionally suspends the agent.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from .client import HttpClient
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class IntentsService:
|
|
30
|
+
def __init__(self, client: HttpClient):
|
|
31
|
+
self._client = client
|
|
32
|
+
|
|
33
|
+
# ─── 2.1 — simulate ─────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
def simulate(
|
|
36
|
+
self,
|
|
37
|
+
intent: dict[str, Any],
|
|
38
|
+
passport_token: str,
|
|
39
|
+
*,
|
|
40
|
+
intent_claim_ref: str | None = None,
|
|
41
|
+
persist: bool | None = None,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""Dry-run an Intent. No approval row created.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
intent: IntentClaim payload (type, intent_type, agent_id,
|
|
47
|
+
named_intent, target, action, parameters, estimated_cost,
|
|
48
|
+
accountability, reason, requires, user_subject,
|
|
49
|
+
mission_ref, submitted_at).
|
|
50
|
+
passport_token: Passport JWT to simulate against.
|
|
51
|
+
intent_claim_ref: Optional pc_* claim id to link this
|
|
52
|
+
simulation to a pre-registered Intent.
|
|
53
|
+
persist: When False, skips writing the simulation claim.
|
|
54
|
+
Default True.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dict with allowed, denial_reasons, simulated_cost,
|
|
58
|
+
predicted_detector_fires, simulated_at, claim_id?,
|
|
59
|
+
diagnostics.
|
|
60
|
+
"""
|
|
61
|
+
body: dict[str, Any] = {"intent": intent}
|
|
62
|
+
if intent_claim_ref is not None:
|
|
63
|
+
body["intent_claim_ref"] = intent_claim_ref
|
|
64
|
+
if persist is not None:
|
|
65
|
+
body["persist"] = persist
|
|
66
|
+
return self._client.request(
|
|
67
|
+
"POST",
|
|
68
|
+
"/v1/intents/simulate",
|
|
69
|
+
json=body,
|
|
70
|
+
extra_headers={"X-Passport-Token": passport_token},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# ─── 2.2 — submit / approval flow ───────────────────────────────
|
|
74
|
+
|
|
75
|
+
def submit(
|
|
76
|
+
self,
|
|
77
|
+
intent: dict[str, Any],
|
|
78
|
+
passport_token: str,
|
|
79
|
+
*,
|
|
80
|
+
expires_in_seconds: int | None = None,
|
|
81
|
+
skip_simulation: bool = False,
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
"""Submit an Intent for pre-execution operator approval.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dict with approval_id, intent_claim_id, simulated_claim_id?,
|
|
87
|
+
status='pending', expires_at, simulation? (when not skipped).
|
|
88
|
+
"""
|
|
89
|
+
body: dict[str, Any] = {"intent": intent}
|
|
90
|
+
if expires_in_seconds is not None:
|
|
91
|
+
body["expires_in_seconds"] = expires_in_seconds
|
|
92
|
+
if skip_simulation:
|
|
93
|
+
body["skip_simulation"] = True
|
|
94
|
+
return self._client.request(
|
|
95
|
+
"POST",
|
|
96
|
+
"/v1/intents/submit",
|
|
97
|
+
json=body,
|
|
98
|
+
extra_headers={"X-Passport-Token": passport_token},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def submit_and_wait(
|
|
102
|
+
self,
|
|
103
|
+
intent: dict[str, Any],
|
|
104
|
+
passport_token: str,
|
|
105
|
+
*,
|
|
106
|
+
expires_in_seconds: int | None = None,
|
|
107
|
+
skip_simulation: bool = False,
|
|
108
|
+
timeout_seconds: float = 60.0,
|
|
109
|
+
poll_interval_seconds: float = 1.0,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
"""Submit then poll the per-id endpoint until the row reaches a
|
|
112
|
+
terminal status (approved | rejected | expired) or the timeout
|
|
113
|
+
elapses.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict with keys 'initial' (the submit response),
|
|
117
|
+
'final' (the last known row state), and 'timed_out' (bool).
|
|
118
|
+
When the row reaches a terminal status before timeout,
|
|
119
|
+
'timed_out' is False and 'final["status"]' is one of
|
|
120
|
+
approved | rejected | expired.
|
|
121
|
+
"""
|
|
122
|
+
initial = self.submit(
|
|
123
|
+
intent,
|
|
124
|
+
passport_token,
|
|
125
|
+
expires_in_seconds=expires_in_seconds,
|
|
126
|
+
skip_simulation=skip_simulation,
|
|
127
|
+
)
|
|
128
|
+
approval_id = initial["approval_id"]
|
|
129
|
+
deadline = time.monotonic() + timeout_seconds
|
|
130
|
+
last_row: dict[str, Any] | None = None
|
|
131
|
+
# Use GET /v1/intents/:id rather than list_pending so we can
|
|
132
|
+
# read the actual terminal status — list_pending alone can't
|
|
133
|
+
# distinguish 'no longer pending' from 'approved'.
|
|
134
|
+
while time.monotonic() < deadline:
|
|
135
|
+
row = self.get(approval_id)
|
|
136
|
+
last_row = row
|
|
137
|
+
if row.get("status") != "pending":
|
|
138
|
+
return {"initial": initial, "final": row, "timed_out": False}
|
|
139
|
+
time.sleep(poll_interval_seconds)
|
|
140
|
+
return {
|
|
141
|
+
"initial": initial,
|
|
142
|
+
"final": last_row or {"id": approval_id, "status": "pending"},
|
|
143
|
+
"timed_out": True,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def get(self, approval_id: str) -> dict[str, Any]:
|
|
147
|
+
"""Fetch a single approval row by id, regardless of status.
|
|
148
|
+
|
|
149
|
+
Returns the full row when the calling operator owns it. Raises
|
|
150
|
+
the SDK's standard 404 error when the row doesn't exist or
|
|
151
|
+
belongs to a different operator (cross-tenant existence hidden).
|
|
152
|
+
"""
|
|
153
|
+
return self._client.request("GET", f"/v1/intents/{approval_id}")
|
|
154
|
+
|
|
155
|
+
def list_pending(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
page: int | None = None,
|
|
159
|
+
limit: int | None = None,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""List pending approvals scoped to the calling operator."""
|
|
162
|
+
params: dict[str, Any] = {}
|
|
163
|
+
if page is not None:
|
|
164
|
+
params["page"] = page
|
|
165
|
+
if limit is not None:
|
|
166
|
+
params["limit"] = limit
|
|
167
|
+
return self._client.request("GET", "/v1/intents/pending", params=params or None)
|
|
168
|
+
|
|
169
|
+
def approve(
|
|
170
|
+
self,
|
|
171
|
+
approval_id: str,
|
|
172
|
+
*,
|
|
173
|
+
notes: str | None = None,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Transition a pending approval → approved."""
|
|
176
|
+
body: dict[str, Any] = {}
|
|
177
|
+
if notes is not None:
|
|
178
|
+
body["notes"] = notes
|
|
179
|
+
return self._client.request(
|
|
180
|
+
"POST",
|
|
181
|
+
f"/v1/intents/{approval_id}/approve",
|
|
182
|
+
json=body,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def reject(
|
|
186
|
+
self,
|
|
187
|
+
approval_id: str,
|
|
188
|
+
*,
|
|
189
|
+
notes: str | None = None,
|
|
190
|
+
block_future: bool = False,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""Transition a pending approval → rejected.
|
|
193
|
+
|
|
194
|
+
Auto-revokes the passport. block_future=True additionally
|
|
195
|
+
suspends the agent so subsequent passports cannot be issued.
|
|
196
|
+
"""
|
|
197
|
+
body: dict[str, Any] = {}
|
|
198
|
+
if notes is not None:
|
|
199
|
+
body["notes"] = notes
|
|
200
|
+
if block_future:
|
|
201
|
+
body["block_future"] = True
|
|
202
|
+
return self._client.request(
|
|
203
|
+
"POST",
|
|
204
|
+
f"/v1/intents/{approval_id}/reject",
|
|
205
|
+
json=body,
|
|
206
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|