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.
- {getstack-0.8.0 → getstack-0.9.0}/PKG-INFO +1 -1
- {getstack-0.8.0 → getstack-0.9.0}/pyproject.toml +1 -1
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/__init__.py +6 -0
- getstack-0.9.0/src/getstack/_async/inbound_webhooks.py +39 -0
- getstack-0.9.0/src/getstack/evidence_packs.py +78 -0
- getstack-0.9.0/src/getstack/inbound_webhooks.py +56 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/missions.py +62 -0
- {getstack-0.8.0 → getstack-0.9.0}/.gitignore +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/CLAUDE.md +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/LICENSE +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/README.md +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/__init__.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/agents.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/audit.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/credentials.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/dropoffs.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/identity.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/intents.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/llm.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/missions.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/notifications.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/passport_session.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/passports.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/proxy.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/reviews.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/scan.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/security_events.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/services.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/_async/skills.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/agent_auth.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/agents.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/audit.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/auth.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/client.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/credentials.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/dropoffs.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/errors.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/identity.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/intents.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/llm.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/notifications.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/passport_session.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/passports.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/proxy.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/py.typed +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/reviews.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/scan.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/security_events.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/services.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/skills.py +0 -0
- {getstack-0.8.0 → getstack-0.9.0}/src/getstack/types.py +0 -0
|
@@ -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
|
|
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
|
|
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
|