getstack 0.13.0__tar.gz → 0.14.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 (70) hide show
  1. getstack-0.14.0/CLAUDE.md +76 -0
  2. {getstack-0.13.0 → getstack-0.14.0}/PKG-INFO +2 -1
  3. {getstack-0.13.0 → getstack-0.14.0}/pyproject.toml +6 -1
  4. getstack-0.14.0/scripts/verify_sign_claim_crosslang.py +125 -0
  5. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/__init__.py +24 -0
  6. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/agents.py +16 -2
  7. getstack-0.14.0/src/getstack/_async/delivery_methods.py +35 -0
  8. getstack-0.14.0/src/getstack/_async/detector_config.py +27 -0
  9. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/dropoffs.py +3 -0
  10. getstack-0.14.0/src/getstack/_async/evidence_packs.py +44 -0
  11. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/notifications.py +3 -0
  12. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/passports.py +35 -0
  13. getstack-0.14.0/src/getstack/_async/services.py +81 -0
  14. getstack-0.14.0/src/getstack/_async/team.py +37 -0
  15. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/agents.py +4 -0
  16. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/client.py +11 -0
  17. getstack-0.14.0/src/getstack/delivery_methods.py +35 -0
  18. getstack-0.14.0/src/getstack/detector_config.py +39 -0
  19. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/dropoffs.py +4 -0
  20. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/notifications.py +4 -0
  21. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/passports.py +38 -0
  22. getstack-0.14.0/src/getstack/services.py +96 -0
  23. getstack-0.14.0/src/getstack/sign_claim.py +96 -0
  24. getstack-0.14.0/src/getstack/team.py +37 -0
  25. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/types.py +5 -0
  26. getstack-0.14.0/src/getstack/verify_offline.py +82 -0
  27. getstack-0.14.0/tests/test_parity.py +109 -0
  28. getstack-0.14.0/tests/test_sign_claim.py +147 -0
  29. getstack-0.13.0/CLAUDE.md +0 -46
  30. getstack-0.13.0/src/getstack/_async/services.py +0 -41
  31. getstack-0.13.0/src/getstack/services.py +0 -41
  32. {getstack-0.13.0 → getstack-0.14.0}/.gitignore +0 -0
  33. {getstack-0.13.0 → getstack-0.14.0}/LICENSE +0 -0
  34. {getstack-0.13.0 → getstack-0.14.0}/README.md +0 -0
  35. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/__init__.py +0 -0
  36. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/audit.py +0 -0
  37. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/credentials.py +0 -0
  38. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/identity.py +0 -0
  39. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/inbound_webhooks.py +0 -0
  40. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/intents.py +0 -0
  41. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/llm.py +0 -0
  42. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/missions.py +0 -0
  43. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/partner_tenants.py +0 -0
  44. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/passport_session.py +0 -0
  45. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/proxy.py +0 -0
  46. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/reviews.py +0 -0
  47. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/scan.py +0 -0
  48. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/security_events.py +0 -0
  49. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/skills.py +0 -0
  50. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/agent_auth.py +0 -0
  51. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/audit.py +0 -0
  52. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/auth.py +0 -0
  53. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/browser_bootstrap.py +0 -0
  54. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/credentials.py +0 -0
  55. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/errors.py +0 -0
  56. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/evidence_packs.py +0 -0
  57. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/identity.py +0 -0
  58. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/inbound_webhooks.py +0 -0
  59. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/intents.py +0 -0
  60. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/llm.py +0 -0
  61. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/missions.py +0 -0
  62. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/partner_tenants.py +0 -0
  63. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/passport_header_verify.py +0 -0
  64. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/passport_session.py +0 -0
  65. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/proxy.py +0 -0
  66. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/py.typed +0 -0
  67. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/reviews.py +0 -0
  68. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/scan.py +0 -0
  69. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/security_events.py +0 -0
  70. {getstack-0.13.0 → getstack-0.14.0}/src/getstack/skills.py +0 -0
@@ -0,0 +1,76 @@
1
+ # getstack (Python SDK)
2
+
3
+ Published to PyPI as `getstack`. Not a workspace member of the main monorepo
4
+ (Python has its own tooling via `pyproject.toml` + its own tests).
5
+
6
+ ## Purpose
7
+
8
+ - Python ergonomic equivalent of `@getstackrun/sdk`
9
+ - `with stack.passports.mission(...) as m:` context manager that handles
10
+ checkpoints + checkout automatically
11
+ - **Feature-parity with the JS SDK** (GET-244, both sync `Stack` and async
12
+ `AsyncStack`): agents, passports (+ delegate + revoke-agent/session/all +
13
+ list_active), services (+ connect + grant/revoke/get_permissions),
14
+ credentials, drop-offs (+ expire), skills, identity, audit, notifications
15
+ (+ update), security-events, proxy, scan, intents, missions,
16
+ evidence-packs, llm gateway, inbound-webhooks, partner, **team**,
17
+ **delivery_methods**, **detector_config**.
18
+ - **Offline / customer-managed crypto** (the JS headline features):
19
+ `verify_passport_offline` (`sdk-python/src/getstack/verify_offline.py` —
20
+ pyjwt/EdDSA, jwks_url OR public_jwk), `verify_passport_header`, and
21
+ `sign_claim_envelope` (`sdk-python/src/getstack/sign_claim.py` —
22
+ COSE_Sign1 for customer_managed agents; cbor2, and **byte-compatible with
23
+ the JS cbor-x / vault verifier** — proven by
24
+ `sdk-python/scripts/verify_sign_claim_crosslang.py`: Python signs, the TS
25
+ vault logic accepts, for all 5 claim types).
26
+
27
+ ## Parity discipline (HARD RULE — GET-244)
28
+
29
+ Every service + method must exist on BOTH the sync modules under
30
+ `sdk-python/src/getstack/` AND their async mirrors under
31
+ `sdk-python/src/getstack/_async/`. The async surface silently drifted
32
+ before (its `register` lacked profile/key_mode and always clobbered the
33
+ preset). `sdk-python/tests/test_parity.py` is the guard: it asserts `Stack`
34
+ and `AsyncStack` expose the same services + the integration-critical
35
+ methods. Add to one client, add to the other, and the parity test stays
36
+ green.
37
+
38
+ Wire-field note: the API's grant field is `service_connection_id` (not
39
+ `connection_id` — that was the JS SDK bug); services `list`/`disconnect`
40
+ hit `/v1/services/connected` and `/v1/services/{id}/disconnect`. Notification
41
+ *rules* are `/v1/notifications/channels` (Python's `notifications`);
42
+ *delivery methods* are `/v1/notifications/destinations` (`delivery_methods`).
43
+
44
+ ## Where to look
45
+
46
+ - Package metadata: `sdk-python/pyproject.toml`
47
+ - Module entry: `sdk-python/src/getstack/`
48
+ - User-facing quickstart + examples: `sdk-python/README.md`
49
+ - License: `sdk-python/LICENSE`
50
+
51
+ ## Key divergence from JS SDK
52
+
53
+ The Python SDK has a **mission context manager** the JS SDK doesn't have:
54
+
55
+ ```python
56
+ with stack.passports.mission(
57
+ agent_id=agent.id,
58
+ intent="Process invoices from Slack",
59
+ services=["slack", "stripe"],
60
+ checkpoint_interval="5m",
61
+ ) as mission:
62
+ mission.log("slack", "read_channel", "#invoices")
63
+ mission.log("stripe", "create_invoice")
64
+ # Checkpoints submitted automatically on schedule
65
+ # Checkout submitted automatically when the block exits
66
+ ```
67
+
68
+ When the JS SDK adds an equivalent, align the shape (same keyword args,
69
+ same method names) for cross-language familiarity.
70
+
71
+ ## Gotchas
72
+
73
+ - **Not installed by `npm install` at monorepo root.** Python devs run `pip install -e sdk-python/` or `uv pip install -e sdk-python/`. The JS monorepo lockfile does not resolve Python deps.
74
+ - **Published to PyPI separately.** There's no coordinated release between the JS + Python SDKs; they can (and sometimes do) drift in version numbers.
75
+ - **Same authentication posture.** API keys, session JWTs, and the same `sk_live_*` prefix convention. Token type detection works identically to JS.
76
+ - **Offline passport verification is also available** (mirrors `verify_passport_offline` from the JS SDK) — but confirm the exact function name in `sdk-python/src/getstack/` before citing.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.13.0
3
+ Version: 0.14.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
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Topic :: Software Development :: Libraries
21
21
  Requires-Python: >=3.9
22
+ Requires-Dist: cbor2>=5.6.0
22
23
  Requires-Dist: cryptography>=42.0.0
23
24
  Requires-Dist: httpx>=0.25.0
24
25
  Requires-Dist: pyjwt>=2.8.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "0.13.0"
7
+ version = "0.14.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -32,6 +32,11 @@ dependencies = [
32
32
  # PyJWT for agent JWT minting; pure-Python, tiny (~50KB), no native
33
33
  # deps beyond cryptography (which we already need).
34
34
  "pyjwt>=2.8.0",
35
+ # cbor2 for customer-managed COSE_Sign1 claim signing (sign_claim.py).
36
+ # Pure-Python CBOR; produces byte-compatible output with the JS SDK's
37
+ # cbor-x for the COSE sig structure (proven by the cross-language
38
+ # round-trip test).
39
+ "cbor2>=5.6.0",
35
40
  ]
36
41
 
37
42
  [project.urls]
@@ -0,0 +1,125 @@
1
+ """Cross-language COSE byte-compat proof for sign_claim_envelope.
2
+
3
+ Python signs claim envelopes; the JS SDK's cbor-x + node:crypto verify
4
+ logic (identical to @stack/vault AgentSigner.verifyPreSigned) must accept
5
+ them. This is the load-bearing guarantee that a customer_managed agent
6
+ signing claims in Python is accepted by STACK server-side.
7
+
8
+ Run from the repo root (needs the JS SDK's node_modules for cbor-x):
9
+ python sdk-python/scripts/verify_sign_claim_crosslang.py
10
+
11
+ Exit 0 = all claim types round-tripped; non-zero = a byte-compat mismatch.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import base64
17
+ import json
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ REPO = Path(__file__).resolve().parents[2]
23
+ sys.path.insert(0, str(REPO / "sdk-python" / "src"))
24
+
25
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey # noqa: E402
26
+ from cryptography.hazmat.primitives.serialization import ( # noqa: E402
27
+ Encoding,
28
+ NoEncryption,
29
+ PrivateFormat,
30
+ PublicFormat,
31
+ )
32
+
33
+ from getstack.sign_claim import sign_claim_envelope # noqa: E402
34
+
35
+ NODE_VERIFIER = r"""
36
+ import { readFileSync } from 'node:fs';
37
+ import { encode as cborEncode, decode as cborDecode } from 'cbor-x';
38
+ import { createPublicKey, verify as cryptoVerify } from 'node:crypto';
39
+ const rows = JSON.parse(readFileSync(process.argv[2], 'utf8'));
40
+ const getH = (h, k) => (h instanceof Map ? h.get(k) : h?.[k]);
41
+ let allPass = true;
42
+ for (const { cose, publicJwk, claim_type } of rows) {
43
+ const [protectedBytes, , payloadBytes, sig] = cborDecode(Buffer.from(cose, 'base64url'));
44
+ const pm = cborDecode(protectedBytes);
45
+ const kidRaw = getH(pm, 4);
46
+ const kid = kidRaw instanceof Uint8Array ? Buffer.from(kidRaw).toString('utf8') : kidRaw;
47
+ const ssb = cborEncode(['Signature1', protectedBytes, Buffer.alloc(0), payloadBytes]);
48
+ const valid = cryptoVerify(null, ssb, createPublicKey({ key: publicJwk, format: 'jwk' }), sig);
49
+ const env = cborDecode(payloadBytes);
50
+ const ok = valid && getH(pm, 1) === -8 && getH(pm, 16) === 'stack/claim+cose'
51
+ && kid === 'agent:ag_cust:v1' && env.claim_type === claim_type;
52
+ console.log((ok ? 'PASS' : 'FAIL') + ' ' + claim_type + ' sigValid=' + valid);
53
+ if (!ok) allPass = false;
54
+ }
55
+ process.exit(allPass ? 0 : 1);
56
+ """
57
+
58
+
59
+ def _b64url(b: bytes) -> str:
60
+ return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
61
+
62
+
63
+ def main() -> int:
64
+ priv = Ed25519PrivateKey.generate()
65
+ pub = priv.public_key()
66
+ x = _b64url(pub.public_bytes(Encoding.Raw, PublicFormat.Raw))
67
+ priv_jwk = {
68
+ "kty": "OKP",
69
+ "crv": "Ed25519",
70
+ "d": _b64url(priv.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())),
71
+ "x": x,
72
+ }
73
+ pub_jwk = {"kty": "OKP", "crv": "Ed25519", "x": x}
74
+
75
+ env = {
76
+ "envelope_version": 1,
77
+ "issuer": "op_cust",
78
+ "subject": "s",
79
+ "issued_at": 1778600000000,
80
+ "claim_type": "intent_claim",
81
+ "claim_version": 1,
82
+ "payload": {"intent_type": "http_call"},
83
+ "receipt": None,
84
+ "co_signatures": [],
85
+ }
86
+ rows = []
87
+ for ct in ["intent_claim", "intent_outcome", "detector_signal", "intent_denial", "intent_simulation"]:
88
+ env["claim_type"] = ct
89
+ rows.append(
90
+ {
91
+ "cose": sign_claim_envelope(env, private_key=priv_jwk, agent_id="ag_cust"),
92
+ "publicJwk": pub_jwk,
93
+ "claim_type": ct,
94
+ }
95
+ )
96
+
97
+ # Node resolves modules relative to the SCRIPT FILE, so the verifier
98
+ # must live inside packages/sdk/ for its cbor-x import to resolve. Write
99
+ # it there transiently and clean up.
100
+ sdk_dir = REPO / "packages" / "sdk"
101
+ verifier_path = sdk_dir / "_crosslang_verify.mjs"
102
+ data_path = sdk_dir / "_crosslang_rows.json"
103
+ try:
104
+ data_path.write_text(json.dumps(rows))
105
+ verifier_path.write_text(NODE_VERIFIER)
106
+ proc = subprocess.run(
107
+ ["node", str(verifier_path), str(data_path)],
108
+ cwd=str(sdk_dir),
109
+ capture_output=True,
110
+ text=True,
111
+ )
112
+ print(proc.stdout, end="")
113
+ if proc.returncode != 0:
114
+ print(proc.stderr, file=sys.stderr)
115
+ print("\n [FAIL] CROSS-LANGUAGE BYTE-COMPAT FAILED", file=sys.stderr)
116
+ return proc.returncode
117
+ finally:
118
+ verifier_path.unlink(missing_ok=True)
119
+ data_path.unlink(missing_ok=True)
120
+ print("\n [OK] Python-signed COSE verifies under the JS/vault logic for all claim types.")
121
+ return 0
122
+
123
+
124
+ if __name__ == "__main__":
125
+ raise SystemExit(main())
@@ -55,10 +55,15 @@ from .identity import IdentityService
55
55
  from .inbound_webhooks import InboundWebhookService
56
56
  from .partner_tenants import PartnerTenantsService
57
57
  from .passport_header_verify import verify_passport_header
58
+ from .verify_offline import verify_passport_offline
59
+ from .sign_claim import sign_claim_envelope
58
60
  from .intents import IntentsService
59
61
  from .missions import MissionsService
60
62
  from .evidence_packs import EvidencePacksService
61
63
  from .llm import LlmService
64
+ from .team import TeamService
65
+ from .delivery_methods import DeliveryMethodService
66
+ from .detector_config import DetectorConfigService
62
67
  from .passport_session import PassportSession
63
68
  from .types import (
64
69
  Agent,
@@ -160,6 +165,14 @@ __all__ = [
160
165
  "IdentityClaim",
161
166
  "IdentitySettings",
162
167
  "VerificationSession",
168
+ # Service classes (for typing / direct construction)
169
+ "TeamService",
170
+ "DeliveryMethodService",
171
+ "DetectorConfigService",
172
+ # Offline / customer-managed crypto (parity with the JS SDK)
173
+ "verify_passport_offline",
174
+ "verify_passport_header",
175
+ "sign_claim_envelope",
163
176
  # Errors
164
177
  "StackError",
165
178
  "NotFoundError",
@@ -229,6 +242,9 @@ class Stack:
229
242
  self.llm = LlmService(self._client)
230
243
  self.inbound_webhooks = InboundWebhookService(self._client)
231
244
  self.partner_tenants = PartnerTenantsService(self._client)
245
+ self.team = TeamService(self._client)
246
+ self.delivery_methods = DeliveryMethodService(self._client)
247
+ self.detector_config = DetectorConfigService(self._client)
232
248
 
233
249
  def issue_passport(
234
250
  self,
@@ -370,6 +386,10 @@ class AsyncStack:
370
386
  from ._async.llm import AsyncLlmService
371
387
  from ._async.inbound_webhooks import AsyncInboundWebhookService
372
388
  from ._async.partner_tenants import AsyncPartnerTenantsService
389
+ from ._async.evidence_packs import AsyncEvidencePacksService
390
+ from ._async.team import AsyncTeamService
391
+ from ._async.delivery_methods import AsyncDeliveryMethodService
392
+ from ._async.detector_config import AsyncDetectorConfigService
373
393
  from ._async.passport_session import AsyncPassportSession
374
394
 
375
395
  self.agents = AsyncAgentService(self._client)
@@ -390,6 +410,10 @@ class AsyncStack:
390
410
  self.llm = AsyncLlmService(self._client)
391
411
  self.inbound_webhooks = AsyncInboundWebhookService(self._client)
392
412
  self.partner_tenants = AsyncPartnerTenantsService(self._client)
413
+ self.evidence_packs = AsyncEvidencePacksService(self._client)
414
+ self.team = AsyncTeamService(self._client)
415
+ self.delivery_methods = AsyncDeliveryMethodService(self._client)
416
+ self.detector_config = AsyncDetectorConfigService(self._client)
393
417
  # Stash for issue_passport() — Python doesn't easily expose the
394
418
  # class binding without re-importing inside the method body.
395
419
  self._AsyncPassportSession = AsyncPassportSession
@@ -17,13 +17,27 @@ class AsyncAgentService:
17
17
  self,
18
18
  name: str,
19
19
  description: str | None = None,
20
- accountability_mode: str = "enforced",
20
+ profile: str | None = None,
21
+ accountability_mode: str | None = None,
22
+ credential_access: str | None = None,
23
+ key_mode: str | None = None,
21
24
  on_warning: str = "notify",
22
25
  on_critical: str = "notify",
23
26
  ) -> Agent:
24
- body: dict[str, Any] = {"name": name, "accountability_mode": accountability_mode}
27
+ """Register an agent. See the sync :meth:`AgentService.register`
28
+ docstring — pass ``profile="proxied-llm-bot"`` for a bot that just
29
+ runs. Unset fields are not sent, so they never clobber a preset."""
30
+ body: dict[str, Any] = {"name": name}
25
31
  if description:
26
32
  body["description"] = description
33
+ if profile is not None:
34
+ body["profile"] = profile
35
+ if accountability_mode is not None:
36
+ body["accountability_mode"] = accountability_mode
37
+ if credential_access is not None:
38
+ body["credential_access"] = credential_access
39
+ if key_mode is not None:
40
+ body["key_mode"] = key_mode
27
41
  if on_warning != "notify":
28
42
  body["on_warning"] = on_warning
29
43
  if on_critical != "notify":
@@ -0,0 +1,35 @@
1
+ """Notification delivery methods (async parity with the JS SDK's
2
+ DeliveryMethodService).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from ..client import AsyncHttpClient
10
+
11
+
12
+ class AsyncDeliveryMethodService:
13
+ def __init__(self, client: AsyncHttpClient):
14
+ self._client = client
15
+
16
+ async def add(self, kind: str, destination: str, **fields: Any) -> dict[str, Any]:
17
+ body: dict[str, Any] = {"kind": kind, "destination": destination, **fields}
18
+ return await self._client.post("/v1/notifications/destinations", json=body)
19
+
20
+ async def list(self) -> list[dict[str, Any]]:
21
+ return await self._client.get("/v1/notifications/destinations")
22
+
23
+ async def delete(self, destination_id: str) -> dict[str, Any]:
24
+ return await self._client.delete(f"/v1/notifications/destinations/{destination_id}")
25
+
26
+ async def send_verification_code(self, destination_id: str) -> dict[str, Any]:
27
+ return await self._client.post(f"/v1/notifications/destinations/{destination_id}/send-code")
28
+
29
+ async def verify(self, destination_id: str, code: str) -> dict[str, Any]:
30
+ return await self._client.post(
31
+ f"/v1/notifications/destinations/{destination_id}/verify", json={"code": code}
32
+ )
33
+
34
+ async def test(self, destination_id: str) -> dict[str, Any]:
35
+ return await self._client.post(f"/v1/notifications/destinations/{destination_id}/test")
@@ -0,0 +1,27 @@
1
+ """Per-operator detector customization (async parity with the JS SDK's
2
+ DetectorConfigService).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+ from urllib.parse import quote
9
+
10
+ from ..client import AsyncHttpClient
11
+
12
+
13
+ class AsyncDetectorConfigService:
14
+ def __init__(self, client: AsyncHttpClient):
15
+ self._client = client
16
+
17
+ async def list(self) -> dict[str, Any]:
18
+ return await self._client.get("/v1/detectors/config")
19
+
20
+ async def get(self, detector_key: str) -> dict[str, Any]:
21
+ return await self._client.get(f"/v1/detectors/{quote(detector_key)}/config")
22
+
23
+ async def upsert(self, detector_key: str, **config: Any) -> dict[str, Any]:
24
+ return await self._client.put(f"/v1/detectors/{quote(detector_key)}/config", json=config)
25
+
26
+ async def reset(self, detector_key: str) -> None:
27
+ await self._client.delete(f"/v1/detectors/{quote(detector_key)}/config")
@@ -42,6 +42,9 @@ class AsyncDropoffService:
42
42
  json={"agent_id": agent_id},
43
43
  )
44
44
 
45
+ async def expire(self, dropoff_id: str) -> dict[str, Any]:
46
+ return await self._client.post(f"/v1/dropoffs/{dropoff_id}/expire")
47
+
45
48
  async def get(self, dropoff_id: str) -> dict[str, Any]:
46
49
  return await self._client.get(f"/v1/dropoffs/{dropoff_id}")
47
50
 
@@ -0,0 +1,44 @@
1
+ """Evidence packs — export, get, list (async mirror of EvidencePacksService)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from ..client import AsyncHttpClient
9
+
10
+
11
+ class AsyncEvidencePacksService:
12
+ def __init__(self, client: AsyncHttpClient):
13
+ self._client = client
14
+
15
+ async def export(
16
+ self,
17
+ *,
18
+ scope_kind: str,
19
+ scope_ref: str,
20
+ supersedes: str | None = None,
21
+ air_gap: bool | None = None,
22
+ output_path: str | None = None,
23
+ ) -> dict[str, Any]:
24
+ body: dict[str, Any] = {"scope": {"kind": scope_kind, "ref": scope_ref}}
25
+ if supersedes is not None:
26
+ body["supersedes"] = supersedes
27
+ if air_gap is not None:
28
+ body["air_gap"] = air_gap
29
+ result = await self._client.request("POST", "/v1/evidence-packs", json=body)
30
+ if output_path is not None and "manifest" in result:
31
+ with open(output_path, "w", encoding="utf-8") as f:
32
+ json.dump(result["manifest"], f, indent=2)
33
+ return result
34
+
35
+ async def get(self, pack_id: str) -> dict[str, Any]:
36
+ return await self._client.request("GET", f"/v1/evidence-packs/{pack_id}")
37
+
38
+ async def list(self, *, scope: str | None = None) -> dict[str, Any]:
39
+ path = "/v1/evidence-packs"
40
+ if scope is not None:
41
+ from urllib.parse import urlencode
42
+
43
+ path = f"{path}?{urlencode({'scope': scope})}"
44
+ return await self._client.request("GET", path)
@@ -45,5 +45,8 @@ class AsyncNotificationService:
45
45
  async def test(self, channel_id: str) -> dict[str, Any]:
46
46
  return await self._client.post(f"/v1/notifications/channels/{channel_id}/test")
47
47
 
48
+ async def update(self, channel_id: str, **fields: Any) -> dict[str, Any]:
49
+ return await self._client.patch(f"/v1/notifications/channels/{channel_id}", json=fields)
50
+
48
51
  async def delete(self, channel_id: str) -> dict[str, Any]:
49
52
  return await self._client.delete(f"/v1/notifications/channels/{channel_id}")
@@ -294,6 +294,41 @@ class AsyncPassportService:
294
294
  async def revoke(self, jti: str, reason: str | None = None) -> dict[str, Any]:
295
295
  return await self._client.post("/v1/passports/revoke", json={"jti": jti, "reason": reason})
296
296
 
297
+ async def revoke_agent(self, agent_id: str, reason: str | None = None) -> dict[str, Any]:
298
+ return await self._client.post(f"/v1/passports/revoke-agent/{agent_id}", json={"reason": reason})
299
+
300
+ async def revoke_session(self, session_id: str, reason: str | None = None) -> dict[str, Any]:
301
+ return await self._client.post(
302
+ f"/v1/passports/revoke-session/{session_id}", json={"reason": reason}
303
+ )
304
+
305
+ async def revoke_all(self, reason: str | None = None) -> dict[str, Any]:
306
+ return await self._client.post("/v1/passports/revoke-all", json={"reason": reason})
307
+
308
+ async def delegate(
309
+ self,
310
+ parent_token: str,
311
+ child_agent_id: str,
312
+ scopes: list[str] | None = None,
313
+ ttl_seconds: int | None = None,
314
+ ) -> Passport:
315
+ body: dict[str, Any] = {"parent_token": parent_token, "child_agent_id": child_agent_id}
316
+ if scopes is not None:
317
+ body["scopes"] = scopes
318
+ if ttl_seconds is not None:
319
+ body["ttl_seconds"] = ttl_seconds
320
+ return _to_passport(await self._client.post("/v1/passports/delegate", json=body))
321
+
322
+ async def list_active(
323
+ self, agent_id: str | None = None, session_id: str | None = None
324
+ ) -> list[dict[str, Any]]:
325
+ params: dict[str, Any] = {}
326
+ if agent_id is not None:
327
+ params["agent_id"] = agent_id
328
+ if session_id is not None:
329
+ params["session_id"] = session_id
330
+ return await self._client.get("/v1/passports/active", params=params or None)
331
+
297
332
  async def refresh(self, token: str, ttl_seconds: int | None = None) -> Passport:
298
333
  body: dict[str, Any] = {"token": token}
299
334
  if ttl_seconds:
@@ -0,0 +1,81 @@
1
+ """Async mirror of ServiceService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncServiceService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def list(self) -> list[dict[str, Any]]:
15
+ # /v1/services/connected (was wrongly /v1/services/connections → 404).
16
+ return await self._client.get("/v1/services/connected")
17
+
18
+ async def list_available(self) -> list[dict[str, Any]]:
19
+ return await self._client.get("/v1/services")
20
+
21
+ async def list_templates(self) -> list[dict[str, Any]]:
22
+ return await self._client.get("/v1/services/templates")
23
+
24
+ async def connect(
25
+ self,
26
+ service_id: str,
27
+ provider: str,
28
+ credential: str,
29
+ scopes: list[str] | None = None,
30
+ ) -> dict[str, Any]:
31
+ """Connect a catalog (OAuth-family) service by supplying the
32
+ credential directly. For services not in the catalog, use
33
+ :meth:`connect_custom`."""
34
+ body: dict[str, Any] = {
35
+ "service_id": service_id,
36
+ "provider": provider,
37
+ "credential": credential,
38
+ }
39
+ if scopes is not None:
40
+ body["scopes"] = scopes
41
+ return await self._client.post("/v1/services/connect", json=body)
42
+
43
+ async def connect_custom(
44
+ self,
45
+ name: str,
46
+ credential: str | dict[str, str],
47
+ description: str | None = None,
48
+ scopes: list[str] | None = None,
49
+ ) -> dict[str, Any]:
50
+ body: dict[str, Any] = {"name": name, "credential": credential}
51
+ if description:
52
+ body["description"] = description
53
+ if scopes:
54
+ body["scopes"] = scopes
55
+ return await self._client.post("/v1/services/custom", json=body)
56
+
57
+ async def verify(self, connection_id: str) -> dict[str, Any]:
58
+ return await self._client.post(f"/v1/services/{connection_id}/verify")
59
+
60
+ async def disconnect(self, connection_id: str) -> dict[str, Any]:
61
+ return await self._client.delete(f"/v1/services/{connection_id}/disconnect")
62
+
63
+ async def grant_agent_access(
64
+ self,
65
+ agent_id: str,
66
+ service_connection_id: str,
67
+ scopes: list[str] | None = None,
68
+ ) -> dict[str, Any]:
69
+ body: dict[str, Any] = {
70
+ "agent_id": agent_id,
71
+ "service_connection_id": service_connection_id,
72
+ }
73
+ if scopes is not None:
74
+ body["scopes"] = scopes
75
+ return await self._client.post("/v1/services/grant", json=body)
76
+
77
+ async def revoke_agent_access(self, agent_id: str) -> dict[str, Any]:
78
+ return await self._client.delete(f"/v1/services/agents/{agent_id}/revoke")
79
+
80
+ async def get_agent_permissions(self, agent_id: str) -> dict[str, Any]:
81
+ return await self._client.get(f"/v1/agents/{agent_id}/permissions")
@@ -0,0 +1,37 @@
1
+ """Team member management (async parity with the JS SDK's TeamService)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncTeamService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def invite(
15
+ self,
16
+ email: str,
17
+ role: str | None = None,
18
+ allowed_connections: list[str] | None = None,
19
+ ) -> dict[str, Any]:
20
+ body: dict[str, Any] = {"email": email}
21
+ if role is not None:
22
+ body["role"] = role
23
+ if allowed_connections is not None:
24
+ body["allowed_connections"] = allowed_connections
25
+ return await self._client.post("/v1/team/members", json=body)
26
+
27
+ async def list(self) -> list[dict[str, Any]]:
28
+ return await self._client.get("/v1/team/members")
29
+
30
+ async def update(self, member_id: str, **fields: Any) -> dict[str, Any]:
31
+ return await self._client.patch(f"/v1/team/members/{member_id}", json=fields)
32
+
33
+ async def revoke(self, member_id: str) -> dict[str, Any]:
34
+ return await self._client.delete(f"/v1/team/members/{member_id}")
35
+
36
+ async def accept_invite(self, member_id: str) -> dict[str, Any]:
37
+ return await self._client.post(f"/v1/team/members/{member_id}/accept")
@@ -27,6 +27,7 @@ def _to_agent(data: dict[str, Any]) -> Agent:
27
27
  created_at=data["created_at"],
28
28
  updated_at=data["updated_at"],
29
29
  credential_access=data.get("credential_access", "direct"),
30
+ key_mode=data.get("key_mode", "stack_managed"),
30
31
  )
31
32
 
32
33
 
@@ -41,6 +42,7 @@ class AgentService:
41
42
  profile: str | None = None,
42
43
  accountability_mode: str | None = None,
43
44
  credential_access: str | None = None,
45
+ key_mode: str | None = None,
44
46
  on_warning: str = "notify",
45
47
  on_critical: str = "notify",
46
48
  ) -> Agent:
@@ -67,6 +69,8 @@ class AgentService:
67
69
  body["accountability_mode"] = accountability_mode
68
70
  if credential_access is not None:
69
71
  body["credential_access"] = credential_access
72
+ if key_mode is not None:
73
+ body["key_mode"] = key_mode
70
74
  if on_warning != "notify":
71
75
  body["on_warning"] = on_warning
72
76
  if on_critical != "notify":