getstack 1.0.0__tar.gz → 1.2.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 (85) hide show
  1. {getstack-1.0.0 → getstack-1.2.0}/PKG-INFO +1 -1
  2. {getstack-1.0.0 → getstack-1.2.0}/pyproject.toml +1 -1
  3. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/inbound_webhooks.py +8 -0
  4. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/partner_tenants.py +14 -0
  5. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/inbound_webhooks.py +16 -6
  6. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/partner_tenants.py +14 -0
  7. getstack-1.2.0/tests/test_inbound_webhooks.py +51 -0
  8. {getstack-1.0.0 → getstack-1.2.0}/tests/test_parity.py +1 -0
  9. getstack-1.2.0/tests/test_partner_signing.py +36 -0
  10. {getstack-1.0.0 → getstack-1.2.0}/.gitignore +0 -0
  11. {getstack-1.0.0 → getstack-1.2.0}/CLAUDE.md +0 -0
  12. {getstack-1.0.0 → getstack-1.2.0}/LICENSE +0 -0
  13. {getstack-1.0.0 → getstack-1.2.0}/README.md +0 -0
  14. {getstack-1.0.0 → getstack-1.2.0}/scripts/verify_sign_claim_crosslang.py +0 -0
  15. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/__init__.py +0 -0
  16. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/__init__.py +0 -0
  17. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/agents.py +0 -0
  18. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/audit.py +0 -0
  19. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/authority.py +0 -0
  20. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/credentials.py +0 -0
  21. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/delivery_methods.py +0 -0
  22. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/detector_config.py +0 -0
  23. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/dropoffs.py +0 -0
  24. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/evidence_packs.py +0 -0
  25. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/identity.py +0 -0
  26. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/intents.py +0 -0
  27. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/llm.py +0 -0
  28. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/missions.py +0 -0
  29. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/notifications.py +0 -0
  30. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/partner_connect.py +0 -0
  31. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/passport_session.py +0 -0
  32. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/passports.py +0 -0
  33. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/pii_config.py +0 -0
  34. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/proxy.py +0 -0
  35. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/reviews.py +0 -0
  36. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/scan.py +0 -0
  37. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/security_events.py +0 -0
  38. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/services.py +0 -0
  39. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/skills.py +0 -0
  40. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/team.py +0 -0
  41. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_async/usage.py +0 -0
  42. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/_authority_request.py +0 -0
  43. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/agent_auth.py +0 -0
  44. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/agents.py +0 -0
  45. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/audit.py +0 -0
  46. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/auth.py +0 -0
  47. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/authority.py +0 -0
  48. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/browser_bootstrap.py +0 -0
  49. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/client.py +0 -0
  50. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/credentials.py +0 -0
  51. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/delivery_methods.py +0 -0
  52. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/detector_config.py +0 -0
  53. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/dropoffs.py +0 -0
  54. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/errors.py +0 -0
  55. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/evidence_packs.py +0 -0
  56. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/identity.py +0 -0
  57. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/intents.py +0 -0
  58. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/llm.py +0 -0
  59. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/missions.py +0 -0
  60. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/notifications.py +0 -0
  61. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/partner_connect.py +0 -0
  62. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/passport_header_verify.py +0 -0
  63. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/passport_session.py +0 -0
  64. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/passports.py +0 -0
  65. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/pii_config.py +0 -0
  66. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/proxy.py +0 -0
  67. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/py.typed +0 -0
  68. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/reviews.py +0 -0
  69. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/scan.py +0 -0
  70. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/security_events.py +0 -0
  71. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/services.py +0 -0
  72. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/sign_claim.py +0 -0
  73. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/skills.py +0 -0
  74. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/team.py +0 -0
  75. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/types.py +0 -0
  76. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/usage.py +0 -0
  77. {getstack-1.0.0 → getstack-1.2.0}/src/getstack/verify_offline.py +0 -0
  78. {getstack-1.0.0 → getstack-1.2.0}/tests/test_authority.py +0 -0
  79. {getstack-1.0.0 → getstack-1.2.0}/tests/test_delivery_methods.py +0 -0
  80. {getstack-1.0.0 → getstack-1.2.0}/tests/test_dropoffs.py +0 -0
  81. {getstack-1.0.0 → getstack-1.2.0}/tests/test_errors.py +0 -0
  82. {getstack-1.0.0 → getstack-1.2.0}/tests/test_evidence_packs.py +0 -0
  83. {getstack-1.0.0 → getstack-1.2.0}/tests/test_identity_requirements.py +0 -0
  84. {getstack-1.0.0 → getstack-1.2.0}/tests/test_missions.py +0 -0
  85. {getstack-1.0.0 → getstack-1.2.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.0.0
3
+ Version: 1.2.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.0.0"
7
+ version = "1.2.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,14 @@ class AsyncInboundWebhookService:
32
32
  res = await self._client.get("/v1/operator/inbound-webhooks")
33
33
  return list(res.get("webhooks", []))
34
34
 
35
+ async def set_provider_signing_secret(self, webhook_id: str, signing_secret: str, downstream_signing_secret: Optional[str] = None) -> dict[str, Any]:
36
+ """Configure the incoming provider key. If a generic downstream key is omitted, STACK generates and returns its replacement once."""
37
+ from urllib.parse import quote
38
+ return await self._client.put(
39
+ f"/v1/operator/inbound-webhooks/{quote(webhook_id, safe='')}/provider-signing-secret",
40
+ json={"signing_secret": signing_secret, **({"downstream_signing_secret": downstream_signing_secret} if downstream_signing_secret is not None else {})},
41
+ )
42
+
35
43
  async def get(self, webhook_id: str) -> dict[str, Any]:
36
44
  return await self._client.get(f"/v1/operator/inbound-webhooks/{webhook_id}")
37
45
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from typing import Any, Literal, Optional
6
+ from urllib.parse import quote
6
7
 
7
8
  from ..client import AsyncHttpClient
8
9
 
@@ -54,6 +55,19 @@ class AsyncPartnerTenantsService:
54
55
  json=body,
55
56
  )
56
57
 
58
+ async def set_inbound_webhook_signing_secret(
59
+ self, tenant_id: str, webhook_id: str, signing_secret: str,
60
+ downstream_signing_secret: Optional[str] = None,
61
+ ) -> dict[str, Any]:
62
+ """Configure signing with direct partner admin authentication; returns status only."""
63
+ body = {"signing_secret": signing_secret}
64
+ if downstream_signing_secret is not None:
65
+ body["downstream_signing_secret"] = downstream_signing_secret
66
+ return await self._client.put(
67
+ f"/v1/partner/tenants/{quote(tenant_id, safe='')}/inbound-webhooks/{quote(webhook_id, safe='')}/provider-signing-secret",
68
+ json=body,
69
+ )
70
+
57
71
  async def issue_passport(
58
72
  self,
59
73
  tenant_id: str,
@@ -1,8 +1,9 @@
1
1
  """Hosted inbound webhook surface (Phase 5.5).
2
2
 
3
- Operator creates a webhook row + receives a STACK-hosted URL + HMAC
4
- secret. Paste both into a provider's (AgentMail / generic) webhook
5
- config; STACK runs the prompt-injection detector on inbound content
3
+ Operator creates a webhook row + receives a STACK-hosted URL and distinct
4
+ incoming/downstream HMAC secrets. Generic providers sign with the incoming secret. AgentMail endpoints
5
+ require their provider signing secret through set_provider_signing_secret.
6
+ STACK runs the prompt-injection detector on inbound content
6
7
  before forwarding to a downstream URL.
7
8
  """
8
9
 
@@ -31,9 +32,10 @@ class InboundWebhookService:
31
32
  """Create a hosted inbound webhook.
32
33
 
33
34
  Returns a dict with keys: id, provider, slug, webhook_url,
34
- webhook_secret (plaintext, returned ONLY at create), agent_id,
35
- forward_url, created_at. Capture webhook_secret immediately —
36
- STACK never returns it again. If lost, revoke + recreate.
35
+ webhook_secret (downstream verification key, returned ONLY at create),
36
+ incoming_signing_secret (generic only, returned ONLY at create), agent_id,
37
+ forward_url, created_at. Capture both immediately. STACK never returns
38
+ them through list/get. If lost, revoke + recreate.
37
39
  """
38
40
  body: dict[str, Any] = {"provider": provider, "agent_id": agent_id}
39
41
  if forward_url is not None:
@@ -47,6 +49,14 @@ class InboundWebhookService:
47
49
  res = self._client.get("/v1/operator/inbound-webhooks")
48
50
  return list(res.get("webhooks", []))
49
51
 
52
+ def set_provider_signing_secret(self, webhook_id: str, signing_secret: str, downstream_signing_secret: Optional[str] = None) -> dict[str, Any]:
53
+ """Configure the incoming provider key. If a generic downstream key is omitted, STACK generates and returns its replacement once."""
54
+ from urllib.parse import quote
55
+ return self._client.put(
56
+ f"/v1/operator/inbound-webhooks/{quote(webhook_id, safe='')}/provider-signing-secret",
57
+ json={"signing_secret": signing_secret, **({"downstream_signing_secret": downstream_signing_secret} if downstream_signing_secret is not None else {})},
58
+ )
59
+
50
60
  def get(self, webhook_id: str) -> dict[str, Any]:
51
61
  """Get a single inbound webhook by id. Never returns the plaintext secret."""
52
62
  return self._client.get(f"/v1/operator/inbound-webhooks/{webhook_id}")
@@ -8,6 +8,7 @@ their customers via this service.
8
8
  from __future__ import annotations
9
9
 
10
10
  from typing import Any, Literal, Optional
11
+ from urllib.parse import quote
11
12
 
12
13
  from .client import HttpClient
13
14
 
@@ -59,6 +60,19 @@ class PartnerTenantsService:
59
60
  json=body,
60
61
  )
61
62
 
63
+ def set_inbound_webhook_signing_secret(
64
+ self, tenant_id: str, webhook_id: str, signing_secret: str,
65
+ downstream_signing_secret: Optional[str] = None,
66
+ ) -> dict[str, Any]:
67
+ """Configure signing with direct partner admin authentication; returns status only."""
68
+ body = {"signing_secret": signing_secret}
69
+ if downstream_signing_secret is not None:
70
+ body["downstream_signing_secret"] = downstream_signing_secret
71
+ return self._client.put(
72
+ f"/v1/partner/tenants/{quote(tenant_id, safe='')}/inbound-webhooks/{quote(webhook_id, safe='')}/provider-signing-secret",
73
+ json=body,
74
+ )
75
+
62
76
  def issue_passport(
63
77
  self,
64
78
  tenant_id: str,
@@ -0,0 +1,51 @@
1
+ """Inbound webhook directional-key wire contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
10
+
11
+ from getstack._async.inbound_webhooks import AsyncInboundWebhookService # noqa: E402
12
+ from getstack.inbound_webhooks import InboundWebhookService # noqa: E402
13
+
14
+
15
+ class CaptureClient:
16
+ def __init__(self) -> None:
17
+ self.path: str | None = None
18
+ self.body: dict | None = None
19
+
20
+ def put(self, path: str, json: dict | None = None):
21
+ self.path = path
22
+ self.body = json
23
+ return {"configured": True, "webhook_secret": "synthetic-generated-downstream"}
24
+
25
+
26
+ class AsyncCaptureClient(CaptureClient):
27
+ async def put(self, path: str, json: dict | None = None):
28
+ return super().put(path, json=json)
29
+
30
+
31
+ def test_sync_signing_setup_keeps_directional_keys_in_the_body() -> None:
32
+ client = CaptureClient()
33
+ result = InboundWebhookService(client).set_provider_signing_secret( # type: ignore[arg-type]
34
+ "iwh fixture", "synthetic-incoming", "synthetic-downstream"
35
+ )
36
+ assert client.path == "/v1/operator/inbound-webhooks/iwh%20fixture/provider-signing-secret"
37
+ assert client.body == {
38
+ "signing_secret": "synthetic-incoming",
39
+ "downstream_signing_secret": "synthetic-downstream",
40
+ }
41
+ assert result == {"configured": True, "webhook_secret": "synthetic-generated-downstream"}
42
+
43
+
44
+ def test_async_signing_setup_matches_sync() -> None:
45
+ client = AsyncCaptureClient()
46
+ result = asyncio.run(AsyncInboundWebhookService(client).set_provider_signing_secret( # type: ignore[arg-type]
47
+ "iwh fixture", "synthetic-incoming"
48
+ ))
49
+ assert client.path == "/v1/operator/inbound-webhooks/iwh%20fixture/provider-signing-secret"
50
+ assert client.body == {"signing_secret": "synthetic-incoming"}
51
+ assert result == {"configured": True, "webhook_secret": "synthetic-generated-downstream"}
@@ -52,6 +52,7 @@ EXPECTED_SERVICES = [
52
52
  # (service, method) pairs that must exist on both clients — the integration-
53
53
  # critical surface, incl. the gaps GET-244 closed.
54
54
  EXPECTED_METHODS = [
55
+ ("inbound_webhooks", "set_provider_signing_secret"),
55
56
  ("services", "connect"),
56
57
  ("services", "connect_custom"),
57
58
  ("services", "grant_agent_access"),
@@ -0,0 +1,36 @@
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
File without changes