getstack 0.14.0__tar.gz → 0.15.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 (67) hide show
  1. {getstack-0.14.0 → getstack-0.15.0}/PKG-INFO +1 -1
  2. {getstack-0.14.0 → getstack-0.15.0}/pyproject.toml +1 -1
  3. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/agents.py +14 -1
  4. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/agents.py +24 -0
  5. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/types.py +7 -0
  6. {getstack-0.14.0 → getstack-0.15.0}/tests/test_parity.py +71 -0
  7. {getstack-0.14.0 → getstack-0.15.0}/.gitignore +0 -0
  8. {getstack-0.14.0 → getstack-0.15.0}/CLAUDE.md +0 -0
  9. {getstack-0.14.0 → getstack-0.15.0}/LICENSE +0 -0
  10. {getstack-0.14.0 → getstack-0.15.0}/README.md +0 -0
  11. {getstack-0.14.0 → getstack-0.15.0}/scripts/verify_sign_claim_crosslang.py +0 -0
  12. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/__init__.py +0 -0
  13. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/__init__.py +0 -0
  14. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/audit.py +0 -0
  15. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/credentials.py +0 -0
  16. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/delivery_methods.py +0 -0
  17. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/detector_config.py +0 -0
  18. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/dropoffs.py +0 -0
  19. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/evidence_packs.py +0 -0
  20. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/identity.py +0 -0
  21. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/inbound_webhooks.py +0 -0
  22. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/intents.py +0 -0
  23. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/llm.py +0 -0
  24. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/missions.py +0 -0
  25. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/notifications.py +0 -0
  26. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/partner_tenants.py +0 -0
  27. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/passport_session.py +0 -0
  28. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/passports.py +0 -0
  29. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/proxy.py +0 -0
  30. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/reviews.py +0 -0
  31. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/scan.py +0 -0
  32. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/security_events.py +0 -0
  33. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/services.py +0 -0
  34. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/skills.py +0 -0
  35. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/team.py +0 -0
  36. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/agent_auth.py +0 -0
  37. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/audit.py +0 -0
  38. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/auth.py +0 -0
  39. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/browser_bootstrap.py +0 -0
  40. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/client.py +0 -0
  41. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/credentials.py +0 -0
  42. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/delivery_methods.py +0 -0
  43. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/detector_config.py +0 -0
  44. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/dropoffs.py +0 -0
  45. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/errors.py +0 -0
  46. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/evidence_packs.py +0 -0
  47. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/identity.py +0 -0
  48. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/inbound_webhooks.py +0 -0
  49. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/intents.py +0 -0
  50. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/llm.py +0 -0
  51. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/missions.py +0 -0
  52. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/notifications.py +0 -0
  53. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/partner_tenants.py +0 -0
  54. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/passport_header_verify.py +0 -0
  55. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/passport_session.py +0 -0
  56. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/passports.py +0 -0
  57. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/proxy.py +0 -0
  58. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/py.typed +0 -0
  59. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/reviews.py +0 -0
  60. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/scan.py +0 -0
  61. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/security_events.py +0 -0
  62. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/services.py +0 -0
  63. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/sign_claim.py +0 -0
  64. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/skills.py +0 -0
  65. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/team.py +0 -0
  66. {getstack-0.14.0 → getstack-0.15.0}/src/getstack/verify_offline.py +0 -0
  67. {getstack-0.14.0 → getstack-0.15.0}/tests/test_sign_claim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.14.0
3
+ Version: 0.15.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.15.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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:
@@ -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
@@ -88,6 +88,74 @@ def test_register_accepts_key_mode_on_both():
88
88
  for client in (s, a):
89
89
  params = inspect.signature(client.agents.register).parameters
90
90
  assert "profile" in params and "credential_access" in params
91
+ # GET-245 LLM gateway toggles.
92
+ assert "llm_inject_system_prompt" in params and "llm_redact_pii" in params
93
+
94
+
95
+ # Minimal valid agent response for _to_agent; llm_redact_pii=False proves
96
+ # the response field lands on the model instead of the True fallback.
97
+ _AGENT_RESPONSE = {
98
+ "id": "agent_x", "operator_id": "op_x", "name": "bot", "status": "active",
99
+ "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z",
100
+ "llm_inject_system_prompt": True, "llm_redact_pii": False,
101
+ }
102
+
103
+
104
+ class _CaptureClient:
105
+ """Stands in for HttpClient/AsyncHttpClient; records the last body."""
106
+
107
+ def __init__(self, is_async: bool = False):
108
+ self.is_async = is_async
109
+ self.json = None
110
+
111
+ def _record(self, json):
112
+ self.json = json
113
+ return dict(_AGENT_RESPONSE)
114
+
115
+ def post(self, path, json=None, **kw):
116
+ if self.is_async:
117
+ async def _coro():
118
+ return self._record(json)
119
+ return _coro()
120
+ return self._record(json)
121
+
122
+ patch = post
123
+
124
+
125
+ def test_register_llm_toggles_round_trip_sync():
126
+ s, _ = _clients()
127
+ s.agents._client = stub = _CaptureClient()
128
+ agent = s.agents.register(
129
+ "bot", llm_inject_system_prompt=True, llm_redact_pii=False
130
+ )
131
+ # Both flags reach the body; explicit False is sent, not dropped.
132
+ assert stub.json["llm_inject_system_prompt"] is True
133
+ assert stub.json["llm_redact_pii"] is False
134
+ # And the response fields land on the Agent model.
135
+ assert agent.llm_inject_system_prompt is True and agent.llm_redact_pii is False
136
+ # Unset -> omitted, so the server/profile default is never clobbered.
137
+ s.agents.register("bot2")
138
+ assert "llm_inject_system_prompt" not in stub.json
139
+ assert "llm_redact_pii" not in stub.json
140
+
141
+
142
+ def test_register_llm_toggles_round_trip_async():
143
+ import asyncio
144
+
145
+ _, a = _clients()
146
+ a.agents._client = stub = _CaptureClient(is_async=True)
147
+ agent = asyncio.run(a.agents.register("bot", llm_redact_pii=False))
148
+ assert stub.json["llm_redact_pii"] is False
149
+ assert "llm_inject_system_prompt" not in stub.json
150
+ assert agent.llm_redact_pii is False
151
+
152
+
153
+ def test_update_llm_toggles_pass_through():
154
+ s, _ = _clients()
155
+ s.agents._client = stub = _CaptureClient()
156
+ agent = s.agents.update("agent_x", llm_redact_pii=False)
157
+ assert stub.json == {"llm_redact_pii": False}
158
+ assert agent.llm_redact_pii is False
91
159
 
92
160
 
93
161
  def test_offline_crypto_exported():
@@ -104,6 +172,9 @@ if __name__ == "__main__":
104
172
  test_both_clients_have_all_services()
105
173
  test_both_clients_have_all_methods()
106
174
  test_register_accepts_key_mode_on_both()
175
+ test_register_llm_toggles_round_trip_sync()
176
+ test_register_llm_toggles_round_trip_async()
177
+ test_update_llm_toggles_pass_through()
107
178
  test_offline_crypto_exported()
108
179
  test_service_classes_exported()
109
180
  print("all parity tests passed")
File without changes
File without changes
File without changes
File without changes
File without changes