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.
Files changed (28) hide show
  1. {getstack-0.4.0 → getstack-0.5.0}/.gitignore +7 -1
  2. {getstack-0.4.0 → getstack-0.5.0}/PKG-INFO +1 -1
  3. {getstack-0.4.0 → getstack-0.5.0}/pyproject.toml +1 -1
  4. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/__init__.py +2 -0
  5. getstack-0.5.0/src/getstack/intents.py +196 -0
  6. {getstack-0.4.0 → getstack-0.5.0}/CLAUDE.md +0 -0
  7. {getstack-0.4.0 → getstack-0.5.0}/LICENSE +0 -0
  8. {getstack-0.4.0 → getstack-0.5.0}/README.md +0 -0
  9. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/agent_auth.py +0 -0
  10. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/agents.py +0 -0
  11. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/audit.py +0 -0
  12. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/auth.py +0 -0
  13. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/browser_bootstrap.py +0 -0
  14. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/client.py +0 -0
  15. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/credentials.py +0 -0
  16. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/dropoffs.py +0 -0
  17. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/errors.py +0 -0
  18. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/identity.py +0 -0
  19. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/notifications.py +0 -0
  20. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/passports.py +0 -0
  21. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/proxy.py +0 -0
  22. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/py.typed +0 -0
  23. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/reviews.py +0 -0
  24. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/scan.py +0 -0
  25. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/security_events.py +0 -0
  26. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/services.py +0 -0
  27. {getstack-0.4.0 → getstack-0.5.0}/src/getstack/skills.py +0 -0
  28. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -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.0"
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,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