getstack 0.10.0__tar.gz → 0.12.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.10.0 → getstack-0.12.0}/.gitignore +8 -4
- {getstack-0.10.0 → getstack-0.12.0}/PKG-INFO +1 -1
- {getstack-0.10.0 → getstack-0.12.0}/pyproject.toml +1 -1
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/__init__.py +2 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/identity.py +3 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/partner_tenants.py +39 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/identity.py +3 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/partner_tenants.py +46 -0
- getstack-0.12.0/src/getstack/passport_header_verify.py +109 -0
- {getstack-0.10.0 → getstack-0.12.0}/CLAUDE.md +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/LICENSE +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/README.md +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/__init__.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/agents.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/audit.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/credentials.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/dropoffs.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/inbound_webhooks.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/intents.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/llm.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/missions.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/notifications.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/passport_session.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/passports.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/proxy.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/reviews.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/scan.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/security_events.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/services.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/_async/skills.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/agent_auth.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/agents.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/audit.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/auth.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/client.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/credentials.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/dropoffs.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/errors.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/evidence_packs.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/inbound_webhooks.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/intents.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/llm.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/missions.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/notifications.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/passport_session.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/passports.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/proxy.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/py.typed +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/reviews.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/scan.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/security_events.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/services.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/skills.py +0 -0
- {getstack-0.10.0 → getstack-0.12.0}/src/getstack/types.py +0 -0
|
@@ -18,10 +18,8 @@ scripts/.env.cto
|
|
|
18
18
|
.npmrc.local
|
|
19
19
|
~/.npmrc
|
|
20
20
|
|
|
21
|
-
# Strategy docs
|
|
22
|
-
|
|
23
|
-
stack-gtm-distribution.md
|
|
24
|
-
stack-platform-spec-v2.md
|
|
21
|
+
# Strategy docs: now tracked in this private repo so they survive a laptop
|
|
22
|
+
# wipe (were gitignored + local-only, a backup gap). Not sensitive.
|
|
25
23
|
|
|
26
24
|
# IDE
|
|
27
25
|
.vscode/settings.json
|
|
@@ -42,6 +40,7 @@ mcp-publisher.exe
|
|
|
42
40
|
# Fly.io deployment config (contains app names, regions)
|
|
43
41
|
**/fly.toml
|
|
44
42
|
!apps/*/fly.toml
|
|
43
|
+
!infra/**/fly.toml
|
|
45
44
|
!**/fly.toml.example
|
|
46
45
|
|
|
47
46
|
# Logs
|
|
@@ -54,6 +53,11 @@ npm-debug.log*
|
|
|
54
53
|
# Claude Code per-machine state (background scheduler)
|
|
55
54
|
.claude/scheduled_tasks.lock
|
|
56
55
|
|
|
56
|
+
# spec-judge per-session state (active plan pointer + judge verdict; never committed)
|
|
57
|
+
.claude/active-plan.json
|
|
58
|
+
.claude/judge-verdict.json
|
|
59
|
+
.claude/.verify*
|
|
60
|
+
|
|
57
61
|
# OpenAPI spec cache (regenerated by scripts/verify-intents-openapi.ts)
|
|
58
62
|
scripts/.openapi-cache/
|
|
59
63
|
|
|
@@ -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
|
|
@@ -90,3 +90,6 @@ class AsyncIdentityService:
|
|
|
90
90
|
|
|
91
91
|
async def set_service_requirement(self, service_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
92
92
|
return await self._client.post(f"/v1/services/{service_id}/requirements", json=kwargs)
|
|
93
|
+
|
|
94
|
+
async def delete_service_requirement(self, service_id: str, requirement_id: str) -> None:
|
|
95
|
+
await self._client.delete(f"/v1/services/{service_id}/requirements/{requirement_id}")
|
|
@@ -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
|
+
}
|
|
@@ -117,3 +117,6 @@ class IdentityService:
|
|
|
117
117
|
|
|
118
118
|
def set_service_requirement(self, service_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
119
119
|
return self._client.post(f"/v1/services/{service_id}/requirements", json=kwargs)
|
|
120
|
+
|
|
121
|
+
def delete_service_requirement(self, service_id: str, requirement_id: str) -> None:
|
|
122
|
+
self._client.delete(f"/v1/services/{service_id}/requirements/{requirement_id}")
|
|
@@ -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
|
|
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
|