getstack 0.14.0__tar.gz → 0.16.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 (69) hide show
  1. {getstack-0.14.0 → getstack-0.16.0}/CLAUDE.md +1 -1
  2. {getstack-0.14.0 → getstack-0.16.0}/PKG-INFO +1 -1
  3. {getstack-0.14.0 → getstack-0.16.0}/pyproject.toml +1 -1
  4. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/__init__.py +4 -0
  5. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/agents.py +14 -1
  6. getstack-0.16.0/src/getstack/_async/usage.py +15 -0
  7. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/agents.py +24 -0
  8. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/types.py +7 -0
  9. getstack-0.16.0/src/getstack/usage.py +37 -0
  10. {getstack-0.14.0 → getstack-0.16.0}/tests/test_parity.py +74 -2
  11. {getstack-0.14.0 → getstack-0.16.0}/.gitignore +0 -0
  12. {getstack-0.14.0 → getstack-0.16.0}/LICENSE +0 -0
  13. {getstack-0.14.0 → getstack-0.16.0}/README.md +0 -0
  14. {getstack-0.14.0 → getstack-0.16.0}/scripts/verify_sign_claim_crosslang.py +0 -0
  15. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/__init__.py +0 -0
  16. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/audit.py +0 -0
  17. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/credentials.py +0 -0
  18. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/delivery_methods.py +0 -0
  19. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/detector_config.py +0 -0
  20. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/dropoffs.py +0 -0
  21. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/evidence_packs.py +0 -0
  22. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/identity.py +0 -0
  23. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/inbound_webhooks.py +0 -0
  24. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/intents.py +0 -0
  25. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/llm.py +0 -0
  26. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/missions.py +0 -0
  27. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/notifications.py +0 -0
  28. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/partner_tenants.py +0 -0
  29. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/passport_session.py +0 -0
  30. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/passports.py +0 -0
  31. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/proxy.py +0 -0
  32. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/reviews.py +0 -0
  33. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/scan.py +0 -0
  34. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/security_events.py +0 -0
  35. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/services.py +0 -0
  36. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/skills.py +0 -0
  37. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/_async/team.py +0 -0
  38. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/agent_auth.py +0 -0
  39. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/audit.py +0 -0
  40. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/auth.py +0 -0
  41. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/browser_bootstrap.py +0 -0
  42. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/client.py +0 -0
  43. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/credentials.py +0 -0
  44. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/delivery_methods.py +0 -0
  45. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/detector_config.py +0 -0
  46. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/dropoffs.py +0 -0
  47. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/errors.py +0 -0
  48. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/evidence_packs.py +0 -0
  49. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/identity.py +0 -0
  50. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/inbound_webhooks.py +0 -0
  51. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/intents.py +0 -0
  52. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/llm.py +0 -0
  53. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/missions.py +0 -0
  54. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/notifications.py +0 -0
  55. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/partner_tenants.py +0 -0
  56. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/passport_header_verify.py +0 -0
  57. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/passport_session.py +0 -0
  58. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/passports.py +0 -0
  59. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/proxy.py +0 -0
  60. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/py.typed +0 -0
  61. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/reviews.py +0 -0
  62. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/scan.py +0 -0
  63. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/security_events.py +0 -0
  64. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/services.py +0 -0
  65. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/sign_claim.py +0 -0
  66. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/skills.py +0 -0
  67. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/team.py +0 -0
  68. {getstack-0.14.0 → getstack-0.16.0}/src/getstack/verify_offline.py +0 -0
  69. {getstack-0.14.0 → getstack-0.16.0}/tests/test_sign_claim.py +0 -0
@@ -12,7 +12,7 @@ Published to PyPI as `getstack`. Not a workspace member of the main monorepo
12
12
  `AsyncStack`): agents, passports (+ delegate + revoke-agent/session/all +
13
13
  list_active), services (+ connect + grant/revoke/get_permissions),
14
14
  credentials, drop-offs (+ expire), skills, identity, audit, notifications
15
- (+ update), security-events, proxy, scan, intents, missions,
15
+ (+ update), security-events, proxy, scan, usage, intents, missions,
16
16
  evidence-packs, llm gateway, inbound-webhooks, partner, **team**,
17
17
  **delivery_methods**, **detector_config**.
18
18
  - **Offline / customer-managed crypto** (the JS headline features):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.14.0
3
+ Version: 0.16.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.14.0"
7
+ version = "0.16.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -49,6 +49,7 @@ from .notifications import NotificationService
49
49
  from .audit import AuditService
50
50
  from .proxy import ProxyService as ProxySvc, ProxyResponse
51
51
  from .scan import ScanService
52
+ from .usage import UsageService
52
53
  from .security_events import SecurityEventService as SecurityEventSvc
53
54
  from .skills import SkillService
54
55
  from .identity import IdentityService
@@ -233,6 +234,7 @@ class Stack:
233
234
  self.audit = AuditService(self._client)
234
235
  self.proxy = ProxySvc(self._client)
235
236
  self.scan = ScanService(self._client)
237
+ self.usage = UsageService(self._client)
236
238
  self.security_events = SecurityEventSvc(self._client)
237
239
  self.skills = SkillService(self._client)
238
240
  self.identity = IdentityService(self._client)
@@ -378,6 +380,7 @@ class AsyncStack:
378
380
  from ._async.audit import AsyncAuditService
379
381
  from ._async.proxy import AsyncProxyService
380
382
  from ._async.scan import AsyncScanService
383
+ from ._async.usage import AsyncUsageService
381
384
  from ._async.security_events import AsyncSecurityEventService
382
385
  from ._async.skills import AsyncSkillService
383
386
  from ._async.identity import AsyncIdentityService
@@ -402,6 +405,7 @@ class AsyncStack:
402
405
  self.audit = AsyncAuditService(self._client)
403
406
  self.proxy = AsyncProxyService(self._client)
404
407
  self.scan = AsyncScanService(self._client)
408
+ self.usage = AsyncUsageService(self._client)
405
409
  self.security_events = AsyncSecurityEventService(self._client)
406
410
  self.skills = AsyncSkillService(self._client)
407
411
  self.identity = AsyncIdentityService(self._client)
@@ -23,10 +23,15 @@ class AsyncAgentService:
23
23
  key_mode: str | None = None,
24
24
  on_warning: str = "notify",
25
25
  on_critical: str = "notify",
26
+ llm_inject_system_prompt: bool | None = None,
27
+ llm_redact_pii: bool | None = None,
26
28
  ) -> Agent:
27
29
  """Register an agent. See the sync :meth:`AgentService.register`
28
30
  docstring — pass ``profile="proxied-llm-bot"`` for a bot that just
29
- runs. Unset fields are not sent, so they never clobber a preset."""
31
+ runs. Unset fields are not sent, so they never clobber a preset.
32
+ The GET-245 LLM gateway toggles both default True server-side;
33
+ ``llm_redact_pii=False`` sends user PII to the LLM provider
34
+ unredacted (explicit operator decision) and False IS sent."""
30
35
  body: dict[str, Any] = {"name": name}
31
36
  if description:
32
37
  body["description"] = description
@@ -42,6 +47,11 @@ class AsyncAgentService:
42
47
  body["on_warning"] = on_warning
43
48
  if on_critical != "notify":
44
49
  body["on_critical"] = on_critical
50
+ # is-not-None (not truthiness) so an explicit False reaches the wire.
51
+ if llm_inject_system_prompt is not None:
52
+ body["llm_inject_system_prompt"] = llm_inject_system_prompt
53
+ if llm_redact_pii is not None:
54
+ body["llm_redact_pii"] = llm_redact_pii
45
55
  return _to_agent(await self._client.post("/v1/agents/register", json=body))
46
56
 
47
57
  async def get(self, agent_id: str) -> Agent:
@@ -52,6 +62,9 @@ class AsyncAgentService:
52
62
  return [_to_agent(a) for a in data]
53
63
 
54
64
  async def update(self, agent_id: str, **kwargs: Any) -> Agent:
65
+ """PATCH mutable agent fields, passed through as-is — including
66
+ the GET-245 LLM gateway toggles; see the sync
67
+ :meth:`AgentService.update`."""
55
68
  return _to_agent(await self._client.patch(f"/v1/agents/{agent_id}", json=kwargs))
56
69
 
57
70
  async def suspend(self, agent_id: str) -> Agent:
@@ -0,0 +1,15 @@
1
+ """Async mirror of UsageService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncUsageService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def get(self) -> dict[str, Any]:
15
+ return await self._client.request("GET", "/v1/usage")
@@ -28,6 +28,10 @@ def _to_agent(data: dict[str, Any]) -> Agent:
28
28
  updated_at=data["updated_at"],
29
29
  credential_access=data.get("credential_access", "direct"),
30
30
  key_mode=data.get("key_mode", "stack_managed"),
31
+ # GET-245 — LLM gateway toggles; True mirrors the DB default
32
+ # (fail-closed) when an older API omits them.
33
+ llm_inject_system_prompt=data.get("llm_inject_system_prompt", True),
34
+ llm_redact_pii=data.get("llm_redact_pii", True),
31
35
  )
32
36
 
33
37
 
@@ -45,6 +49,8 @@ class AgentService:
45
49
  key_mode: str | None = None,
46
50
  on_warning: str = "notify",
47
51
  on_critical: str = "notify",
52
+ llm_inject_system_prompt: bool | None = None,
53
+ llm_redact_pii: bool | None = None,
48
54
  ) -> Agent:
49
55
  """Register an agent.
50
56
 
@@ -59,6 +65,14 @@ class AgentService:
59
65
  server default (``enforced`` + ``direct``) OR the profile value —
60
66
  they are only sent when provided, so an unset value never clobbers
61
67
  a preset.
68
+
69
+ ``llm_inject_system_prompt`` / ``llm_redact_pii`` tune the
70
+ ``/v1/llm/*`` gateway per agent (both default True server-side):
71
+ inject prepends the STACK security context to every gateway call;
72
+ redact replaces PII in user messages before forwarding.
73
+ ``llm_redact_pii=False`` means user PII flows to the LLM provider
74
+ unredacted — an explicit operator decision. ``False`` is sent on
75
+ the wire; only ``None`` (unset) is omitted.
62
76
  """
63
77
  body: dict[str, Any] = {"name": name}
64
78
  if description:
@@ -75,6 +89,11 @@ class AgentService:
75
89
  body["on_warning"] = on_warning
76
90
  if on_critical != "notify":
77
91
  body["on_critical"] = on_critical
92
+ # is-not-None (not truthiness) so an explicit False reaches the wire.
93
+ if llm_inject_system_prompt is not None:
94
+ body["llm_inject_system_prompt"] = llm_inject_system_prompt
95
+ if llm_redact_pii is not None:
96
+ body["llm_redact_pii"] = llm_redact_pii
78
97
  return _to_agent(self._client.post("/v1/agents/register", json=body))
79
98
 
80
99
  def get(self, agent_id: str) -> Agent:
@@ -85,6 +104,11 @@ class AgentService:
85
104
  return [_to_agent(a) for a in data]
86
105
 
87
106
  def update(self, agent_id: str, **kwargs: Any) -> Agent:
107
+ """PATCH mutable agent fields, passed through as-is. The GET-245
108
+ LLM gateway toggles (``llm_inject_system_prompt``,
109
+ ``llm_redact_pii``) ride here too — see :meth:`register` for
110
+ semantics; ``llm_redact_pii=False`` sends user PII to the LLM
111
+ provider unredacted."""
88
112
  return _to_agent(self._client.patch(f"/v1/agents/{agent_id}", json=kwargs))
89
113
 
90
114
  def suspend(self, agent_id: str) -> Agent:
@@ -28,6 +28,13 @@ class Agent:
28
28
  # see whether the box needs an agent keypair (customer_managed) or the
29
29
  # operator credential (stack_managed). Defaulted for older responses.
30
30
  key_mode: str = "stack_managed"
31
+ # GET-245 — per-agent LLM gateway toggles. Both default true at the DB
32
+ # (fail-closed): inject prepends the STACK security context to /v1/llm/*
33
+ # calls; redact replaces PII in user messages before forwarding. False on
34
+ # llm_redact_pii means user PII reaches the LLM provider unredacted.
35
+ # Defaulted for older API responses that predate the fields.
36
+ llm_inject_system_prompt: bool = True
37
+ llm_redact_pii: bool = True
31
38
 
32
39
 
33
40
  @dataclass
@@ -0,0 +1,37 @@
1
+ """Usage service — `/v1/usage` (usage + spend + earnings rollup).
2
+
3
+ The one-call rollup of what your operator consumed against its plan,
4
+ spent, and earned this billing period. Superset of the per-domain meters
5
+ (`stack.scan.usage()`, `stack.llm.usage`, `stack.proxy` counts) — one
6
+ authoritative shape the CLI and dashboard read too. Money fields are in
7
+ cents.
8
+
9
+ Example::
10
+
11
+ u = stack.usage.get()
12
+ p = u["consumption"]["passports"]
13
+ print(f"Passports {p['used']}/{p['limit']}")
14
+ print(f"Spent this period: ${u['spend_cents']['total'] / 100:.2f}")
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ from .client import HttpClient
22
+
23
+
24
+ class UsageService:
25
+ def __init__(self, client: HttpClient):
26
+ self._client = client
27
+
28
+ def get(self) -> dict[str, Any]:
29
+ """Usage, spend, and earnings for the current billing period.
30
+
31
+ Returns:
32
+ Dict with keys: tier, period_start, period_end, consumption
33
+ (per-meter {used, limit}), spend_cents (skills / llm_compute /
34
+ identity / overage / total), wallet_cents ({balance}), and
35
+ earnings_cents ({unpaid, lifetime}). All money in cents.
36
+ """
37
+ return self._client.request("GET", "/v1/usage")
@@ -32,8 +32,8 @@ from getstack import ( # noqa: E402
32
32
  # Services that must exist on BOTH clients.
33
33
  EXPECTED_SERVICES = [
34
34
  "agents", "passports", "credentials", "services", "dropoffs", "reviews",
35
- "notifications", "audit", "proxy", "scan", "security_events", "skills",
36
- "identity", "intents", "missions", "evidence_packs", "llm",
35
+ "notifications", "audit", "proxy", "scan", "usage", "security_events",
36
+ "skills", "identity", "intents", "missions", "evidence_packs", "llm",
37
37
  "inbound_webhooks", "partner_tenants", "team", "delivery_methods",
38
38
  "detector_config",
39
39
  ]
@@ -58,6 +58,7 @@ EXPECTED_METHODS = [
58
58
  ("team", "invite"),
59
59
  ("delivery_methods", "add"),
60
60
  ("detector_config", "upsert"),
61
+ ("usage", "get"),
61
62
  ]
62
63
 
63
64
 
@@ -88,6 +89,74 @@ def test_register_accepts_key_mode_on_both():
88
89
  for client in (s, a):
89
90
  params = inspect.signature(client.agents.register).parameters
90
91
  assert "profile" in params and "credential_access" in params
92
+ # GET-245 LLM gateway toggles.
93
+ assert "llm_inject_system_prompt" in params and "llm_redact_pii" in params
94
+
95
+
96
+ # Minimal valid agent response for _to_agent; llm_redact_pii=False proves
97
+ # the response field lands on the model instead of the True fallback.
98
+ _AGENT_RESPONSE = {
99
+ "id": "agent_x", "operator_id": "op_x", "name": "bot", "status": "active",
100
+ "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z",
101
+ "llm_inject_system_prompt": True, "llm_redact_pii": False,
102
+ }
103
+
104
+
105
+ class _CaptureClient:
106
+ """Stands in for HttpClient/AsyncHttpClient; records the last body."""
107
+
108
+ def __init__(self, is_async: bool = False):
109
+ self.is_async = is_async
110
+ self.json = None
111
+
112
+ def _record(self, json):
113
+ self.json = json
114
+ return dict(_AGENT_RESPONSE)
115
+
116
+ def post(self, path, json=None, **kw):
117
+ if self.is_async:
118
+ async def _coro():
119
+ return self._record(json)
120
+ return _coro()
121
+ return self._record(json)
122
+
123
+ patch = post
124
+
125
+
126
+ def test_register_llm_toggles_round_trip_sync():
127
+ s, _ = _clients()
128
+ s.agents._client = stub = _CaptureClient()
129
+ agent = s.agents.register(
130
+ "bot", llm_inject_system_prompt=True, llm_redact_pii=False
131
+ )
132
+ # Both flags reach the body; explicit False is sent, not dropped.
133
+ assert stub.json["llm_inject_system_prompt"] is True
134
+ assert stub.json["llm_redact_pii"] is False
135
+ # And the response fields land on the Agent model.
136
+ assert agent.llm_inject_system_prompt is True and agent.llm_redact_pii is False
137
+ # Unset -> omitted, so the server/profile default is never clobbered.
138
+ s.agents.register("bot2")
139
+ assert "llm_inject_system_prompt" not in stub.json
140
+ assert "llm_redact_pii" not in stub.json
141
+
142
+
143
+ def test_register_llm_toggles_round_trip_async():
144
+ import asyncio
145
+
146
+ _, a = _clients()
147
+ a.agents._client = stub = _CaptureClient(is_async=True)
148
+ agent = asyncio.run(a.agents.register("bot", llm_redact_pii=False))
149
+ assert stub.json["llm_redact_pii"] is False
150
+ assert "llm_inject_system_prompt" not in stub.json
151
+ assert agent.llm_redact_pii is False
152
+
153
+
154
+ def test_update_llm_toggles_pass_through():
155
+ s, _ = _clients()
156
+ s.agents._client = stub = _CaptureClient()
157
+ agent = s.agents.update("agent_x", llm_redact_pii=False)
158
+ assert stub.json == {"llm_redact_pii": False}
159
+ assert agent.llm_redact_pii is False
91
160
 
92
161
 
93
162
  def test_offline_crypto_exported():
@@ -104,6 +173,9 @@ if __name__ == "__main__":
104
173
  test_both_clients_have_all_services()
105
174
  test_both_clients_have_all_methods()
106
175
  test_register_accepts_key_mode_on_both()
176
+ test_register_llm_toggles_round_trip_sync()
177
+ test_register_llm_toggles_round_trip_async()
178
+ test_update_llm_toggles_pass_through()
107
179
  test_offline_crypto_exported()
108
180
  test_service_classes_exported()
109
181
  print("all parity tests passed")
File without changes
File without changes
File without changes
File without changes