getstack 0.8.0__tar.gz → 0.9.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 (52) hide show
  1. {getstack-0.8.0 → getstack-0.9.0}/PKG-INFO +1 -1
  2. {getstack-0.8.0 → getstack-0.9.0}/pyproject.toml +1 -1
  3. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/__init__.py +6 -0
  4. getstack-0.9.0/src/getstack/_async/inbound_webhooks.py +39 -0
  5. getstack-0.9.0/src/getstack/evidence_packs.py +78 -0
  6. getstack-0.9.0/src/getstack/inbound_webhooks.py +56 -0
  7. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/missions.py +62 -0
  8. {getstack-0.8.0 → getstack-0.9.0}/.gitignore +0 -0
  9. {getstack-0.8.0 → getstack-0.9.0}/CLAUDE.md +0 -0
  10. {getstack-0.8.0 → getstack-0.9.0}/LICENSE +0 -0
  11. {getstack-0.8.0 → getstack-0.9.0}/README.md +0 -0
  12. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/__init__.py +0 -0
  13. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/agents.py +0 -0
  14. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/audit.py +0 -0
  15. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/credentials.py +0 -0
  16. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/dropoffs.py +0 -0
  17. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/identity.py +0 -0
  18. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/intents.py +0 -0
  19. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/llm.py +0 -0
  20. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/missions.py +0 -0
  21. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/notifications.py +0 -0
  22. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/passport_session.py +0 -0
  23. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/passports.py +0 -0
  24. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/proxy.py +0 -0
  25. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/reviews.py +0 -0
  26. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/scan.py +0 -0
  27. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/security_events.py +0 -0
  28. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/services.py +0 -0
  29. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/skills.py +0 -0
  30. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/agent_auth.py +0 -0
  31. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/agents.py +0 -0
  32. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/audit.py +0 -0
  33. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/auth.py +0 -0
  34. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/browser_bootstrap.py +0 -0
  35. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/client.py +0 -0
  36. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/credentials.py +0 -0
  37. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/dropoffs.py +0 -0
  38. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/errors.py +0 -0
  39. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/identity.py +0 -0
  40. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/intents.py +0 -0
  41. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/llm.py +0 -0
  42. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/notifications.py +0 -0
  43. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/passport_session.py +0 -0
  44. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/passports.py +0 -0
  45. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/proxy.py +0 -0
  46. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/py.typed +0 -0
  47. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/reviews.py +0 -0
  48. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/scan.py +0 -0
  49. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/security_events.py +0 -0
  50. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/services.py +0 -0
  51. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/skills.py +0 -0
  52. {getstack-0.8.0 → getstack-0.9.0}/src/getstack/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.8.0
3
+ Version: 0.9.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.8.0"
7
+ version = "0.9.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -52,8 +52,10 @@ from .scan import ScanService
52
52
  from .security_events import SecurityEventService as SecurityEventSvc
53
53
  from .skills import SkillService
54
54
  from .identity import IdentityService
55
+ from .inbound_webhooks import InboundWebhookService
55
56
  from .intents import IntentsService
56
57
  from .missions import MissionsService
58
+ from .evidence_packs import EvidencePacksService
57
59
  from .llm import LlmService
58
60
  from .passport_session import PassportSession
59
61
  from .types import (
@@ -221,7 +223,9 @@ class Stack:
221
223
  self.identity = IdentityService(self._client)
222
224
  self.intents = IntentsService(self._client)
223
225
  self.missions = MissionsService(self._client)
226
+ self.evidence_packs = EvidencePacksService(self._client)
224
227
  self.llm = LlmService(self._client)
228
+ self.inbound_webhooks = InboundWebhookService(self._client)
225
229
 
226
230
  def issue_passport(
227
231
  self,
@@ -361,6 +365,7 @@ class AsyncStack:
361
365
  from ._async.intents import AsyncIntentsService
362
366
  from ._async.missions import AsyncMissionsService
363
367
  from ._async.llm import AsyncLlmService
368
+ from ._async.inbound_webhooks import AsyncInboundWebhookService
364
369
  from ._async.passport_session import AsyncPassportSession
365
370
 
366
371
  self.agents = AsyncAgentService(self._client)
@@ -379,6 +384,7 @@ class AsyncStack:
379
384
  self.intents = AsyncIntentsService(self._client)
380
385
  self.missions = AsyncMissionsService(self._client)
381
386
  self.llm = AsyncLlmService(self._client)
387
+ self.inbound_webhooks = AsyncInboundWebhookService(self._client)
382
388
  # Stash for issue_passport() — Python doesn't easily expose the
383
389
  # class binding without re-importing inside the method body.
384
390
  self._AsyncPassportSession = AsyncPassportSession
@@ -0,0 +1,39 @@
1
+ """Async mirror of InboundWebhookService (Phase 5.5)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, Optional
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ Provider = Literal["agentmail", "generic"]
11
+
12
+
13
+ class AsyncInboundWebhookService:
14
+ def __init__(self, client: AsyncHttpClient):
15
+ self._client = client
16
+
17
+ async def create(
18
+ self,
19
+ provider: Provider,
20
+ agent_id: str,
21
+ forward_url: Optional[str] = None,
22
+ generic_config: Optional[dict[str, Any]] = None,
23
+ ) -> dict[str, Any]:
24
+ body: dict[str, Any] = {"provider": provider, "agent_id": agent_id}
25
+ if forward_url is not None:
26
+ body["forward_url"] = forward_url
27
+ if generic_config is not None:
28
+ body["generic_config"] = generic_config
29
+ return await self._client.post("/v1/operator/inbound-webhooks", json=body)
30
+
31
+ async def list(self) -> list[dict[str, Any]]:
32
+ res = await self._client.get("/v1/operator/inbound-webhooks")
33
+ return list(res.get("webhooks", []))
34
+
35
+ async def get(self, webhook_id: str) -> dict[str, Any]:
36
+ return await self._client.get(f"/v1/operator/inbound-webhooks/{webhook_id}")
37
+
38
+ async def revoke(self, webhook_id: str) -> dict[str, Any]:
39
+ return await self._client.delete(f"/v1/operator/inbound-webhooks/{webhook_id}")
@@ -0,0 +1,78 @@
1
+ """Evidence packs — export, get, list.
2
+
3
+ Portable cryptographic bundles. Verifiable offline by any third party
4
+ via the open-source `@getstackrun/verify` package.
5
+
6
+ export() Export a pack for a mission / session / incident / period
7
+ scope. Returns the pack_id + the full canonical manifest
8
+ JSON inline. Persist the manifest to disk and hand it to
9
+ an auditor / insurer / regulator.
10
+ get() Fetch one pack by id. Returns the signed claim envelope
11
+ bytes alongside the parsed pack_claim.
12
+ list() List packs for the calling operator. Filter by scope.
13
+
14
+ Enterprise-tier only at the API layer.
15
+
16
+ Phase 5.4.7.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from typing import Any
23
+
24
+ from .client import HttpClient
25
+
26
+
27
+ class EvidencePacksService:
28
+ def __init__(self, client: HttpClient):
29
+ self._client = client
30
+
31
+ def export(
32
+ self,
33
+ *,
34
+ scope_kind: str,
35
+ scope_ref: str,
36
+ supersedes: str | None = None,
37
+ air_gap: bool | None = None,
38
+ output_path: str | None = None,
39
+ ) -> dict[str, Any]:
40
+ """Export an evidence pack.
41
+
42
+ `scope_kind` is one of `mission` | `session` | `incident` | `period`.
43
+ `scope_ref` is the matching reference (ms_* / ses_* / period:YYYY-Qn /
44
+ incident:<id>). The pack manifest is returned inline; pass
45
+ `output_path` to also write it to disk in one step.
46
+
47
+ Returns a dict with `pack_id`, `claim_id`, `pack_claim`, `manifest`.
48
+ """
49
+ body: dict[str, Any] = {
50
+ "scope": {"kind": scope_kind, "ref": scope_ref},
51
+ }
52
+ if supersedes is not None:
53
+ body["supersedes"] = supersedes
54
+ if air_gap is not None:
55
+ body["air_gap"] = air_gap
56
+ result = self._client.request("POST", "/v1/evidence-packs", json=body)
57
+ if output_path is not None and "manifest" in result:
58
+ with open(output_path, "w", encoding="utf-8") as f:
59
+ json.dump(result["manifest"], f, indent=2)
60
+ return result
61
+
62
+ def get(self, pack_id: str) -> dict[str, Any]:
63
+ """Fetch one pack by id. Returns the signed envelope_cose bytes and
64
+ the parsed `pack_claim` for convenient introspection."""
65
+ return self._client.request("GET", f"/v1/evidence-packs/{pack_id}")
66
+
67
+ def list(self, *, scope: str | None = None) -> dict[str, Any]:
68
+ """List packs for the calling operator.
69
+
70
+ `scope` filter is `<kind>:<ref>` (e.g. `mission:ms_abc`,
71
+ `period:2026-Q1`). Omit to list every pack. Newest first.
72
+ """
73
+ path = "/v1/evidence-packs"
74
+ if scope is not None:
75
+ from urllib.parse import urlencode
76
+
77
+ path = f"{path}?{urlencode({'scope': scope})}"
78
+ return self._client.request("GET", path)
@@ -0,0 +1,56 @@
1
+ """Hosted inbound webhook surface (Phase 5.5).
2
+
3
+ Operator creates a webhook row + receives a STACK-hosted URL + HMAC
4
+ secret. Paste both into a provider's (AgentMail / generic) webhook
5
+ config; STACK runs the prompt-injection detector on inbound content
6
+ before forwarding to a downstream URL.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Literal, Optional
12
+
13
+ from .client import HttpClient
14
+
15
+
16
+ Provider = Literal["agentmail", "generic"]
17
+ SignatureAlgorithm = Literal["hmac-sha256", "hmac-sha1"]
18
+
19
+
20
+ class InboundWebhookService:
21
+ def __init__(self, client: HttpClient):
22
+ self._client = client
23
+
24
+ def create(
25
+ self,
26
+ provider: Provider,
27
+ agent_id: str,
28
+ forward_url: Optional[str] = None,
29
+ generic_config: Optional[dict[str, Any]] = None,
30
+ ) -> dict[str, Any]:
31
+ """Create a hosted inbound webhook.
32
+
33
+ Returns a dict with keys: id, provider, slug, webhook_url,
34
+ webhook_secret (plaintext, returned ONLY at create), agent_id,
35
+ forward_url, created_at. Capture webhook_secret immediately —
36
+ STACK never returns it again. If lost, revoke + recreate.
37
+ """
38
+ body: dict[str, Any] = {"provider": provider, "agent_id": agent_id}
39
+ if forward_url is not None:
40
+ body["forward_url"] = forward_url
41
+ if generic_config is not None:
42
+ body["generic_config"] = generic_config
43
+ return self._client.post("/v1/operator/inbound-webhooks", json=body)
44
+
45
+ def list(self) -> list[dict[str, Any]]:
46
+ """List all inbound webhooks for the operator (active + revoked)."""
47
+ res = self._client.get("/v1/operator/inbound-webhooks")
48
+ return list(res.get("webhooks", []))
49
+
50
+ def get(self, webhook_id: str) -> dict[str, Any]:
51
+ """Get a single inbound webhook by id. Never returns the plaintext secret."""
52
+ return self._client.get(f"/v1/operator/inbound-webhooks/{webhook_id}")
53
+
54
+ def revoke(self, webhook_id: str) -> dict[str, Any]:
55
+ """Soft-delete an inbound webhook. Idempotent on repeat calls."""
56
+ return self._client.delete(f"/v1/operator/inbound-webhooks/{webhook_id}")
@@ -128,3 +128,65 @@ class MissionsService:
128
128
 
129
129
  path = f"{path}?{urlencode(params)}"
130
130
  return self._client.request("GET", path)
131
+
132
+ # ─── Phase 5.4 — replay + dossier ────────────────────────────────
133
+
134
+ def replay(self, mission_id: str) -> dict[str, Any]:
135
+ """Reconstruct the forensic timeline for a mission.
136
+
137
+ Returns a dict matching the @stack/shared `ReplayedTimeline` shape:
138
+ events ordered chronologically, behavioral_diff (close vs baseline
139
+ genome), cap_usage, dossiers chain, twin_divergence. Same shape the
140
+ open-source verifier produces from an exported pack.
141
+
142
+ Phase 5.4.5 / 5.4.7.
143
+ """
144
+ return self._client.request(
145
+ "GET", f"/v1/missions/{mission_id}/replay"
146
+ )
147
+
148
+ def list_dossiers(self, mission_id: str) -> dict[str, Any]:
149
+ """List every dossier authored for a mission, ordered chronologically
150
+ by `authored_at_ms`. Returns dict with `items` (each carrying
151
+ `body.narrative`, `body.lessons_learned`, etc.). Empty list when no
152
+ dossier exists.
153
+
154
+ Phase 5.4.2 / 5.4.7.
155
+ """
156
+ return self._client.request(
157
+ "GET", f"/v1/missions/{mission_id}/dossiers"
158
+ )
159
+
160
+ def create_dossier(
161
+ self,
162
+ mission_id: str,
163
+ *,
164
+ narrative: str,
165
+ lessons_learned: str,
166
+ incident_classification: str,
167
+ decision_tree: list[dict[str, Any]] | None = None,
168
+ alerts_walkthrough: list[dict[str, Any]] | None = None,
169
+ ) -> dict[str, Any]:
170
+ """Author a fresh dossier for a mission. The mission must be in a
171
+ terminal state (completed_* or revoked_*) — 409 surfaces otherwise.
172
+ `authored_by_member_id` is derived from the calling auth context
173
+ server-side (not passed here). Enterprise-tier only.
174
+
175
+ `incident_classification` is one of:
176
+ `false_positive` | `true_positive_handled`
177
+ | `true_positive_escalated` | `inconclusive`.
178
+
179
+ Phase 5.4.2 / 5.4.7.
180
+ """
181
+ body: dict[str, Any] = {
182
+ "narrative": narrative,
183
+ "lessons_learned": lessons_learned,
184
+ "incident_classification": incident_classification,
185
+ }
186
+ if decision_tree is not None:
187
+ body["decision_tree"] = decision_tree
188
+ if alerts_walkthrough is not None:
189
+ body["alerts_walkthrough"] = alerts_walkthrough
190
+ return self._client.request(
191
+ "POST", f"/v1/missions/{mission_id}/dossier", json=body
192
+ )
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