getstack 1.4.0__tar.gz → 2.0.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 (90) hide show
  1. {getstack-1.4.0 → getstack-2.0.0}/PKG-INFO +1 -1
  2. {getstack-1.4.0 → getstack-2.0.0}/pyproject.toml +1 -1
  3. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/partner_tenants.py +17 -6
  4. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/partner_connect.py +3 -2
  5. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/partner_tenants.py +33 -9
  6. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/proxy.py +3 -1
  7. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/scan.py +3 -1
  8. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/usage.py +4 -0
  9. getstack-2.0.0/tests/test_partner_signing.py +69 -0
  10. getstack-1.4.0/tests/test_partner_signing.py +0 -36
  11. {getstack-1.4.0 → getstack-2.0.0}/.gitignore +0 -0
  12. {getstack-1.4.0 → getstack-2.0.0}/CLAUDE.md +0 -0
  13. {getstack-1.4.0 → getstack-2.0.0}/LICENSE +0 -0
  14. {getstack-1.4.0 → getstack-2.0.0}/README.md +0 -0
  15. {getstack-1.4.0 → getstack-2.0.0}/scripts/verify_sign_claim_crosslang.py +0 -0
  16. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/__init__.py +0 -0
  17. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/__init__.py +0 -0
  18. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/agents.py +0 -0
  19. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/audit.py +0 -0
  20. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/authority.py +0 -0
  21. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/credentials.py +0 -0
  22. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/delivery_methods.py +0 -0
  23. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/detector_config.py +0 -0
  24. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/dropoffs.py +0 -0
  25. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/evidence_packs.py +0 -0
  26. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/fleets.py +0 -0
  27. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/identity.py +0 -0
  28. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/inbound_webhooks.py +0 -0
  29. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/intents.py +0 -0
  30. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/llm.py +0 -0
  31. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/missions.py +0 -0
  32. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/notifications.py +0 -0
  33. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/partner_connect.py +0 -0
  34. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/passport_session.py +0 -0
  35. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/passports.py +0 -0
  36. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/pii_config.py +0 -0
  37. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/proxy.py +0 -0
  38. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/reviews.py +0 -0
  39. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/scan.py +0 -0
  40. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/security_events.py +0 -0
  41. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/services.py +0 -0
  42. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/skills.py +0 -0
  43. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/team.py +0 -0
  44. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_async/usage.py +0 -0
  45. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_authority_request.py +0 -0
  46. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/_fleet_key.py +0 -0
  47. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/agent_auth.py +0 -0
  48. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/agents.py +0 -0
  49. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/audit.py +0 -0
  50. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/auth.py +0 -0
  51. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/authority.py +0 -0
  52. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/browser_bootstrap.py +0 -0
  53. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/client.py +0 -0
  54. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/credentials.py +0 -0
  55. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/delivery_methods.py +0 -0
  56. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/detector_config.py +0 -0
  57. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/dropoffs.py +0 -0
  58. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/errors.py +0 -0
  59. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/evidence_packs.py +0 -0
  60. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/fleets.py +0 -0
  61. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/identity.py +0 -0
  62. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/inbound_webhooks.py +0 -0
  63. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/intents.py +0 -0
  64. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/llm.py +0 -0
  65. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/missions.py +0 -0
  66. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/notifications.py +0 -0
  67. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/passport_header_verify.py +0 -0
  68. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/passport_session.py +0 -0
  69. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/passports.py +0 -0
  70. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/pii_config.py +0 -0
  71. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/py.typed +0 -0
  72. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/reviews.py +0 -0
  73. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/security_events.py +0 -0
  74. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/services.py +0 -0
  75. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/sign_claim.py +0 -0
  76. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/skills.py +0 -0
  77. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/team.py +0 -0
  78. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/types.py +0 -0
  79. {getstack-1.4.0 → getstack-2.0.0}/src/getstack/verify_offline.py +0 -0
  80. {getstack-1.4.0 → getstack-2.0.0}/tests/test_authority.py +0 -0
  81. {getstack-1.4.0 → getstack-2.0.0}/tests/test_delivery_methods.py +0 -0
  82. {getstack-1.4.0 → getstack-2.0.0}/tests/test_dropoffs.py +0 -0
  83. {getstack-1.4.0 → getstack-2.0.0}/tests/test_errors.py +0 -0
  84. {getstack-1.4.0 → getstack-2.0.0}/tests/test_evidence_packs.py +0 -0
  85. {getstack-1.4.0 → getstack-2.0.0}/tests/test_fleets.py +0 -0
  86. {getstack-1.4.0 → getstack-2.0.0}/tests/test_identity_requirements.py +0 -0
  87. {getstack-1.4.0 → getstack-2.0.0}/tests/test_inbound_webhooks.py +0 -0
  88. {getstack-1.4.0 → getstack-2.0.0}/tests/test_missions.py +0 -0
  89. {getstack-1.4.0 → getstack-2.0.0}/tests/test_parity.py +0 -0
  90. {getstack-1.4.0 → getstack-2.0.0}/tests/test_sign_claim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: getstack
3
- Version: 1.4.0
3
+ Version: 2.0.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 = "1.4.0"
7
+ version = "2.0.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,4 @@
1
- """Async mirror of PartnerTenantsService (Phase 5.6)."""
1
+ """Async mirror of PartnerTenantsService (white-label tenants)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -8,8 +8,6 @@ from urllib.parse import quote
8
8
  from ..client import AsyncHttpClient
9
9
 
10
10
 
11
- Tier = Literal["free", "developer", "pro", "business", "enterprise"]
12
-
13
11
 
14
12
  class AsyncPartnerTenantsService:
15
13
  def __init__(self, client: AsyncHttpClient):
@@ -18,23 +16,36 @@ class AsyncPartnerTenantsService:
18
16
  async def create(
19
17
  self,
20
18
  external_customer_id: Optional[str] = None,
21
- tier: Optional[Tier] = None,
22
19
  ) -> dict[str, Any]:
20
+ """Create a white-label tenant.
21
+
22
+ Tenants have no plan of their own; they draw from your allowance
23
+ and wallet. Returns operator_id, api_key (shown once — store it),
24
+ external_customer_id, tier ('partner'), billing ('partner') and
25
+ created_at. Raises with code TENANT_LIMIT_REACHED (409) when your
26
+ deal caps the tenant count.
27
+ """
23
28
  body: dict[str, Any] = {}
24
29
  if external_customer_id is not None:
25
30
  body["external_customer_id"] = external_customer_id
26
- if tier is not None:
27
- body["tier"] = tier
28
31
  return await self._client.post("/v1/partner/tenants", json=body)
29
32
 
30
33
  async def list(self) -> list[dict[str, Any]]:
34
+ """List your tenants. Each carries ``usage`` ({period, actions}):
35
+ its month-to-date share of your action pool, when available."""
31
36
  res = await self._client.get("/v1/partner/tenants")
32
37
  return list(res.get("tenants", []))
33
38
 
34
39
  async def get(self, tenant_id: str) -> dict[str, Any]:
40
+ """Fetch one tenant (same shape as a ``list`` entry)."""
35
41
  return await self._client.get(f"/v1/partner/tenants/{tenant_id}")
36
42
 
37
43
  async def revoke(self, tenant_id: str) -> dict[str, Any]:
44
+ """Revoke a tenant. Cascades to its inbound webhooks and passports.
45
+
46
+ Returns {transitioned, webhooks_revoked, passports_revoked}.
47
+ Idempotent: a retry finishes a cascade that failed part-way.
48
+ """
38
49
  return await self._client.delete(f"/v1/partner/tenants/{tenant_id}")
39
50
 
40
51
  async def create_inbound_webhook(
@@ -6,8 +6,9 @@ the account exists, and returns an opaque ``sk_grant_*`` token you
6
6
  attach to the calls you already make — same Bearer header, scoped to
7
7
  what the grant created, revocable by the user under /services.
8
8
 
9
- Supersedes the Phase 5.6 child-tenant OWNERSHIP semantics; the tenant
10
- surface remains available through its deprecation window.
9
+ This is the user-owned partner model. For white-label deployment, where
10
+ you provision and manage the customer's account yourself, use the tenant
11
+ surface (``partner_tenants``) instead.
11
12
 
12
13
  Near-zero integration::
13
14
 
@@ -1,8 +1,21 @@
1
- """Partner-tenancy surface (Phase 5.6).
1
+ """White-label tenants — partner surface.
2
2
 
3
- Platform partners (AgentMail, Cofounder, etc.) with is_partner=True on
4
- their operator row provision isolated child operators on behalf of
5
- their customers via this service.
3
+ Platform partners (is_partner=True on their operator row) run STACK under
4
+ their own product: each customer gets an isolated child operator that the
5
+ partner provisions and manages. This is the white-label alternative to
6
+ "Connect with STACK" (``partner_connect.connect``), where the customer owns
7
+ their own STACK account.
8
+
9
+ Tenants have no plan of their own. Every tenant draws from the partner's
10
+ single allowance and wallet, under the partner's negotiated limits.
11
+
12
+ Error codes worth handling (``StackError.code``):
13
+
14
+ - TENANT_TIER_NOT_ALLOWED (400): ``tier`` was sent on create.
15
+ - TENANT_LIMIT_REACHED (409): the partner's deal caps the tenant count.
16
+ - ALLOWANCE_EXHAUSTED (402): the pool hit a negotiated hard cap.
17
+ - TENANT_BILLED_BY_PARTNER (403): a tenant key called a billing mutation.
18
+ - BILLING_ACCOUNT_INACTIVE (403): the tenant's partner is suspended or removed.
6
19
  """
7
20
 
8
21
  from __future__ import annotations
@@ -13,8 +26,6 @@ from urllib.parse import quote
13
26
  from .client import HttpClient
14
27
 
15
28
 
16
- Tier = Literal["free", "developer", "pro", "business", "enterprise"]
17
-
18
29
 
19
30
  class PartnerTenantsService:
20
31
  def __init__(self, client: HttpClient):
@@ -23,23 +34,36 @@ class PartnerTenantsService:
23
34
  def create(
24
35
  self,
25
36
  external_customer_id: Optional[str] = None,
26
- tier: Optional[Tier] = None,
27
37
  ) -> dict[str, Any]:
38
+ """Create a white-label tenant.
39
+
40
+ Tenants have no plan of their own; they draw from your allowance
41
+ and wallet. Returns operator_id, api_key (shown once — store it),
42
+ external_customer_id, tier ('partner'), billing ('partner') and
43
+ created_at. Raises with code TENANT_LIMIT_REACHED (409) when your
44
+ deal caps the tenant count.
45
+ """
28
46
  body: dict[str, Any] = {}
29
47
  if external_customer_id is not None:
30
48
  body["external_customer_id"] = external_customer_id
31
- if tier is not None:
32
- body["tier"] = tier
33
49
  return self._client.post("/v1/partner/tenants", json=body)
34
50
 
35
51
  def list(self) -> list[dict[str, Any]]:
52
+ """List your tenants. Each carries ``usage`` ({period, actions}):
53
+ its month-to-date share of your action pool, when available."""
36
54
  res = self._client.get("/v1/partner/tenants")
37
55
  return list(res.get("tenants", []))
38
56
 
39
57
  def get(self, tenant_id: str) -> dict[str, Any]:
58
+ """Fetch one tenant (same shape as a ``list`` entry)."""
40
59
  return self._client.get(f"/v1/partner/tenants/{tenant_id}")
41
60
 
42
61
  def revoke(self, tenant_id: str) -> dict[str, Any]:
62
+ """Revoke a tenant. Cascades to its inbound webhooks and passports.
63
+
64
+ Returns {transitioned, webhooks_revoked, passports_revoked}.
65
+ Idempotent: a retry finishes a cascade that failed part-way.
66
+ """
43
67
  return self._client.delete(f"/v1/partner/tenants/{tenant_id}")
44
68
 
45
69
  def create_inbound_webhook(
@@ -174,6 +174,8 @@ class ProxyService:
174
174
  def usage(self) -> dict:
175
175
  """Get monthly proxy usage stats.
176
176
 
177
- Returns dict with: tier, limit, used, remaining, period.
177
+ Returns dict with: tier (the plan the account draws from), limit,
178
+ used, remaining, period, and pooled=True on white-label tenants,
179
+ whose counts are the partner's shared pool.
178
180
  """
179
181
  return self._client.request("GET", "/v1/proxy/usage")
@@ -72,6 +72,8 @@ class ScanService:
72
72
  """Get monthly scan usage stats.
73
73
 
74
74
  Returns:
75
- Dict with keys: tier, limit, used, remaining, period.
75
+ Dict with keys: tier (the plan the account draws from), limit,
76
+ used, remaining, period, and pooled=True on white-label tenants,
77
+ whose counts are the partner's shared pool.
76
78
  """
77
79
  return self._client.request("GET", "/v1/scan/usage")
@@ -50,6 +50,10 @@ class UsageService:
50
50
  {used, limit, created_in_window?}), passthrough (None under a since/until
51
51
  window - those counters only exist for the current month),
52
52
  spend_cents (skills / llm_compute / identity / overage / total),
53
+ For pooled accounts (white-label tenants and their partners),
54
+ consumption.actions also carries pooled=True, own_used (this
55
+ account's share) and tenants (accounts sharing the pool), and
56
+ its ``used`` is the whole pool total.
53
57
  wallet_cents ({balance}), and earnings_cents ({unpaid, lifetime}).
54
58
  All money in cents.
55
59
  """
@@ -0,0 +1,69 @@
1
+ """White-label tenant service: create without tier, revoke cascade, signing setup."""
2
+ import asyncio
3
+ import sys
4
+ from pathlib import Path
5
+ from unittest.mock import AsyncMock, Mock
6
+
7
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
8
+
9
+ from getstack.partner_tenants import PartnerTenantsService
10
+ from getstack._async.partner_tenants import AsyncPartnerTenantsService
11
+
12
+
13
+ def test_sync_tenant_signing():
14
+ client = Mock()
15
+ client.put.return_value = {"configured": True}
16
+ result = PartnerTenantsService(client).set_inbound_webhook_signing_secret(
17
+ "tenant fixture", "wh/fixture", "synthetic-provider", "synthetic-downstream",
18
+ )
19
+ assert result == {"configured": True}
20
+ client.put.assert_called_once_with(
21
+ "/v1/partner/tenants/tenant%20fixture/inbound-webhooks/wh%2Ffixture/provider-signing-secret",
22
+ json={"signing_secret": "synthetic-provider", "downstream_signing_secret": "synthetic-downstream"},
23
+ )
24
+
25
+
26
+ def test_async_tenant_signing():
27
+ client = Mock()
28
+ client.put = AsyncMock(return_value={"configured": True})
29
+ result = asyncio.run(AsyncPartnerTenantsService(client).set_inbound_webhook_signing_secret(
30
+ "tenant fixture", "wh/fixture", "synthetic-provider",
31
+ ))
32
+ assert result == {"configured": True}
33
+ client.put.assert_awaited_once_with(
34
+ "/v1/partner/tenants/tenant%20fixture/inbound-webhooks/wh%2Ffixture/provider-signing-secret",
35
+ json={"signing_secret": "synthetic-provider"},
36
+ )
37
+
38
+
39
+ def test_sync_tenant_create_sends_no_tier():
40
+ client = Mock()
41
+ client.post.return_value = {"operator_id": "op_t", "tier": "partner", "billing": "partner"}
42
+ service = PartnerTenantsService(client)
43
+ result = service.create(external_customer_id="cust_1")
44
+ assert result["billing"] == "partner"
45
+ client.post.assert_called_once_with("/v1/partner/tenants", json={"external_customer_id": "cust_1"})
46
+ try:
47
+ service.create(external_customer_id="cust_2", tier="pro") # type: ignore[call-arg]
48
+ except TypeError:
49
+ pass
50
+ else:
51
+ raise AssertionError("create must not accept tier; tenants draw from the partner's plan")
52
+
53
+
54
+ def test_async_tenant_create_sends_no_tier():
55
+ client = Mock()
56
+ client.post = AsyncMock(return_value={"operator_id": "op_t", "tier": "partner", "billing": "partner"})
57
+ asyncio.run(AsyncPartnerTenantsService(client).create())
58
+ client.post.assert_awaited_once_with("/v1/partner/tenants", json={})
59
+
60
+
61
+ def test_tenant_revoke_returns_cascade_counts():
62
+ cascade = {"transitioned": True, "webhooks_revoked": 2, "passports_revoked": 3}
63
+ client = Mock()
64
+ client.delete.return_value = cascade
65
+ assert PartnerTenantsService(client).revoke("op_t") == cascade
66
+ client.delete.assert_called_once_with("/v1/partner/tenants/op_t")
67
+ aclient = Mock()
68
+ aclient.delete = AsyncMock(return_value=cascade)
69
+ assert asyncio.run(AsyncPartnerTenantsService(aclient).revoke("op_t")) == cascade
@@ -1,36 +0,0 @@
1
- """Tenant signing setup sends fixture keys in the body, never the URL."""
2
- import asyncio
3
- import sys
4
- from pathlib import Path
5
- from unittest.mock import AsyncMock, Mock
6
-
7
- sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
8
-
9
- from getstack.partner_tenants import PartnerTenantsService
10
- from getstack._async.partner_tenants import AsyncPartnerTenantsService
11
-
12
-
13
- def test_sync_tenant_signing():
14
- client = Mock()
15
- client.put.return_value = {"configured": True}
16
- result = PartnerTenantsService(client).set_inbound_webhook_signing_secret(
17
- "tenant fixture", "wh/fixture", "synthetic-provider", "synthetic-downstream",
18
- )
19
- assert result == {"configured": True}
20
- client.put.assert_called_once_with(
21
- "/v1/partner/tenants/tenant%20fixture/inbound-webhooks/wh%2Ffixture/provider-signing-secret",
22
- json={"signing_secret": "synthetic-provider", "downstream_signing_secret": "synthetic-downstream"},
23
- )
24
-
25
-
26
- def test_async_tenant_signing():
27
- client = Mock()
28
- client.put = AsyncMock(return_value={"configured": True})
29
- result = asyncio.run(AsyncPartnerTenantsService(client).set_inbound_webhook_signing_secret(
30
- "tenant fixture", "wh/fixture", "synthetic-provider",
31
- ))
32
- assert result == {"configured": True}
33
- client.put.assert_awaited_once_with(
34
- "/v1/partner/tenants/tenant%20fixture/inbound-webhooks/wh%2Ffixture/provider-signing-secret",
35
- json={"signing_secret": "synthetic-provider"},
36
- )
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