getstack 0.10.0__tar.gz → 0.11.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 (55) hide show
  1. {getstack-0.10.0 → getstack-0.11.0}/PKG-INFO +1 -1
  2. {getstack-0.10.0 → getstack-0.11.0}/pyproject.toml +1 -1
  3. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/__init__.py +2 -0
  4. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/partner_tenants.py +39 -0
  5. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/partner_tenants.py +46 -0
  6. getstack-0.11.0/src/getstack/passport_header_verify.py +109 -0
  7. {getstack-0.10.0 → getstack-0.11.0}/.gitignore +0 -0
  8. {getstack-0.10.0 → getstack-0.11.0}/CLAUDE.md +0 -0
  9. {getstack-0.10.0 → getstack-0.11.0}/LICENSE +0 -0
  10. {getstack-0.10.0 → getstack-0.11.0}/README.md +0 -0
  11. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/__init__.py +0 -0
  12. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/agents.py +0 -0
  13. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/audit.py +0 -0
  14. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/credentials.py +0 -0
  15. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/dropoffs.py +0 -0
  16. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/identity.py +0 -0
  17. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/inbound_webhooks.py +0 -0
  18. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/intents.py +0 -0
  19. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/llm.py +0 -0
  20. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/missions.py +0 -0
  21. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/notifications.py +0 -0
  22. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/passport_session.py +0 -0
  23. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/passports.py +0 -0
  24. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/proxy.py +0 -0
  25. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/reviews.py +0 -0
  26. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/scan.py +0 -0
  27. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/security_events.py +0 -0
  28. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/services.py +0 -0
  29. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/_async/skills.py +0 -0
  30. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/agent_auth.py +0 -0
  31. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/agents.py +0 -0
  32. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/audit.py +0 -0
  33. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/auth.py +0 -0
  34. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/browser_bootstrap.py +0 -0
  35. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/client.py +0 -0
  36. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/credentials.py +0 -0
  37. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/dropoffs.py +0 -0
  38. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/errors.py +0 -0
  39. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/evidence_packs.py +0 -0
  40. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/identity.py +0 -0
  41. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/inbound_webhooks.py +0 -0
  42. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/intents.py +0 -0
  43. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/llm.py +0 -0
  44. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/missions.py +0 -0
  45. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/notifications.py +0 -0
  46. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/passport_session.py +0 -0
  47. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/passports.py +0 -0
  48. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/proxy.py +0 -0
  49. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/py.typed +0 -0
  50. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/reviews.py +0 -0
  51. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/scan.py +0 -0
  52. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/security_events.py +0 -0
  53. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/services.py +0 -0
  54. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/skills.py +0 -0
  55. {getstack-0.10.0 → getstack-0.11.0}/src/getstack/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: Python SDK for STACK — trust infrastructure for AI agents
5
5
  Project-URL: Homepage, https://getstack.run
6
6
  Project-URL: Documentation, https://getstack.run/docs/sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "0.10.0"
7
+ version = "0.11.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -53,6 +53,8 @@ from .security_events import SecurityEventService as SecurityEventSvc
53
53
  from .skills import SkillService
54
54
  from .identity import IdentityService
55
55
  from .inbound_webhooks import InboundWebhookService
56
+ from .partner_tenants import PartnerTenantsService
57
+ from .passport_header_verify import verify_passport_header
56
58
  from .intents import IntentsService
57
59
  from .missions import MissionsService
58
60
  from .evidence_packs import EvidencePacksService
@@ -76,3 +76,42 @@ class AsyncPartnerTenantsService:
76
76
  f"/v1/partner/tenants/{tenant_id}/passports/issue",
77
77
  json=body,
78
78
  )
79
+
80
+ async def mint_outbound_passport(
81
+ self,
82
+ tenant_id: str,
83
+ agent_id: str,
84
+ scopes: Optional[list[str]] = None,
85
+ ttl_seconds: int = 900,
86
+ accountability_mode: Literal["enforced", "logged", "standard"] = "enforced",
87
+ named_intents: Optional[list[str]] = None,
88
+ ) -> dict[str, Any]:
89
+ """Phase 5.7 — convenience helper for the MIME-stamping pattern.
90
+
91
+ Async mirror of PartnerTenantsService.mint_outbound_passport.
92
+ """
93
+ services = [{"scopes": scopes}] if scopes else None
94
+ issued = await self.issue_passport(
95
+ tenant_id,
96
+ agent_id,
97
+ ttl_seconds=ttl_seconds,
98
+ accountability_mode=accountability_mode,
99
+ services=services,
100
+ named_intents=named_intents,
101
+ )
102
+ token = issued.get("token") or issued.get("passport")
103
+ passport_id = issued.get("passport_id") or issued.get("jti")
104
+ if not token or not passport_id:
105
+ raise RuntimeError("mint_outbound_passport: API response missing token or passport_id")
106
+ expires_at = issued.get("expires_at")
107
+ if not expires_at:
108
+ exp = issued.get("exp")
109
+ if isinstance(exp, (int, float)):
110
+ from datetime import datetime, timezone
111
+ expires_at = datetime.fromtimestamp(exp, tz=timezone.utc).isoformat()
112
+ return {
113
+ "token": token,
114
+ "passport_id": passport_id,
115
+ "expires_at": expires_at,
116
+ "header_value": token,
117
+ }
@@ -81,3 +81,49 @@ class PartnerTenantsService:
81
81
  f"/v1/partner/tenants/{tenant_id}/passports/issue",
82
82
  json=body,
83
83
  )
84
+
85
+ def mint_outbound_passport(
86
+ self,
87
+ tenant_id: str,
88
+ agent_id: str,
89
+ scopes: Optional[list[str]] = None,
90
+ ttl_seconds: int = 900,
91
+ accountability_mode: Literal["enforced", "logged", "standard"] = "enforced",
92
+ named_intents: Optional[list[str]] = None,
93
+ ) -> dict[str, Any]:
94
+ """Phase 5.7 — convenience helper for the MIME-stamping pattern.
95
+
96
+ Mints a passport under the tenant's per-operator signing key
97
+ with a default-narrow scope, returning the token along with
98
+ the value ready for direct header injection (`X-Stack-Passport:
99
+ <token>`). The receiver verifies offline via verify_passport_header.
100
+
101
+ Defaults: 15-minute TTL, accountability_mode='enforced'.
102
+
103
+ Returns dict with keys: token, passport_id, expires_at, header_value.
104
+ """
105
+ services = [{"scopes": scopes}] if scopes else None
106
+ issued = self.issue_passport(
107
+ tenant_id,
108
+ agent_id,
109
+ ttl_seconds=ttl_seconds,
110
+ accountability_mode=accountability_mode,
111
+ services=services,
112
+ named_intents=named_intents,
113
+ )
114
+ token = issued.get("token") or issued.get("passport")
115
+ passport_id = issued.get("passport_id") or issued.get("jti")
116
+ if not token or not passport_id:
117
+ raise RuntimeError("mint_outbound_passport: API response missing token or passport_id")
118
+ expires_at = issued.get("expires_at")
119
+ if not expires_at:
120
+ exp = issued.get("exp")
121
+ if isinstance(exp, (int, float)):
122
+ from datetime import datetime, timezone
123
+ expires_at = datetime.fromtimestamp(exp, tz=timezone.utc).isoformat()
124
+ return {
125
+ "token": token,
126
+ "passport_id": passport_id,
127
+ "expires_at": expires_at,
128
+ "header_value": token,
129
+ }
@@ -0,0 +1,109 @@
1
+ """Partner-side helper for the X-Stack-Passport convention (Phase 5.7).
2
+
3
+ Pulls the passport token from request headers (or accepts the raw
4
+ string), looks up the issuing tenant's JWKS, and verifies offline.
5
+ No network at verify time after first JWKS fetch per tenant.
6
+
7
+ Usage:
8
+
9
+ from getstack import verify_passport_header
10
+
11
+ claims = verify_passport_header(
12
+ request.headers.get('x-stack-passport'),
13
+ base_url='https://api.getstack.run',
14
+ )
15
+ tenant_id = claims['stk']['operator_id']
16
+ agent_id = claims['stk']['agent_id']
17
+
18
+ Does NOT check revocation. For revocation-aware verification, layer
19
+ a short-window cache on POST /v1/passports/verify.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, Optional, Union
25
+
26
+ import jwt
27
+ from jwt import PyJWKClient
28
+
29
+
30
+ DEFAULT_BASE_URL = "https://api.getstack.run"
31
+ DEFAULT_ISSUER = "getstack.run"
32
+ DEFAULT_AUDIENCE = "stack:passport"
33
+
34
+ # Cache PyJWKClient per JWKS URL so repeat verifies share the key cache.
35
+ _jwks_client_cache: dict[str, PyJWKClient] = {}
36
+
37
+
38
+ def _get_jwks_client(jwks_url: str) -> PyJWKClient:
39
+ client = _jwks_client_cache.get(jwks_url)
40
+ if client is None:
41
+ client = PyJWKClient(jwks_url)
42
+ _jwks_client_cache[jwks_url] = client
43
+ return client
44
+
45
+
46
+ def verify_passport_header(
47
+ header_value: Optional[Union[str, list[str]]],
48
+ *,
49
+ base_url: str = DEFAULT_BASE_URL,
50
+ issuer: str = DEFAULT_ISSUER,
51
+ audience: str = DEFAULT_AUDIENCE,
52
+ ) -> dict[str, Any]:
53
+ """Verify an X-Stack-Passport header value.
54
+
55
+ The header value is a STACK passport JWT signed under the issuing
56
+ tenant's per-operator signing key. This function decodes the JWT
57
+ (unverified) to read the sub claim (= tenant operator_id), resolves
58
+ the per-tenant JWKS URL, and verifies signature + issuer +
59
+ audience + expiry.
60
+
61
+ Raises ValueError on any failure — missing header, malformed JWT,
62
+ missing sub, signature failure, wrong issuer/audience, expired.
63
+ The caller should treat any raise as a 401 reject.
64
+
65
+ Returns the decoded claims dict on success.
66
+ """
67
+ if header_value is None:
68
+ raise ValueError("verify_passport_header: missing header value")
69
+ if isinstance(header_value, list):
70
+ if not header_value:
71
+ raise ValueError("verify_passport_header: header value is empty list")
72
+ token = header_value[0]
73
+ else:
74
+ token = header_value
75
+ if not isinstance(token, str) or not token:
76
+ raise ValueError("verify_passport_header: header value is empty")
77
+
78
+ # Decode unverified to read the tenant operator_id. The signature
79
+ # check that follows binds the sub to the JWKS so an attacker can't
80
+ # substitute a different tenant's sub.
81
+ try:
82
+ unverified = jwt.decode(token, options={"verify_signature": False})
83
+ except jwt.PyJWTError as err:
84
+ raise ValueError(f"verify_passport_header: malformed JWT ({err})") from err
85
+
86
+ tenant_id = unverified.get("sub")
87
+ if not tenant_id or not isinstance(tenant_id, str):
88
+ raise ValueError("verify_passport_header: token missing sub claim")
89
+
90
+ jwks_url = f"{base_url.rstrip('/')}/v1/operators/{tenant_id}/jwks.json"
91
+ jwk_client = _get_jwks_client(jwks_url)
92
+
93
+ try:
94
+ signing_key = jwk_client.get_signing_key_from_jwt(token)
95
+ except jwt.PyJWKClientError as err:
96
+ raise ValueError(f"verify_passport_header: jwks lookup failed ({err})") from err
97
+
98
+ try:
99
+ verified = jwt.decode(
100
+ token,
101
+ signing_key.key,
102
+ algorithms=["EdDSA"],
103
+ issuer=issuer,
104
+ audience=audience,
105
+ )
106
+ except jwt.InvalidTokenError as err:
107
+ raise ValueError(f"verify_passport_header: signature/claims invalid ({err})") from err
108
+
109
+ return verified
File without changes
File without changes
File without changes
File without changes
File without changes