getstack 0.4.0__tar.gz → 0.5.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-0.4.0 → getstack-0.5.0}/.gitignore +7 -1
- {getstack-0.4.0 → getstack-0.5.0}/PKG-INFO +1 -1
- {getstack-0.4.0 → getstack-0.5.0}/pyproject.toml +1 -1
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/__init__.py +2 -0
- getstack-0.5.0/src/getstack/intents.py +196 -0
- {getstack-0.4.0 → getstack-0.5.0}/CLAUDE.md +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/LICENSE +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/README.md +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/agent_auth.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/agents.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/audit.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/auth.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/client.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/credentials.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/dropoffs.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/errors.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/identity.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/notifications.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/passports.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/proxy.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/py.typed +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/reviews.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/scan.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/security_events.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/services.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/src/getstack/skills.py +0 -0
- {getstack-0.4.0 → getstack-0.5.0}/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,196 @@
|
|
|
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 pending list until the row leaves
|
|
112
|
+
'pending' or the timeout elapses.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict with keys 'initial' (the submit response),
|
|
116
|
+
'final' (the last known row state), and 'timed_out' (bool).
|
|
117
|
+
When the row disappears from the pending list before timeout,
|
|
118
|
+
it has been decided; 'timed_out' is False.
|
|
119
|
+
"""
|
|
120
|
+
initial = self.submit(
|
|
121
|
+
intent,
|
|
122
|
+
passport_token,
|
|
123
|
+
expires_in_seconds=expires_in_seconds,
|
|
124
|
+
skip_simulation=skip_simulation,
|
|
125
|
+
)
|
|
126
|
+
approval_id = initial["approval_id"]
|
|
127
|
+
deadline = time.monotonic() + timeout_seconds
|
|
128
|
+
last_row: dict[str, Any] | None = None
|
|
129
|
+
while time.monotonic() < deadline:
|
|
130
|
+
response = self.list_pending()
|
|
131
|
+
items = response.get("items", [])
|
|
132
|
+
match = next((r for r in items if r.get("id") == approval_id), None)
|
|
133
|
+
if match is None:
|
|
134
|
+
# Row left the pending queue → terminal state.
|
|
135
|
+
final = last_row or {"id": approval_id, "status": "approved"}
|
|
136
|
+
return {"initial": initial, "final": final, "timed_out": False}
|
|
137
|
+
last_row = match
|
|
138
|
+
time.sleep(poll_interval_seconds)
|
|
139
|
+
return {
|
|
140
|
+
"initial": initial,
|
|
141
|
+
"final": last_row or {"id": approval_id, "status": "pending"},
|
|
142
|
+
"timed_out": True,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def list_pending(
|
|
146
|
+
self,
|
|
147
|
+
*,
|
|
148
|
+
page: int | None = None,
|
|
149
|
+
limit: int | None = None,
|
|
150
|
+
) -> dict[str, Any]:
|
|
151
|
+
"""List pending approvals scoped to the calling operator."""
|
|
152
|
+
params: dict[str, Any] = {}
|
|
153
|
+
if page is not None:
|
|
154
|
+
params["page"] = page
|
|
155
|
+
if limit is not None:
|
|
156
|
+
params["limit"] = limit
|
|
157
|
+
return self._client.request("GET", "/v1/intents/pending", params=params or None)
|
|
158
|
+
|
|
159
|
+
def approve(
|
|
160
|
+
self,
|
|
161
|
+
approval_id: str,
|
|
162
|
+
*,
|
|
163
|
+
notes: str | None = None,
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
"""Transition a pending approval → approved."""
|
|
166
|
+
body: dict[str, Any] = {}
|
|
167
|
+
if notes is not None:
|
|
168
|
+
body["notes"] = notes
|
|
169
|
+
return self._client.request(
|
|
170
|
+
"POST",
|
|
171
|
+
f"/v1/intents/{approval_id}/approve",
|
|
172
|
+
json=body,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def reject(
|
|
176
|
+
self,
|
|
177
|
+
approval_id: str,
|
|
178
|
+
*,
|
|
179
|
+
notes: str | None = None,
|
|
180
|
+
block_future: bool = False,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""Transition a pending approval → rejected.
|
|
183
|
+
|
|
184
|
+
Auto-revokes the passport. block_future=True additionally
|
|
185
|
+
suspends the agent so subsequent passports cannot be issued.
|
|
186
|
+
"""
|
|
187
|
+
body: dict[str, Any] = {}
|
|
188
|
+
if notes is not None:
|
|
189
|
+
body["notes"] = notes
|
|
190
|
+
if block_future:
|
|
191
|
+
body["block_future"] = True
|
|
192
|
+
return self._client.request(
|
|
193
|
+
"POST",
|
|
194
|
+
f"/v1/intents/{approval_id}/reject",
|
|
195
|
+
json=body,
|
|
196
|
+
)
|
|
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
|