sovest-firewall 0.1.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.
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: sovest-firewall
3
+ Version: 0.1.0
4
+ Summary: Stdlib-only Python client for the Sovest Agent Honesty Firewall HTTP surface (claim <= evidence, action <= authority).
5
+ Author: Sovest
6
+ License: Proprietary
7
+ Project-URL: API contract, https://example.invalid/AGENT_FIREWALL_API_v1
8
+ Keywords: agent,firewall,honesty,claim-ceiling,guardrail
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+
12
+ # sovest-firewall (Python SDK)
13
+
14
+ Stdlib-only Python client for the **Sovest Agent Honesty Firewall** HTTP surface.
15
+ It calls one pre-action gate before your agent does anything consequential.
16
+
17
+ > Honest claim ceiling: the firewall enforces a **policy** — claim ≤ evidence, action ≤ authority.
18
+ > It does **not** make an agent correct, safe, or aligned, and it is not a guarantee of safe outcomes.
19
+ > It blocks unsupported claims from becoming actions and makes admitted ones scoped, verifiable,
20
+ > disputable, and correctable.
21
+
22
+ **Status:** v0.1.0. Not yet published to PyPI. Dependency-free (uses `urllib` from the standard library).
23
+ Requires Python >= 3.9.
24
+
25
+ ## Install
26
+
27
+ From this directory (editable install):
28
+
29
+ ```bash
30
+ pip install -e .
31
+ ```
32
+
33
+ That exposes the `sovest_firewall` package importably. No third-party packages are pulled in.
34
+
35
+ ## Usage
36
+
37
+ ```python
38
+ from sovest_firewall import FirewallClient, NotAdmitted, guard
39
+
40
+ fw = FirewallClient("http://127.0.0.1:8787")
41
+
42
+ # 1. Get a verdict (never raises on a refusal — the refusal IS the answer):
43
+ v = fw.check(
44
+ claim_text="Refund approved for order 4821",
45
+ claim_type="LIVE_PROCESS",
46
+ scope="refunds:order_4821",
47
+ requested_action="REFUND_APPROVE",
48
+ action_impact="consequential",
49
+ evidence_refs=["sha256:order_record", "sha256:customer_chatlog"],
50
+ authority_refs=["refund_policy_v2"],
51
+ )
52
+ print(v["verdict"], v["admitted"])
53
+
54
+ # 2. Admit + record (returns the receipt, raises NotAdmitted on refusal):
55
+ try:
56
+ receipt = fw.admit(
57
+ claim_text="Refund approved for order 4821",
58
+ scope="refunds:order_4821",
59
+ requested_action="REFUND_APPROVE",
60
+ action_impact="consequential",
61
+ evidence_refs=["sha256:order_record"],
62
+ authority_refs=["refund_policy_v2"],
63
+ )
64
+ print(receipt["admissibility_id"])
65
+ except NotAdmitted as e:
66
+ print("refused:", e.verdict["reason"])
67
+
68
+ # 3. Guard a callable — runs it ONLY if the firewall admits the action:
69
+ def do_refund():
70
+ ... # the real side effect
71
+ result = guard(
72
+ do_refund, fw,
73
+ claim_text="Refund approved for order 4821",
74
+ scope="refunds:order_4821",
75
+ requested_action="REFUND_APPROVE",
76
+ evidence_refs=["sha256:order_record"],
77
+ authority_refs=["refund_policy_v2"],
78
+ )
79
+ ```
80
+
81
+ ## API surface (stable for v0.1.0)
82
+
83
+ | symbol | purpose |
84
+ |---|---|
85
+ | `FirewallClient(base_url, timeout)` | client bound to a firewall server |
86
+ | `.check(**fields) -> dict` | get a verdict; does not raise on refusal |
87
+ | `.admit(**fields) -> dict` | admit + record; raises `NotAdmitted` on refusal |
88
+ | `.build_request(path, fields)` | build the `urllib` request without sending (offline tests) |
89
+ | `guard(action, client, **fields)` | run `action` only if admitted |
90
+ | `NotAdmitted` | exception carrying the full refusal verdict (`.verdict`) |
91
+ | `FirewallError` | transport / unexpected-status error |
92
+
93
+ ## Running the examples and checks
94
+
95
+ - `examples/quickstart.py` — runnable end-to-end when a firewall server is up
96
+ (start `python3 ../../server.py` first), prints the verdict.
97
+ - `tests/test_offline.py` — offline check; constructs a client and validates request
98
+ serialization (URL, method, headers, JSON body) with no network call.
99
+
100
+ See `SDK_PY_CHECK.txt` for captured proof of the import + offline checks.
@@ -0,0 +1,89 @@
1
+ # sovest-firewall (Python SDK)
2
+
3
+ Stdlib-only Python client for the **Sovest Agent Honesty Firewall** HTTP surface.
4
+ It calls one pre-action gate before your agent does anything consequential.
5
+
6
+ > Honest claim ceiling: the firewall enforces a **policy** — claim ≤ evidence, action ≤ authority.
7
+ > It does **not** make an agent correct, safe, or aligned, and it is not a guarantee of safe outcomes.
8
+ > It blocks unsupported claims from becoming actions and makes admitted ones scoped, verifiable,
9
+ > disputable, and correctable.
10
+
11
+ **Status:** v0.1.0. Not yet published to PyPI. Dependency-free (uses `urllib` from the standard library).
12
+ Requires Python >= 3.9.
13
+
14
+ ## Install
15
+
16
+ From this directory (editable install):
17
+
18
+ ```bash
19
+ pip install -e .
20
+ ```
21
+
22
+ That exposes the `sovest_firewall` package importably. No third-party packages are pulled in.
23
+
24
+ ## Usage
25
+
26
+ ```python
27
+ from sovest_firewall import FirewallClient, NotAdmitted, guard
28
+
29
+ fw = FirewallClient("http://127.0.0.1:8787")
30
+
31
+ # 1. Get a verdict (never raises on a refusal — the refusal IS the answer):
32
+ v = fw.check(
33
+ claim_text="Refund approved for order 4821",
34
+ claim_type="LIVE_PROCESS",
35
+ scope="refunds:order_4821",
36
+ requested_action="REFUND_APPROVE",
37
+ action_impact="consequential",
38
+ evidence_refs=["sha256:order_record", "sha256:customer_chatlog"],
39
+ authority_refs=["refund_policy_v2"],
40
+ )
41
+ print(v["verdict"], v["admitted"])
42
+
43
+ # 2. Admit + record (returns the receipt, raises NotAdmitted on refusal):
44
+ try:
45
+ receipt = fw.admit(
46
+ claim_text="Refund approved for order 4821",
47
+ scope="refunds:order_4821",
48
+ requested_action="REFUND_APPROVE",
49
+ action_impact="consequential",
50
+ evidence_refs=["sha256:order_record"],
51
+ authority_refs=["refund_policy_v2"],
52
+ )
53
+ print(receipt["admissibility_id"])
54
+ except NotAdmitted as e:
55
+ print("refused:", e.verdict["reason"])
56
+
57
+ # 3. Guard a callable — runs it ONLY if the firewall admits the action:
58
+ def do_refund():
59
+ ... # the real side effect
60
+ result = guard(
61
+ do_refund, fw,
62
+ claim_text="Refund approved for order 4821",
63
+ scope="refunds:order_4821",
64
+ requested_action="REFUND_APPROVE",
65
+ evidence_refs=["sha256:order_record"],
66
+ authority_refs=["refund_policy_v2"],
67
+ )
68
+ ```
69
+
70
+ ## API surface (stable for v0.1.0)
71
+
72
+ | symbol | purpose |
73
+ |---|---|
74
+ | `FirewallClient(base_url, timeout)` | client bound to a firewall server |
75
+ | `.check(**fields) -> dict` | get a verdict; does not raise on refusal |
76
+ | `.admit(**fields) -> dict` | admit + record; raises `NotAdmitted` on refusal |
77
+ | `.build_request(path, fields)` | build the `urllib` request without sending (offline tests) |
78
+ | `guard(action, client, **fields)` | run `action` only if admitted |
79
+ | `NotAdmitted` | exception carrying the full refusal verdict (`.verdict`) |
80
+ | `FirewallError` | transport / unexpected-status error |
81
+
82
+ ## Running the examples and checks
83
+
84
+ - `examples/quickstart.py` — runnable end-to-end when a firewall server is up
85
+ (start `python3 ../../server.py` first), prints the verdict.
86
+ - `tests/test_offline.py` — offline check; constructs a client and validates request
87
+ serialization (URL, method, headers, JSON body) with no network call.
88
+
89
+ See `SDK_PY_CHECK.txt` for captured proof of the import + offline checks.
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sovest-firewall"
7
+ version = "0.1.0"
8
+ description = "Stdlib-only Python client for the Sovest Agent Honesty Firewall HTTP surface (claim <= evidence, action <= authority)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Proprietary" }
12
+ authors = [{ name = "Sovest" }]
13
+ keywords = ["agent", "firewall", "honesty", "claim-ceiling", "guardrail"]
14
+ # Stdlib only (urllib) — no required runtime dependencies.
15
+ dependencies = []
16
+
17
+ [project.urls]
18
+ "API contract" = "https://example.invalid/AGENT_FIREWALL_API_v1"
19
+
20
+ [tool.setuptools]
21
+ packages = ["sovest_firewall"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """Sovest Agent Honesty Firewall — Python client SDK (stdlib only).
2
+
3
+ Public API surface (stable for v0.1.0):
4
+
5
+ from sovest_firewall import FirewallClient, NotAdmitted, FirewallError, guard
6
+
7
+ See README.md for usage. This package is dependency-free (urllib).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from .client import FirewallClient, FirewallError, NotAdmitted, guard
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __all__ = [
16
+ "FirewallClient",
17
+ "FirewallError",
18
+ "NotAdmitted",
19
+ "guard",
20
+ "__version__",
21
+ ]
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """Sovest Agent Honesty Firewall — tiny Python client SDK (stdlib only).
3
+
4
+ Talks to the firewall HTTP server (server.py) using urllib. No external dependencies.
5
+
6
+ Usage:
7
+
8
+ from sovest_firewall import FirewallClient, NotAdmitted, guard
9
+
10
+ fw = FirewallClient("http://127.0.0.1:8787")
11
+
12
+ # Get a verdict (never raises on a refusal — the refusal IS the answer):
13
+ v = fw.check(
14
+ claim_text="Refund approved for order 4821",
15
+ claim_type="LIVE_PROCESS",
16
+ scope="refunds:order_4821",
17
+ requested_action="REFUND_APPROVE",
18
+ evidence_refs=["sha256:order", "sha256:chatlog"],
19
+ authority_refs=["refund_policy_v2"],
20
+ )
21
+ print(v["verdict"])
22
+
23
+ # Admit + record (returns the receipt, raises NotAdmitted on refusal):
24
+ receipt = fw.admit(**fields)
25
+
26
+ # Guard a callable: run it ONLY if the firewall admits the action.
27
+ result = guard(do_refund, fw, claim_text=..., requested_action="REFUND_APPROVE", ...)
28
+
29
+ Honest claim ceiling: this enforces a policy (claim <= evidence, action <= authority).
30
+ It does NOT make an agent correct, safe, or aligned. It blocks unsupported claims from
31
+ becoming actions and makes admitted ones scoped, verifiable, disputable, and correctable.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import urllib.error
37
+ import urllib.request
38
+ from typing import Any, Callable, Dict
39
+
40
+
41
+ class FirewallError(RuntimeError):
42
+ """Transport / server error talking to the firewall."""
43
+
44
+
45
+ class NotAdmitted(Exception):
46
+ """Raised when an action was refused. Carries the full refusal verdict."""
47
+
48
+ def __init__(self, verdict: Dict[str, Any]):
49
+ self.verdict = verdict
50
+ msg = "%s: %s" % (verdict.get("verdict", "REFUSED"), verdict.get("reason", ""))
51
+ super().__init__(msg)
52
+
53
+
54
+ class FirewallClient:
55
+ def __init__(self, base_url: str = "http://127.0.0.1:8787", timeout: float = 10.0):
56
+ self.base_url = base_url.rstrip("/")
57
+ self.timeout = timeout
58
+
59
+ # ---- serialization (no network) ----
60
+ def build_request(self, path: str, fields: Dict[str, Any]) -> urllib.request.Request:
61
+ """Build the urllib Request for a call WITHOUT sending it.
62
+
63
+ Useful for offline tests that want to assert the URL, method, headers,
64
+ and JSON body the client would send. No network access happens here.
65
+ """
66
+ url = self.base_url + path
67
+ data = json.dumps(fields).encode("utf-8")
68
+ return urllib.request.Request(
69
+ url, data=data, method="POST",
70
+ headers={"Content-Type": "application/json"},
71
+ )
72
+
73
+ # ---- low-level POST ----
74
+ def _post(self, path: str, fields: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
75
+ url = self.base_url + path
76
+ data = json.dumps(fields).encode("utf-8")
77
+ req = urllib.request.Request(
78
+ url, data=data, method="POST",
79
+ headers={"Content-Type": "application/json"},
80
+ )
81
+ try:
82
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
83
+ return resp.status, json.loads(resp.read().decode("utf-8"))
84
+ except urllib.error.HTTPError as e:
85
+ # 400 from /actions/admit carries the refusal verdict — read and return it.
86
+ body = e.read().decode("utf-8")
87
+ try:
88
+ parsed = json.loads(body)
89
+ except json.JSONDecodeError:
90
+ raise FirewallError("HTTP %s: %s" % (e.code, body)) from e
91
+ return e.code, parsed
92
+ except urllib.error.URLError as e:
93
+ raise FirewallError("cannot reach firewall at %s: %s" % (url, e)) from e
94
+
95
+ # ---- public API ----
96
+ def check(self, **fields: Any) -> Dict[str, Any]:
97
+ """Return the verdict dict. Does not raise on a refusal (the refusal is the answer)."""
98
+ status, body = self._post("/claims/check", fields)
99
+ if status != 200:
100
+ raise FirewallError("unexpected status %s from /claims/check: %s" % (status, body))
101
+ return body
102
+
103
+ def admit(self, **fields: Any) -> Dict[str, Any]:
104
+ """Admit + record the action; return the admissibility receipt.
105
+
106
+ Raises NotAdmitted (carrying the refusal verdict) if the action was refused.
107
+ """
108
+ status, body = self._post("/actions/admit", fields)
109
+ if status == 200:
110
+ return body # admissibility receipt
111
+ if status == 400:
112
+ raise NotAdmitted(body)
113
+ raise FirewallError("unexpected status %s from /actions/admit: %s" % (status, body))
114
+
115
+
116
+ def guard(action: Callable[..., Any], client: FirewallClient, *,
117
+ action_args: tuple = (), action_kwargs: Dict[str, Any] | None = None,
118
+ **fields: Any) -> Any:
119
+ """Run `action(*action_args, **action_kwargs)` ONLY if the firewall admits it.
120
+
121
+ The firewall claim fields are passed as keyword args (claim_text, requested_action, ...).
122
+ Raises NotAdmitted before the action runs if it is refused. On admission, the action runs
123
+ and its return value is returned (the receipt is available via client.admit if needed).
124
+ """
125
+ verdict = client.check(**fields)
126
+ if not verdict.get("admitted"):
127
+ raise NotAdmitted(verdict)
128
+ return action(*action_args, **(action_kwargs or {}))
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: sovest-firewall
3
+ Version: 0.1.0
4
+ Summary: Stdlib-only Python client for the Sovest Agent Honesty Firewall HTTP surface (claim <= evidence, action <= authority).
5
+ Author: Sovest
6
+ License: Proprietary
7
+ Project-URL: API contract, https://example.invalid/AGENT_FIREWALL_API_v1
8
+ Keywords: agent,firewall,honesty,claim-ceiling,guardrail
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+
12
+ # sovest-firewall (Python SDK)
13
+
14
+ Stdlib-only Python client for the **Sovest Agent Honesty Firewall** HTTP surface.
15
+ It calls one pre-action gate before your agent does anything consequential.
16
+
17
+ > Honest claim ceiling: the firewall enforces a **policy** — claim ≤ evidence, action ≤ authority.
18
+ > It does **not** make an agent correct, safe, or aligned, and it is not a guarantee of safe outcomes.
19
+ > It blocks unsupported claims from becoming actions and makes admitted ones scoped, verifiable,
20
+ > disputable, and correctable.
21
+
22
+ **Status:** v0.1.0. Not yet published to PyPI. Dependency-free (uses `urllib` from the standard library).
23
+ Requires Python >= 3.9.
24
+
25
+ ## Install
26
+
27
+ From this directory (editable install):
28
+
29
+ ```bash
30
+ pip install -e .
31
+ ```
32
+
33
+ That exposes the `sovest_firewall` package importably. No third-party packages are pulled in.
34
+
35
+ ## Usage
36
+
37
+ ```python
38
+ from sovest_firewall import FirewallClient, NotAdmitted, guard
39
+
40
+ fw = FirewallClient("http://127.0.0.1:8787")
41
+
42
+ # 1. Get a verdict (never raises on a refusal — the refusal IS the answer):
43
+ v = fw.check(
44
+ claim_text="Refund approved for order 4821",
45
+ claim_type="LIVE_PROCESS",
46
+ scope="refunds:order_4821",
47
+ requested_action="REFUND_APPROVE",
48
+ action_impact="consequential",
49
+ evidence_refs=["sha256:order_record", "sha256:customer_chatlog"],
50
+ authority_refs=["refund_policy_v2"],
51
+ )
52
+ print(v["verdict"], v["admitted"])
53
+
54
+ # 2. Admit + record (returns the receipt, raises NotAdmitted on refusal):
55
+ try:
56
+ receipt = fw.admit(
57
+ claim_text="Refund approved for order 4821",
58
+ scope="refunds:order_4821",
59
+ requested_action="REFUND_APPROVE",
60
+ action_impact="consequential",
61
+ evidence_refs=["sha256:order_record"],
62
+ authority_refs=["refund_policy_v2"],
63
+ )
64
+ print(receipt["admissibility_id"])
65
+ except NotAdmitted as e:
66
+ print("refused:", e.verdict["reason"])
67
+
68
+ # 3. Guard a callable — runs it ONLY if the firewall admits the action:
69
+ def do_refund():
70
+ ... # the real side effect
71
+ result = guard(
72
+ do_refund, fw,
73
+ claim_text="Refund approved for order 4821",
74
+ scope="refunds:order_4821",
75
+ requested_action="REFUND_APPROVE",
76
+ evidence_refs=["sha256:order_record"],
77
+ authority_refs=["refund_policy_v2"],
78
+ )
79
+ ```
80
+
81
+ ## API surface (stable for v0.1.0)
82
+
83
+ | symbol | purpose |
84
+ |---|---|
85
+ | `FirewallClient(base_url, timeout)` | client bound to a firewall server |
86
+ | `.check(**fields) -> dict` | get a verdict; does not raise on refusal |
87
+ | `.admit(**fields) -> dict` | admit + record; raises `NotAdmitted` on refusal |
88
+ | `.build_request(path, fields)` | build the `urllib` request without sending (offline tests) |
89
+ | `guard(action, client, **fields)` | run `action` only if admitted |
90
+ | `NotAdmitted` | exception carrying the full refusal verdict (`.verdict`) |
91
+ | `FirewallError` | transport / unexpected-status error |
92
+
93
+ ## Running the examples and checks
94
+
95
+ - `examples/quickstart.py` — runnable end-to-end when a firewall server is up
96
+ (start `python3 ../../server.py` first), prints the verdict.
97
+ - `tests/test_offline.py` — offline check; constructs a client and validates request
98
+ serialization (URL, method, headers, JSON body) with no network call.
99
+
100
+ See `SDK_PY_CHECK.txt` for captured proof of the import + offline checks.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ sovest_firewall/__init__.py
4
+ sovest_firewall/client.py
5
+ sovest_firewall.egg-info/PKG-INFO
6
+ sovest_firewall.egg-info/SOURCES.txt
7
+ sovest_firewall.egg-info/dependency_links.txt
8
+ sovest_firewall.egg-info/top_level.txt
9
+ tests/test_offline.py
@@ -0,0 +1 @@
1
+ sovest_firewall
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Offline unit check — no network access.
3
+
4
+ Constructs a FirewallClient and validates that it serializes a request the way
5
+ the firewall HTTP surface expects (URL, method, headers, JSON body). Uses the
6
+ client's `build_request` helper, which builds the urllib Request without sending.
7
+
8
+ Run directly: python3 tests/test_offline.py
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import sys
14
+
15
+ from sovest_firewall import FirewallClient
16
+
17
+
18
+ def test_serialization() -> None:
19
+ fw = FirewallClient("http://127.0.0.1:8787/") # trailing slash should be stripped
20
+ assert fw.base_url == "http://127.0.0.1:8787", fw.base_url
21
+
22
+ fields = dict(
23
+ claim_text="Refund approved for order 4821",
24
+ claim_type="LIVE_PROCESS",
25
+ scope="refunds:order_4821",
26
+ requested_action="REFUND_APPROVE",
27
+ evidence_refs=["sha256:order_record"],
28
+ authority_refs=["refund_policy_v2"],
29
+ )
30
+
31
+ req = fw.build_request("/claims/check", fields)
32
+
33
+ assert req.full_url == "http://127.0.0.1:8787/claims/check", req.full_url
34
+ assert req.method == "POST", req.method
35
+ assert req.headers.get("Content-type") == "application/json", req.headers
36
+
37
+ body = json.loads(req.data.decode("utf-8"))
38
+ assert body == fields, body
39
+ # round-trips exactly: no fields dropped or renamed
40
+ assert body["requested_action"] == "REFUND_APPROVE"
41
+ assert body["evidence_refs"] == ["sha256:order_record"]
42
+
43
+
44
+ def main() -> int:
45
+ test_serialization()
46
+ print("OK: offline serialization check passed (no network used)")
47
+ return 0
48
+
49
+
50
+ if __name__ == "__main__":
51
+ sys.exit(main())