prosader 0.1.0__py3-none-any.whl
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.
- prosader/__init__.py +378 -0
- prosader-0.1.0.dist-info/METADATA +102 -0
- prosader-0.1.0.dist-info/RECORD +5 -0
- prosader-0.1.0.dist-info/WHEEL +5 -0
- prosader-0.1.0.dist-info/top_level.txt +1 -0
prosader/__init__.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Prosader Python SDK — guard AI agent tool calls with a Prosader gateway.
|
|
2
|
+
|
|
3
|
+
Every guarded call is evaluated against your Prosader policy and produces a
|
|
4
|
+
signed, tamper-evident audit receipt, whatever the outcome.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
import prosader
|
|
9
|
+
|
|
10
|
+
client = prosader.Client(
|
|
11
|
+
gateway_url="https://gateway.example.com:8443",
|
|
12
|
+
api_key="psk-live-...", # or PROSADER_API_KEY
|
|
13
|
+
agent_id="billing-agent", # or PROSADER_AGENT_ID
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Explicit check before executing a tool yourself:
|
|
17
|
+
decision = client.check("bash", parameters={"command": "ls /tmp"})
|
|
18
|
+
if decision.allowed:
|
|
19
|
+
... # run the tool
|
|
20
|
+
|
|
21
|
+
# Or wrap a function so the check happens automatically:
|
|
22
|
+
@client.wrap("send_email")
|
|
23
|
+
def send_email(to, subject, body): ...
|
|
24
|
+
|
|
25
|
+
send_email(to="a@b.com", subject="hi", body="...") # raises prosader.Denied if blocked
|
|
26
|
+
|
|
27
|
+
The SDK is intentionally dependency-free (Python standard library only).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import functools
|
|
33
|
+
import inspect
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import ssl
|
|
37
|
+
import urllib.error
|
|
38
|
+
import urllib.request
|
|
39
|
+
import uuid
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from datetime import datetime, timezone
|
|
42
|
+
from typing import Any, Callable, Dict, Optional
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Client",
|
|
46
|
+
"Decision",
|
|
47
|
+
"ProsaderError",
|
|
48
|
+
"Denied",
|
|
49
|
+
"BillingError",
|
|
50
|
+
"GatewayError",
|
|
51
|
+
"configure",
|
|
52
|
+
"check",
|
|
53
|
+
"wrap",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
__version__ = "0.1.0"
|
|
57
|
+
|
|
58
|
+
_DEFAULT_TIMEOUT_S = 35 # slightly above the gateway's 30 s request budget
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Results and errors ────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Decision:
|
|
66
|
+
"""The gateway's verdict for one tool call."""
|
|
67
|
+
|
|
68
|
+
allowed: bool
|
|
69
|
+
outcome: str # "ALLOW" | "DENY" | "STEP_UP"
|
|
70
|
+
reason: str = ""
|
|
71
|
+
rule_id: str = ""
|
|
72
|
+
receipt_id: str = ""
|
|
73
|
+
latency_ms: int = 0
|
|
74
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ProsaderError(Exception):
|
|
78
|
+
"""Base class for all SDK errors."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Denied(ProsaderError):
|
|
82
|
+
"""Raised by wrapped functions when the gateway blocks the call."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, decision: Decision):
|
|
85
|
+
self.decision = decision
|
|
86
|
+
msg = f"blocked by Prosader ({decision.outcome})"
|
|
87
|
+
if decision.rule_id:
|
|
88
|
+
msg += f" rule={decision.rule_id}"
|
|
89
|
+
if decision.reason:
|
|
90
|
+
msg += f": {decision.reason}"
|
|
91
|
+
super().__init__(msg)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class BillingError(ProsaderError):
|
|
95
|
+
"""Raised on HTTP 402 — subscription inactive or decision quota exhausted."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, message: str, status: str = "", code: str = ""):
|
|
98
|
+
self.status = status
|
|
99
|
+
self.code = code
|
|
100
|
+
super().__init__(message)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class GatewayError(ProsaderError):
|
|
104
|
+
"""Raised when the gateway is unreachable or returns an unexpected response.
|
|
105
|
+
|
|
106
|
+
Prosader is fail-closed: treat this as a denial unless you have a
|
|
107
|
+
considered reason not to.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── Client ────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Client:
|
|
115
|
+
"""A connection to one Prosader gateway for one agent.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
gateway_url: Base URL of the gateway mediator (e.g.
|
|
119
|
+
``https://gateway.example.com:8443``). Falls back to the
|
|
120
|
+
``PROSADER_GATEWAY_URL`` environment variable.
|
|
121
|
+
api_key: Prosader API key (``psk-...``). Falls back to
|
|
122
|
+
``PROSADER_API_KEY``. Optional; without it the gateway attributes
|
|
123
|
+
activity to the default tenant.
|
|
124
|
+
agent_id: Stable identifier for this agent, shown in the dashboard
|
|
125
|
+
and stamped on every receipt. Falls back to ``PROSADER_AGENT_ID``.
|
|
126
|
+
session_id: Groups related calls into one session. A fresh ID is
|
|
127
|
+
generated per Client if omitted.
|
|
128
|
+
verify_tls: Set to ``False`` only for gateways using a self-signed
|
|
129
|
+
certificate in development.
|
|
130
|
+
timeout: Per-request timeout in seconds.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
gateway_url: Optional[str] = None,
|
|
136
|
+
api_key: Optional[str] = None,
|
|
137
|
+
agent_id: Optional[str] = None,
|
|
138
|
+
session_id: Optional[str] = None,
|
|
139
|
+
verify_tls: bool = True,
|
|
140
|
+
timeout: float = _DEFAULT_TIMEOUT_S,
|
|
141
|
+
):
|
|
142
|
+
gateway_url = gateway_url or os.environ.get("PROSADER_GATEWAY_URL", "")
|
|
143
|
+
if not gateway_url:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"gateway_url is required (or set PROSADER_GATEWAY_URL)")
|
|
146
|
+
self.gateway_url = gateway_url.rstrip("/")
|
|
147
|
+
self.api_key = api_key or os.environ.get("PROSADER_API_KEY", "")
|
|
148
|
+
self.agent_id = agent_id or os.environ.get("PROSADER_AGENT_ID", "")
|
|
149
|
+
if not self.agent_id:
|
|
150
|
+
raise ValueError("agent_id is required (or set PROSADER_AGENT_ID)")
|
|
151
|
+
self.session_id = session_id or f"sdk-{uuid.uuid4().hex[:16]}"
|
|
152
|
+
self.timeout = timeout
|
|
153
|
+
self._ssl_context: Optional[ssl.SSLContext] = None
|
|
154
|
+
if not verify_tls:
|
|
155
|
+
self._ssl_context = ssl.create_default_context()
|
|
156
|
+
self._ssl_context.check_hostname = False
|
|
157
|
+
self._ssl_context.verify_mode = ssl.CERT_NONE
|
|
158
|
+
|
|
159
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
def check(
|
|
162
|
+
self,
|
|
163
|
+
tool_name: str,
|
|
164
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
165
|
+
payload: Any = None,
|
|
166
|
+
) -> Decision:
|
|
167
|
+
"""Ask the gateway whether this tool call is allowed.
|
|
168
|
+
|
|
169
|
+
Sends a check-only request: the gateway evaluates policy and writes a
|
|
170
|
+
signed receipt but does not forward the call anywhere — executing the
|
|
171
|
+
tool remains the caller's responsibility.
|
|
172
|
+
|
|
173
|
+
Returns a :class:`Decision`. Raises :class:`BillingError` on quota or
|
|
174
|
+
subscription problems and :class:`GatewayError` when the gateway is
|
|
175
|
+
unreachable (fail closed: do not run the tool in that case).
|
|
176
|
+
"""
|
|
177
|
+
body = {
|
|
178
|
+
"session_id": self.session_id,
|
|
179
|
+
"request_id": f"req-{uuid.uuid4().hex}",
|
|
180
|
+
"tool_name": tool_name,
|
|
181
|
+
"agent_id": self.agent_id,
|
|
182
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
183
|
+
}
|
|
184
|
+
if parameters:
|
|
185
|
+
body["parameters"] = {str(k): _stringify(v) for k, v in parameters.items()}
|
|
186
|
+
if payload is not None:
|
|
187
|
+
body["payload"] = payload
|
|
188
|
+
|
|
189
|
+
status, headers, resp = self._post_action(body)
|
|
190
|
+
|
|
191
|
+
if status == 402:
|
|
192
|
+
raise BillingError(
|
|
193
|
+
resp.get("error", "payment required"),
|
|
194
|
+
status=resp.get("status", ""),
|
|
195
|
+
code=resp.get("code", ""),
|
|
196
|
+
)
|
|
197
|
+
if status == 400:
|
|
198
|
+
raise ProsaderError(
|
|
199
|
+
f"gateway rejected request: {resp.get('error', 'bad request')}")
|
|
200
|
+
if status not in (200, 429):
|
|
201
|
+
raise GatewayError(f"unexpected gateway response: HTTP {status}")
|
|
202
|
+
|
|
203
|
+
outcome = headers.get("X-Prosader-Decision", "")
|
|
204
|
+
if not outcome:
|
|
205
|
+
outcome = "ALLOW" if resp.get("allowed") else "DENY"
|
|
206
|
+
try:
|
|
207
|
+
latency_ms = int(headers.get("X-Prosader-Latency-Ms", "0"))
|
|
208
|
+
except ValueError:
|
|
209
|
+
latency_ms = 0
|
|
210
|
+
|
|
211
|
+
return Decision(
|
|
212
|
+
allowed=bool(resp.get("allowed", False)),
|
|
213
|
+
outcome=outcome,
|
|
214
|
+
reason=resp.get("reason", ""),
|
|
215
|
+
rule_id=resp.get("rule_id", ""),
|
|
216
|
+
receipt_id=resp.get("receipt_id", headers.get("X-Prosader-Receipt-ID", "")),
|
|
217
|
+
latency_ms=latency_ms,
|
|
218
|
+
raw=resp,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def wrap(self, tool_name: Optional[str] = None) -> Callable:
|
|
222
|
+
"""Decorator: check with the gateway before every call to the function.
|
|
223
|
+
|
|
224
|
+
The function's arguments are sent as the tool call's parameters so
|
|
225
|
+
policy rules can match on them. Blocked calls raise :class:`Denied`
|
|
226
|
+
instead of executing.
|
|
227
|
+
|
|
228
|
+
::
|
|
229
|
+
|
|
230
|
+
@client.wrap("bash")
|
|
231
|
+
def run_bash(command): ...
|
|
232
|
+
"""
|
|
233
|
+
def decorator(fn: Callable) -> Callable:
|
|
234
|
+
name = tool_name or fn.__name__
|
|
235
|
+
try:
|
|
236
|
+
sig = inspect.signature(fn)
|
|
237
|
+
except (TypeError, ValueError):
|
|
238
|
+
sig = None
|
|
239
|
+
|
|
240
|
+
@functools.wraps(fn)
|
|
241
|
+
def guarded(*args: Any, **kwargs: Any) -> Any:
|
|
242
|
+
params: Dict[str, Any] = {}
|
|
243
|
+
if sig is not None:
|
|
244
|
+
try:
|
|
245
|
+
bound = sig.bind(*args, **kwargs)
|
|
246
|
+
bound.apply_defaults()
|
|
247
|
+
params = dict(bound.arguments)
|
|
248
|
+
except TypeError:
|
|
249
|
+
params = dict(kwargs)
|
|
250
|
+
else:
|
|
251
|
+
params = dict(kwargs)
|
|
252
|
+
|
|
253
|
+
decision = self.check(name, parameters=params)
|
|
254
|
+
if not decision.allowed:
|
|
255
|
+
raise Denied(decision)
|
|
256
|
+
return fn(*args, **kwargs)
|
|
257
|
+
|
|
258
|
+
return guarded
|
|
259
|
+
|
|
260
|
+
return decorator
|
|
261
|
+
|
|
262
|
+
# ── Internals ─────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
def _post_action(self, body: Dict[str, Any]):
|
|
265
|
+
"""POST the action to the gateway; returns (status, headers, json_body)."""
|
|
266
|
+
data = json.dumps(body).encode("utf-8")
|
|
267
|
+
req = urllib.request.Request(
|
|
268
|
+
self.gateway_url + "/v1/action",
|
|
269
|
+
data=data,
|
|
270
|
+
method="POST",
|
|
271
|
+
headers={
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
"X-Prosader-Check-Only": "true",
|
|
274
|
+
"User-Agent": f"prosader-python/{__version__}",
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
if self.api_key:
|
|
278
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
with urllib.request.urlopen(
|
|
282
|
+
req, timeout=self.timeout, context=self._ssl_context
|
|
283
|
+
) as r:
|
|
284
|
+
return r.status, dict(r.headers), _parse_json(r.read())
|
|
285
|
+
except urllib.error.HTTPError as e:
|
|
286
|
+
# Non-2xx responses (400/402/429/...) still carry a JSON body.
|
|
287
|
+
with e:
|
|
288
|
+
return e.code, dict(e.headers), _parse_json(e.read())
|
|
289
|
+
except (urllib.error.URLError, OSError, TimeoutError) as e:
|
|
290
|
+
raise GatewayError(f"gateway unreachable: {e}") from e
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _stringify(v: Any) -> str:
|
|
294
|
+
"""Render a parameter value the way policy rules expect to match it."""
|
|
295
|
+
if isinstance(v, str):
|
|
296
|
+
return v
|
|
297
|
+
try:
|
|
298
|
+
return json.dumps(v)
|
|
299
|
+
except (TypeError, ValueError):
|
|
300
|
+
return str(v)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _parse_json(raw: bytes) -> Dict[str, Any]:
|
|
304
|
+
try:
|
|
305
|
+
parsed = json.loads(raw)
|
|
306
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
307
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
308
|
+
return {}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ── Module-level convenience (single global client) ───────────────────────────
|
|
312
|
+
|
|
313
|
+
_default_client: Optional[Client] = None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def configure(**kwargs: Any) -> Client:
|
|
317
|
+
"""Create and install the module-level default client.
|
|
318
|
+
|
|
319
|
+
Accepts the same arguments as :class:`Client`. After calling this,
|
|
320
|
+
:func:`prosader.check` and :func:`prosader.wrap` use the configured
|
|
321
|
+
client.
|
|
322
|
+
"""
|
|
323
|
+
global _default_client
|
|
324
|
+
_default_client = Client(**kwargs)
|
|
325
|
+
return _default_client
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _require_default() -> Client:
|
|
329
|
+
global _default_client
|
|
330
|
+
if _default_client is None:
|
|
331
|
+
# Attempt implicit configuration from environment variables.
|
|
332
|
+
_default_client = Client()
|
|
333
|
+
return _default_client
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def check(
|
|
337
|
+
tool_name: str,
|
|
338
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
339
|
+
payload: Any = None,
|
|
340
|
+
) -> Decision:
|
|
341
|
+
"""Module-level :meth:`Client.check` using the default client."""
|
|
342
|
+
return _require_default().check(tool_name, parameters=parameters, payload=payload)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def wrap(tool_name: Optional[str] = None) -> Callable:
|
|
346
|
+
"""Module-level :meth:`Client.wrap` using the default client.
|
|
347
|
+
|
|
348
|
+
The client is resolved at call time, so ``@prosader.wrap()`` can decorate
|
|
349
|
+
functions before :func:`configure` runs.
|
|
350
|
+
"""
|
|
351
|
+
def decorator(fn: Callable) -> Callable:
|
|
352
|
+
try:
|
|
353
|
+
sig = inspect.signature(fn)
|
|
354
|
+
except (TypeError, ValueError):
|
|
355
|
+
sig = None
|
|
356
|
+
name = tool_name or fn.__name__
|
|
357
|
+
|
|
358
|
+
@functools.wraps(fn)
|
|
359
|
+
def guarded(*args: Any, **kwargs: Any) -> Any:
|
|
360
|
+
client = _require_default()
|
|
361
|
+
params: Dict[str, Any] = {}
|
|
362
|
+
if sig is not None:
|
|
363
|
+
try:
|
|
364
|
+
bound = sig.bind(*args, **kwargs)
|
|
365
|
+
bound.apply_defaults()
|
|
366
|
+
params = dict(bound.arguments)
|
|
367
|
+
except TypeError:
|
|
368
|
+
params = dict(kwargs)
|
|
369
|
+
else:
|
|
370
|
+
params = dict(kwargs)
|
|
371
|
+
decision = client.check(name, parameters=params)
|
|
372
|
+
if not decision.allowed:
|
|
373
|
+
raise Denied(decision)
|
|
374
|
+
return fn(*args, **kwargs)
|
|
375
|
+
|
|
376
|
+
return guarded
|
|
377
|
+
|
|
378
|
+
return decorator
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prosader
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Guard AI agent tool calls with a Prosader runtime security gateway
|
|
5
|
+
Author: Prosader
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://prosader.com
|
|
8
|
+
Keywords: ai-agents,security,audit,compliance,mcp
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Security
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Prosader Python SDK
|
|
17
|
+
|
|
18
|
+
Guard AI agent tool calls with a [Prosader](https://prosader.com) runtime
|
|
19
|
+
security gateway. Every guarded call is evaluated against your policy and
|
|
20
|
+
produces a signed, tamper-evident audit receipt — evidence you can hand to an
|
|
21
|
+
auditor and verify independently with `prosader-verify`.
|
|
22
|
+
|
|
23
|
+
No dependencies: the SDK uses only the Python standard library (3.9+).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install prosader
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import prosader
|
|
35
|
+
|
|
36
|
+
client = prosader.Client(
|
|
37
|
+
gateway_url="https://gateway.example.com:8443",
|
|
38
|
+
api_key="psk-live-...",
|
|
39
|
+
agent_id="billing-agent",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Option 1 — explicit check before you execute a tool:
|
|
43
|
+
decision = client.check("bash", parameters={"command": "rm -rf /tmp/cache"})
|
|
44
|
+
if decision.allowed:
|
|
45
|
+
run_command(...)
|
|
46
|
+
else:
|
|
47
|
+
print(f"blocked by rule {decision.rule_id}: {decision.reason}")
|
|
48
|
+
# decision.receipt_id — signed audit receipt either way
|
|
49
|
+
|
|
50
|
+
# Option 2 — wrap a function; blocked calls raise prosader.Denied:
|
|
51
|
+
@client.wrap("send_email")
|
|
52
|
+
def send_email(to, subject, body):
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
send_email(to="cfo@example.com", subject="Invoice", body="...")
|
|
57
|
+
except prosader.Denied as e:
|
|
58
|
+
print(e.decision.reason)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or configure once and use module-level helpers:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
prosader.configure(gateway_url="https://...", api_key="psk-...", agent_id="my-agent")
|
|
65
|
+
|
|
66
|
+
@prosader.wrap("bash")
|
|
67
|
+
def run_bash(command): ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Configuration falls back to environment variables: `PROSADER_GATEWAY_URL`,
|
|
71
|
+
`PROSADER_API_KEY`, `PROSADER_AGENT_ID`.
|
|
72
|
+
|
|
73
|
+
## Outcomes
|
|
74
|
+
|
|
75
|
+
| Outcome | `decision.allowed` | Meaning |
|
|
76
|
+
|-----------|--------------------|----------------------------------------------------|
|
|
77
|
+
| `ALLOW` | `True` | Proceed. |
|
|
78
|
+
| `DENY` | `False` | Blocked by policy (`rule_id`, `reason` populated). |
|
|
79
|
+
| `STEP_UP` | `False` | Held for human approval in the Prosader dashboard. |
|
|
80
|
+
|
|
81
|
+
Errors:
|
|
82
|
+
|
|
83
|
+
- `prosader.Denied` — raised by wrapped functions when blocked.
|
|
84
|
+
- `prosader.BillingError` — subscription inactive or decision quota exhausted (HTTP 402).
|
|
85
|
+
- `prosader.GatewayError` — gateway unreachable. Prosader is fail-closed:
|
|
86
|
+
treat this as a denial.
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- Function arguments of wrapped functions are sent as the tool call's
|
|
91
|
+
`parameters` so policy rules can match on them (e.g. deny `bash` where
|
|
92
|
+
`command` contains `rm -rf`).
|
|
93
|
+
- For development gateways with a self-signed certificate, pass
|
|
94
|
+
`verify_tls=False`. Never do this in production.
|
|
95
|
+
- Requires a gateway new enough to support check-only mode
|
|
96
|
+
(`X-Prosader-Check-Only`), July 2026 or later.
|
|
97
|
+
|
|
98
|
+
## Running tests
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python -m unittest discover -s tests -v
|
|
102
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
prosader/__init__.py,sha256=paxeiWzp9hBpTqQO3gDh9PPLHy6fDxumzXVtXI-mlcE,13278
|
|
2
|
+
prosader-0.1.0.dist-info/METADATA,sha256=cXpjWh2RSj5nm6L0arDeavrDR9kkV9Hjt9Bmkp9m0HU,3201
|
|
3
|
+
prosader-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
prosader-0.1.0.dist-info/top_level.txt,sha256=l1ncwE0n8ZgCBMsHBzkzlKmkZnnur0SR1uE4km8pMyw,9
|
|
5
|
+
prosader-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prosader
|