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.
Files changed (28) hide show
  1. {getstack-0.4.0 → getstack-0.5.1}/.gitignore +7 -1
  2. {getstack-0.4.0 → getstack-0.5.1}/PKG-INFO +1 -1
  3. {getstack-0.4.0 → getstack-0.5.1}/pyproject.toml +1 -1
  4. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/__init__.py +2 -0
  5. getstack-0.5.1/src/getstack/intents.py +206 -0
  6. {getstack-0.4.0 → getstack-0.5.1}/CLAUDE.md +0 -0
  7. {getstack-0.4.0 → getstack-0.5.1}/LICENSE +0 -0
  8. {getstack-0.4.0 → getstack-0.5.1}/README.md +0 -0
  9. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/agent_auth.py +0 -0
  10. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/agents.py +0 -0
  11. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/audit.py +0 -0
  12. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/auth.py +0 -0
  13. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/browser_bootstrap.py +0 -0
  14. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/client.py +0 -0
  15. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/credentials.py +0 -0
  16. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/dropoffs.py +0 -0
  17. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/errors.py +0 -0
  18. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/identity.py +0 -0
  19. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/notifications.py +0 -0
  20. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/passports.py +0 -0
  21. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/proxy.py +0 -0
  22. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/py.typed +0 -0
  23. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/reviews.py +0 -0
  24. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/scan.py +0 -0
  25. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/security_events.py +0 -0
  26. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/services.py +0 -0
  27. {getstack-0.4.0 → getstack-0.5.1}/src/getstack/skills.py +0 -0
  28. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "0.4.0"
7
+ version = "0.5.1"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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