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.
- getstack-0.14.0/CLAUDE.md +76 -0
- {getstack-0.13.0 → getstack-0.14.0}/PKG-INFO +2 -1
- {getstack-0.13.0 → getstack-0.14.0}/pyproject.toml +6 -1
- getstack-0.14.0/scripts/verify_sign_claim_crosslang.py +125 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/__init__.py +24 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/agents.py +16 -2
- getstack-0.14.0/src/getstack/_async/delivery_methods.py +35 -0
- getstack-0.14.0/src/getstack/_async/detector_config.py +27 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/dropoffs.py +3 -0
- getstack-0.14.0/src/getstack/_async/evidence_packs.py +44 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/notifications.py +3 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/passports.py +35 -0
- getstack-0.14.0/src/getstack/_async/services.py +81 -0
- getstack-0.14.0/src/getstack/_async/team.py +37 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/agents.py +4 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/client.py +11 -0
- getstack-0.14.0/src/getstack/delivery_methods.py +35 -0
- getstack-0.14.0/src/getstack/detector_config.py +39 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/dropoffs.py +4 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/notifications.py +4 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/passports.py +38 -0
- getstack-0.14.0/src/getstack/services.py +96 -0
- getstack-0.14.0/src/getstack/sign_claim.py +96 -0
- getstack-0.14.0/src/getstack/team.py +37 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/types.py +5 -0
- getstack-0.14.0/src/getstack/verify_offline.py +82 -0
- getstack-0.14.0/tests/test_parity.py +109 -0
- getstack-0.14.0/tests/test_sign_claim.py +147 -0
- getstack-0.13.0/CLAUDE.md +0 -46
- getstack-0.13.0/src/getstack/_async/services.py +0 -41
- getstack-0.13.0/src/getstack/services.py +0 -41
- {getstack-0.13.0 → getstack-0.14.0}/.gitignore +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/LICENSE +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/README.md +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/__init__.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/audit.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/credentials.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/identity.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/inbound_webhooks.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/intents.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/llm.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/missions.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/partner_tenants.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/passport_session.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/proxy.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/reviews.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/scan.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/security_events.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/_async/skills.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/agent_auth.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/audit.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/auth.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/credentials.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/errors.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/evidence_packs.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/identity.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/inbound_webhooks.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/intents.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/llm.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/missions.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/partner_tenants.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/passport_header_verify.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/passport_session.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/proxy.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/py.typed +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/reviews.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/scan.py +0 -0
- {getstack-0.13.0 → getstack-0.14.0}/src/getstack/security_events.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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":
|