getstack 0.8.0__tar.gz → 0.10.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.10.0}/PKG-INFO +1 -1
- {getstack-0.8.0 → getstack-0.10.0}/pyproject.toml +1 -1
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/__init__.py +9 -0
- getstack-0.10.0/src/getstack/_async/inbound_webhooks.py +39 -0
- getstack-0.10.0/src/getstack/_async/partner_tenants.py +78 -0
- getstack-0.10.0/src/getstack/evidence_packs.py +78 -0
- getstack-0.10.0/src/getstack/inbound_webhooks.py +56 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/missions.py +62 -0
- getstack-0.10.0/src/getstack/partner_tenants.py +83 -0
- {getstack-0.8.0 → getstack-0.10.0}/.gitignore +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/CLAUDE.md +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/LICENSE +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/README.md +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/__init__.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/agents.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/audit.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/credentials.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/dropoffs.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/identity.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/intents.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/llm.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/missions.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/notifications.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/passport_session.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/passports.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/proxy.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/reviews.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/scan.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/security_events.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/services.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/_async/skills.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/agent_auth.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/agents.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/audit.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/auth.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/client.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/credentials.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/dropoffs.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/errors.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/identity.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/intents.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/llm.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/notifications.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/passport_session.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/passports.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/proxy.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/py.typed +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/reviews.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/scan.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/security_events.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/services.py +0 -0
- {getstack-0.8.0 → getstack-0.10.0}/src/getstack/skills.py +0 -0
- {getstack-0.8.0 → getstack-0.10.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,10 @@ 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)
|
|
229
|
+
self.partner_tenants = PartnerTenantsService(self._client)
|
|
225
230
|
|
|
226
231
|
def issue_passport(
|
|
227
232
|
self,
|
|
@@ -361,6 +366,8 @@ class AsyncStack:
|
|
|
361
366
|
from ._async.intents import AsyncIntentsService
|
|
362
367
|
from ._async.missions import AsyncMissionsService
|
|
363
368
|
from ._async.llm import AsyncLlmService
|
|
369
|
+
from ._async.inbound_webhooks import AsyncInboundWebhookService
|
|
370
|
+
from ._async.partner_tenants import AsyncPartnerTenantsService
|
|
364
371
|
from ._async.passport_session import AsyncPassportSession
|
|
365
372
|
|
|
366
373
|
self.agents = AsyncAgentService(self._client)
|
|
@@ -379,6 +386,8 @@ class AsyncStack:
|
|
|
379
386
|
self.intents = AsyncIntentsService(self._client)
|
|
380
387
|
self.missions = AsyncMissionsService(self._client)
|
|
381
388
|
self.llm = AsyncLlmService(self._client)
|
|
389
|
+
self.inbound_webhooks = AsyncInboundWebhookService(self._client)
|
|
390
|
+
self.partner_tenants = AsyncPartnerTenantsService(self._client)
|
|
382
391
|
# Stash for issue_passport() — Python doesn't easily expose the
|
|
383
392
|
# class binding without re-importing inside the method body.
|
|
384
393
|
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
|
+
"""Async mirror of PartnerTenantsService (Phase 5.6)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from ..client import AsyncHttpClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Tier = Literal["free", "developer", "studio", "enterprise"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncPartnerTenantsService:
|
|
14
|
+
def __init__(self, client: AsyncHttpClient):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
async def create(
|
|
18
|
+
self,
|
|
19
|
+
external_customer_id: Optional[str] = None,
|
|
20
|
+
tier: Optional[Tier] = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
body: dict[str, Any] = {}
|
|
23
|
+
if external_customer_id is not None:
|
|
24
|
+
body["external_customer_id"] = external_customer_id
|
|
25
|
+
if tier is not None:
|
|
26
|
+
body["tier"] = tier
|
|
27
|
+
return await self._client.post("/v1/partner/tenants", json=body)
|
|
28
|
+
|
|
29
|
+
async def list(self) -> list[dict[str, Any]]:
|
|
30
|
+
res = await self._client.get("/v1/partner/tenants")
|
|
31
|
+
return list(res.get("tenants", []))
|
|
32
|
+
|
|
33
|
+
async def get(self, tenant_id: str) -> dict[str, Any]:
|
|
34
|
+
return await self._client.get(f"/v1/partner/tenants/{tenant_id}")
|
|
35
|
+
|
|
36
|
+
async def revoke(self, tenant_id: str) -> dict[str, Any]:
|
|
37
|
+
return await self._client.delete(f"/v1/partner/tenants/{tenant_id}")
|
|
38
|
+
|
|
39
|
+
async def create_inbound_webhook(
|
|
40
|
+
self,
|
|
41
|
+
tenant_id: str,
|
|
42
|
+
provider: Literal["agentmail", "generic"],
|
|
43
|
+
agent_id: str,
|
|
44
|
+
forward_url: Optional[str] = None,
|
|
45
|
+
generic_config: Optional[dict[str, Any]] = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
body: dict[str, Any] = {"provider": provider, "agent_id": agent_id}
|
|
48
|
+
if forward_url is not None:
|
|
49
|
+
body["forward_url"] = forward_url
|
|
50
|
+
if generic_config is not None:
|
|
51
|
+
body["generic_config"] = generic_config
|
|
52
|
+
return await self._client.post(
|
|
53
|
+
f"/v1/partner/tenants/{tenant_id}/inbound-webhooks",
|
|
54
|
+
json=body,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def issue_passport(
|
|
58
|
+
self,
|
|
59
|
+
tenant_id: str,
|
|
60
|
+
agent_id: str,
|
|
61
|
+
ttl_seconds: Optional[int] = None,
|
|
62
|
+
services: Optional[list[dict[str, Any]]] = None,
|
|
63
|
+
named_intents: Optional[list[str]] = None,
|
|
64
|
+
accountability_mode: Optional[Literal["enforced", "logged", "standard"]] = None,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
body: dict[str, Any] = {"agent_id": agent_id}
|
|
67
|
+
if ttl_seconds is not None:
|
|
68
|
+
body["ttl_seconds"] = ttl_seconds
|
|
69
|
+
if services is not None:
|
|
70
|
+
body["services"] = services
|
|
71
|
+
if named_intents is not None:
|
|
72
|
+
body["named_intents"] = named_intents
|
|
73
|
+
if accountability_mode is not None:
|
|
74
|
+
body["accountability_mode"] = accountability_mode
|
|
75
|
+
return await self._client.post(
|
|
76
|
+
f"/v1/partner/tenants/{tenant_id}/passports/issue",
|
|
77
|
+
json=body,
|
|
78
|
+
)
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Partner-tenancy surface (Phase 5.6).
|
|
2
|
+
|
|
3
|
+
Platform partners (AgentMail, Cofounder, etc.) with is_partner=True on
|
|
4
|
+
their operator row provision isolated child operators on behalf of
|
|
5
|
+
their customers via this service.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Literal, Optional
|
|
11
|
+
|
|
12
|
+
from .client import HttpClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Tier = Literal["free", "developer", "studio", "enterprise"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PartnerTenantsService:
|
|
19
|
+
def __init__(self, client: HttpClient):
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def create(
|
|
23
|
+
self,
|
|
24
|
+
external_customer_id: Optional[str] = None,
|
|
25
|
+
tier: Optional[Tier] = None,
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
body: dict[str, Any] = {}
|
|
28
|
+
if external_customer_id is not None:
|
|
29
|
+
body["external_customer_id"] = external_customer_id
|
|
30
|
+
if tier is not None:
|
|
31
|
+
body["tier"] = tier
|
|
32
|
+
return self._client.post("/v1/partner/tenants", json=body)
|
|
33
|
+
|
|
34
|
+
def list(self) -> list[dict[str, Any]]:
|
|
35
|
+
res = self._client.get("/v1/partner/tenants")
|
|
36
|
+
return list(res.get("tenants", []))
|
|
37
|
+
|
|
38
|
+
def get(self, tenant_id: str) -> dict[str, Any]:
|
|
39
|
+
return self._client.get(f"/v1/partner/tenants/{tenant_id}")
|
|
40
|
+
|
|
41
|
+
def revoke(self, tenant_id: str) -> dict[str, Any]:
|
|
42
|
+
return self._client.delete(f"/v1/partner/tenants/{tenant_id}")
|
|
43
|
+
|
|
44
|
+
def create_inbound_webhook(
|
|
45
|
+
self,
|
|
46
|
+
tenant_id: str,
|
|
47
|
+
provider: Literal["agentmail", "generic"],
|
|
48
|
+
agent_id: str,
|
|
49
|
+
forward_url: Optional[str] = None,
|
|
50
|
+
generic_config: Optional[dict[str, Any]] = None,
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
body: dict[str, Any] = {"provider": provider, "agent_id": agent_id}
|
|
53
|
+
if forward_url is not None:
|
|
54
|
+
body["forward_url"] = forward_url
|
|
55
|
+
if generic_config is not None:
|
|
56
|
+
body["generic_config"] = generic_config
|
|
57
|
+
return self._client.post(
|
|
58
|
+
f"/v1/partner/tenants/{tenant_id}/inbound-webhooks",
|
|
59
|
+
json=body,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def issue_passport(
|
|
63
|
+
self,
|
|
64
|
+
tenant_id: str,
|
|
65
|
+
agent_id: str,
|
|
66
|
+
ttl_seconds: Optional[int] = None,
|
|
67
|
+
services: Optional[list[dict[str, Any]]] = None,
|
|
68
|
+
named_intents: Optional[list[str]] = None,
|
|
69
|
+
accountability_mode: Optional[Literal["enforced", "logged", "standard"]] = None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
body: dict[str, Any] = {"agent_id": agent_id}
|
|
72
|
+
if ttl_seconds is not None:
|
|
73
|
+
body["ttl_seconds"] = ttl_seconds
|
|
74
|
+
if services is not None:
|
|
75
|
+
body["services"] = services
|
|
76
|
+
if named_intents is not None:
|
|
77
|
+
body["named_intents"] = named_intents
|
|
78
|
+
if accountability_mode is not None:
|
|
79
|
+
body["accountability_mode"] = accountability_mode
|
|
80
|
+
return self._client.post(
|
|
81
|
+
f"/v1/partner/tenants/{tenant_id}/passports/issue",
|
|
82
|
+
json=body,
|
|
83
|
+
)
|
|
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
|