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.
- {getstack-0.14.0 → getstack-0.15.0}/PKG-INFO +1 -1
- {getstack-0.14.0 → getstack-0.15.0}/pyproject.toml +1 -1
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/agents.py +14 -1
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/agents.py +24 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/types.py +7 -0
- {getstack-0.14.0 → getstack-0.15.0}/tests/test_parity.py +71 -0
- {getstack-0.14.0 → getstack-0.15.0}/.gitignore +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/CLAUDE.md +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/LICENSE +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/README.md +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/scripts/verify_sign_claim_crosslang.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/__init__.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/__init__.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/audit.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/credentials.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/delivery_methods.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/detector_config.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/dropoffs.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/evidence_packs.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/identity.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/inbound_webhooks.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/intents.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/llm.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/missions.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/notifications.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/partner_tenants.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/passport_session.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/passports.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/proxy.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/reviews.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/scan.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/security_events.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/services.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/skills.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/_async/team.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/agent_auth.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/audit.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/auth.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/browser_bootstrap.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/client.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/credentials.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/delivery_methods.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/detector_config.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/dropoffs.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/errors.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/evidence_packs.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/identity.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/inbound_webhooks.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/intents.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/llm.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/missions.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/notifications.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/partner_tenants.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/passport_header_verify.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/passport_session.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/passports.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/proxy.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/py.typed +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/reviews.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/scan.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/security_events.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/services.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/sign_claim.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/skills.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/team.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/src/getstack/verify_offline.py +0 -0
- {getstack-0.14.0 → getstack-0.15.0}/tests/test_sign_claim.py +0 -0
|
@@ -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
|
|
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
|
|
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
|