helm-sdk 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,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: helm-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for HELM — fail-closed tool calling for AI agents
5
+ Author-email: Mindburn Labs <oss@mindburn.org>
6
+ License: BSL-1.1
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.25.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7.0; extra == "dev"
12
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
13
+ Requires-Dist: mypy>=1.0; extra == "dev"
14
+ Requires-Dist: types-requests; extra == "dev"
15
+
16
+ # HELM SDK — Python
17
+
18
+ Typed Python client for the HELM kernel API. One dependency: `httpx`.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install helm-sdk
24
+ ```
25
+
26
+ ## Quick Example
27
+
28
+ ```python
29
+ from helm_sdk import HelmClient, HelmApiError, ChatCompletionRequest, ChatMessage
30
+
31
+ helm = HelmClient(base_url="http://localhost:8080")
32
+
33
+ # OpenAI-compatible chat (tool calls governed by HELM)
34
+ try:
35
+ res = helm.chat_completions(ChatCompletionRequest(
36
+ model="gpt-4",
37
+ messages=[ChatMessage(role="user", content="List files in /tmp")],
38
+ ))
39
+ print(res.choices[0].message.content)
40
+ except HelmApiError as e:
41
+ print(f"Denied: {e.reason_code}") # e.g. DENY_TOOL_NOT_FOUND
42
+
43
+ # Export + verify evidence pack
44
+ pack = helm.export_evidence()
45
+ result = helm.verify_evidence(pack)
46
+ print(result.verdict) # PASS
47
+
48
+ # Conformance
49
+ from helm_sdk import ConformanceRequest
50
+ conf = helm.conformance_run(ConformanceRequest(level="L2"))
51
+ print(conf.verdict, conf.gates, "gates")
52
+ ```
53
+
54
+ ## API
55
+
56
+ | Method | Endpoint |
57
+ |--------|----------|
58
+ | `chat_completions(req)` | `POST /v1/chat/completions` |
59
+ | `approve_intent(req)` | `POST /api/v1/kernel/approve` |
60
+ | `list_sessions()` | `GET /api/v1/proofgraph/sessions` |
61
+ | `get_receipts(session_id)` | `GET /api/v1/proofgraph/sessions/{id}/receipts` |
62
+ | `export_evidence(session_id?)` | `POST /api/v1/evidence/export` |
63
+ | `verify_evidence(bundle)` | `POST /api/v1/evidence/verify` |
64
+ | `replay_verify(bundle)` | `POST /api/v1/replay/verify` |
65
+ | `conformance_run(req)` | `POST /api/v1/conformance/run` |
66
+ | `health()` | `GET /healthz` |
67
+ | `version()` | `GET /version` |
68
+
69
+ ## Error Handling
70
+
71
+ All errors raise `HelmApiError` with a typed `reason_code`:
72
+ ```python
73
+ try: helm.chat_completions(req)
74
+ except HelmApiError as e: print(e.reason_code)
75
+ ```
@@ -0,0 +1,60 @@
1
+ # HELM SDK — Python
2
+
3
+ Typed Python client for the HELM kernel API. One dependency: `httpx`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install helm-sdk
9
+ ```
10
+
11
+ ## Quick Example
12
+
13
+ ```python
14
+ from helm_sdk import HelmClient, HelmApiError, ChatCompletionRequest, ChatMessage
15
+
16
+ helm = HelmClient(base_url="http://localhost:8080")
17
+
18
+ # OpenAI-compatible chat (tool calls governed by HELM)
19
+ try:
20
+ res = helm.chat_completions(ChatCompletionRequest(
21
+ model="gpt-4",
22
+ messages=[ChatMessage(role="user", content="List files in /tmp")],
23
+ ))
24
+ print(res.choices[0].message.content)
25
+ except HelmApiError as e:
26
+ print(f"Denied: {e.reason_code}") # e.g. DENY_TOOL_NOT_FOUND
27
+
28
+ # Export + verify evidence pack
29
+ pack = helm.export_evidence()
30
+ result = helm.verify_evidence(pack)
31
+ print(result.verdict) # PASS
32
+
33
+ # Conformance
34
+ from helm_sdk import ConformanceRequest
35
+ conf = helm.conformance_run(ConformanceRequest(level="L2"))
36
+ print(conf.verdict, conf.gates, "gates")
37
+ ```
38
+
39
+ ## API
40
+
41
+ | Method | Endpoint |
42
+ |--------|----------|
43
+ | `chat_completions(req)` | `POST /v1/chat/completions` |
44
+ | `approve_intent(req)` | `POST /api/v1/kernel/approve` |
45
+ | `list_sessions()` | `GET /api/v1/proofgraph/sessions` |
46
+ | `get_receipts(session_id)` | `GET /api/v1/proofgraph/sessions/{id}/receipts` |
47
+ | `export_evidence(session_id?)` | `POST /api/v1/evidence/export` |
48
+ | `verify_evidence(bundle)` | `POST /api/v1/evidence/verify` |
49
+ | `replay_verify(bundle)` | `POST /api/v1/replay/verify` |
50
+ | `conformance_run(req)` | `POST /api/v1/conformance/run` |
51
+ | `health()` | `GET /healthz` |
52
+ | `version()` | `GET /version` |
53
+
54
+ ## Error Handling
55
+
56
+ All errors raise `HelmApiError` with a typed `reason_code`:
57
+ ```python
58
+ try: helm.chat_completions(req)
59
+ except HelmApiError as e: print(e.reason_code)
60
+ ```
@@ -0,0 +1,28 @@
1
+ """HELM SDK for Python."""
2
+
3
+ from .client import HelmClient, HelmApiError
4
+ from .types_gen import (
5
+ ApprovalRequest,
6
+ ChatCompletionRequest,
7
+ ChatCompletionResponse,
8
+ ConformanceRequest,
9
+ ConformanceResult,
10
+ Receipt,
11
+ Session,
12
+ VerificationResult,
13
+ VersionInfo,
14
+ )
15
+
16
+ __all__ = [
17
+ "HelmClient",
18
+ "HelmApiError",
19
+ "ApprovalRequest",
20
+ "ChatCompletionRequest",
21
+ "ChatCompletionResponse",
22
+ "ConformanceRequest",
23
+ "ConformanceResult",
24
+ "Receipt",
25
+ "Session",
26
+ "VerificationResult",
27
+ "VersionInfo",
28
+ ]
@@ -0,0 +1,156 @@
1
+ """HELM SDK — Python Client
2
+
3
+ Typed client for HELM kernel API. Minimal deps (httpx).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import asdict
9
+ from typing import Any, Optional
10
+
11
+ import httpx
12
+
13
+ from .types_gen import (
14
+ ApprovalRequest,
15
+ ChatCompletionRequest,
16
+ ChatCompletionResponse,
17
+ ConformanceRequest,
18
+ ConformanceResult,
19
+ Receipt,
20
+ Session,
21
+ VerificationResult,
22
+ VersionInfo,
23
+ )
24
+
25
+
26
+ class HelmApiError(Exception):
27
+ """Raised when the HELM API returns a non-2xx response."""
28
+
29
+ def __init__(self, status: int, message: str, reason_code: str, details: Any = None):
30
+ super().__init__(message)
31
+ self.status = status
32
+ self.reason_code = reason_code
33
+ self.details = details
34
+
35
+
36
+ class HelmClient:
37
+ """Typed client for HELM kernel API."""
38
+
39
+ def __init__(
40
+ self,
41
+ base_url: str = "http://localhost:8080",
42
+ api_key: Optional[str] = None,
43
+ timeout: float = 30.0,
44
+ ):
45
+ self.base_url = base_url.rstrip("/")
46
+ headers: dict[str, str] = {"Content-Type": "application/json"}
47
+ if api_key:
48
+ headers["Authorization"] = f"Bearer {api_key}"
49
+ self._client = httpx.Client(
50
+ base_url=self.base_url,
51
+ headers=headers,
52
+ timeout=timeout,
53
+ )
54
+
55
+ def close(self) -> None:
56
+ self._client.close()
57
+
58
+ def __enter__(self) -> "HelmClient":
59
+ return self
60
+
61
+ def __exit__(self, *args: Any) -> None:
62
+ self.close()
63
+
64
+ def _check(self, resp: httpx.Response) -> None:
65
+ if resp.status_code >= 400:
66
+ try:
67
+ body = resp.json()
68
+ err = body.get("error", {})
69
+ raise HelmApiError(
70
+ status=resp.status_code,
71
+ message=err.get("message", resp.text),
72
+ reason_code=err.get("reason_code", "ERROR_INTERNAL"),
73
+ details=err.get("details"),
74
+ )
75
+ except (ValueError, KeyError):
76
+ raise HelmApiError(
77
+ status=resp.status_code,
78
+ message=resp.text,
79
+ reason_code="ERROR_INTERNAL",
80
+ )
81
+
82
+ # ── OpenAI Proxy ────────────────────────────────
83
+ def chat_completions(self, req: ChatCompletionRequest) -> ChatCompletionResponse:
84
+ resp = self._client.post("/v1/chat/completions", json=asdict(req))
85
+ self._check(resp)
86
+ data = resp.json()
87
+ return ChatCompletionResponse(**{k: data.get(k) for k in ChatCompletionResponse.__dataclass_fields__})
88
+
89
+ # ── Approval Ceremony ───────────────────────────
90
+ def approve_intent(self, req: ApprovalRequest) -> Receipt:
91
+ resp = self._client.post("/api/v1/kernel/approve", json=asdict(req))
92
+ self._check(resp)
93
+ return Receipt(**resp.json())
94
+
95
+ # ── ProofGraph ──────────────────────────────────
96
+ def list_sessions(self, limit: int = 50, offset: int = 0) -> list[Session]:
97
+ resp = self._client.get(f"/api/v1/proofgraph/sessions?limit={limit}&offset={offset}")
98
+ self._check(resp)
99
+ return [Session(**s) for s in resp.json()]
100
+
101
+ def get_receipts(self, session_id: str) -> list[Receipt]:
102
+ resp = self._client.get(f"/api/v1/proofgraph/sessions/{session_id}/receipts")
103
+ self._check(resp)
104
+ return [Receipt(**r) for r in resp.json()]
105
+
106
+ def get_receipt(self, receipt_hash: str) -> Receipt:
107
+ resp = self._client.get(f"/api/v1/proofgraph/receipts/{receipt_hash}")
108
+ self._check(resp)
109
+ return Receipt(**resp.json())
110
+
111
+ # ── Evidence ────────────────────────────────────
112
+ def export_evidence(self, session_id: Optional[str] = None) -> bytes:
113
+ resp = self._client.post(
114
+ "/api/v1/evidence/export",
115
+ json={"session_id": session_id, "format": "tar.gz"},
116
+ )
117
+ self._check(resp)
118
+ return resp.content
119
+
120
+ def verify_evidence(self, bundle: bytes) -> VerificationResult:
121
+ resp = self._client.post(
122
+ "/api/v1/evidence/verify",
123
+ files={"bundle": ("pack.tar.gz", bundle, "application/octet-stream")},
124
+ )
125
+ self._check(resp)
126
+ return VerificationResult(**resp.json())
127
+
128
+ def replay_verify(self, bundle: bytes) -> VerificationResult:
129
+ resp = self._client.post(
130
+ "/api/v1/replay/verify",
131
+ files={"bundle": ("pack.tar.gz", bundle, "application/octet-stream")},
132
+ )
133
+ self._check(resp)
134
+ return VerificationResult(**resp.json())
135
+
136
+ # ── Conformance ─────────────────────────────────
137
+ def conformance_run(self, req: ConformanceRequest) -> ConformanceResult:
138
+ resp = self._client.post("/api/v1/conformance/run", json=asdict(req))
139
+ self._check(resp)
140
+ return ConformanceResult(**resp.json())
141
+
142
+ def get_conformance_report(self, report_id: str) -> ConformanceResult:
143
+ resp = self._client.get(f"/api/v1/conformance/reports/{report_id}")
144
+ self._check(resp)
145
+ return ConformanceResult(**resp.json())
146
+
147
+ # ── System ──────────────────────────────────────
148
+ def health(self) -> dict[str, str]:
149
+ resp = self._client.get("/healthz")
150
+ self._check(resp)
151
+ return resp.json()
152
+
153
+ def version(self) -> VersionInfo:
154
+ resp = self._client.get("/version")
155
+ self._check(resp)
156
+ return VersionInfo(**resp.json())
@@ -0,0 +1,173 @@
1
+ # AUTO-GENERATED from api/openapi/helm.openapi.yaml — DO NOT EDIT
2
+ # Regenerate: bash scripts/sdk/gen.sh
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ # ── Reason Codes ──────────────────────────────────────
11
+ REASON_CODES = [
12
+ "ALLOW",
13
+ "DENY_TOOL_NOT_FOUND",
14
+ "DENY_SCHEMA_MISMATCH",
15
+ "DENY_OUTPUT_DRIFT",
16
+ "DENY_BUDGET_EXCEEDED",
17
+ "DENY_APPROVAL_REQUIRED",
18
+ "DENY_APPROVAL_TIMEOUT",
19
+ "DENY_SANDBOX_TRAP",
20
+ "DENY_GAS_EXHAUSTION",
21
+ "DENY_TIME_LIMIT",
22
+ "DENY_MEMORY_LIMIT",
23
+ "DENY_POLICY_VIOLATION",
24
+ "DENY_TRUST_KEY_REVOKED",
25
+ "DENY_IDEMPOTENCY_DUPLICATE",
26
+ "ERROR_INTERNAL",
27
+ ]
28
+
29
+
30
+ @dataclass
31
+ class HelmErrorDetail:
32
+ message: str = ""
33
+ type: str = ""
34
+ code: str = ""
35
+ reason_code: str = ""
36
+ details: Optional[Dict[str, Any]] = None
37
+
38
+
39
+ @dataclass
40
+ class ChatMessage:
41
+ role: str = ""
42
+ content: str = ""
43
+ tool_call_id: Optional[str] = None
44
+
45
+
46
+ @dataclass
47
+ class ToolFunction:
48
+ name: str = ""
49
+ description: str = ""
50
+ parameters: Optional[Dict[str, Any]] = None
51
+
52
+
53
+ @dataclass
54
+ class Tool:
55
+ type: str = "function"
56
+ function: Optional[ToolFunction] = None
57
+
58
+
59
+ @dataclass
60
+ class ChatCompletionRequest:
61
+ model: str = ""
62
+ messages: List[ChatMessage] = field(default_factory=list)
63
+ tools: Optional[List[Tool]] = None
64
+ temperature: Optional[float] = None
65
+ max_tokens: Optional[int] = None
66
+ stream: bool = False
67
+
68
+
69
+ @dataclass
70
+ class ToolCall:
71
+ id: str = ""
72
+ type: str = ""
73
+ function: Optional[Dict[str, str]] = None
74
+
75
+
76
+ @dataclass
77
+ class ChoiceMessage:
78
+ role: str = ""
79
+ content: Optional[str] = None
80
+ tool_calls: Optional[List[ToolCall]] = None
81
+
82
+
83
+ @dataclass
84
+ class Choice:
85
+ index: int = 0
86
+ message: Optional[ChoiceMessage] = None
87
+ finish_reason: str = ""
88
+
89
+
90
+ @dataclass
91
+ class Usage:
92
+ prompt_tokens: int = 0
93
+ completion_tokens: int = 0
94
+ total_tokens: int = 0
95
+
96
+
97
+ @dataclass
98
+ class ChatCompletionResponse:
99
+ id: str = ""
100
+ object: str = "chat.completion"
101
+ created: int = 0
102
+ model: str = ""
103
+ choices: List[Choice] = field(default_factory=list)
104
+ usage: Optional[Usage] = None
105
+
106
+
107
+ @dataclass
108
+ class ApprovalRequest:
109
+ intent_hash: str = ""
110
+ signature_b64: str = ""
111
+ public_key_b64: str = ""
112
+ challenge_response: Optional[str] = None
113
+
114
+
115
+ @dataclass
116
+ class Receipt:
117
+ receipt_id: str = ""
118
+ decision_id: str = ""
119
+ effect_id: str = ""
120
+ status: str = ""
121
+ reason_code: str = ""
122
+ output_hash: str = ""
123
+ blob_hash: str = ""
124
+ prev_hash: str = ""
125
+ lamport_clock: int = 0
126
+ signature: str = ""
127
+ timestamp: str = ""
128
+ principal: str = ""
129
+
130
+
131
+ @dataclass
132
+ class Session:
133
+ session_id: str = ""
134
+ created_at: str = ""
135
+ receipt_count: int = 0
136
+ last_lamport_clock: int = 0
137
+
138
+
139
+ @dataclass
140
+ class ExportRequest:
141
+ session_id: Optional[str] = None
142
+ format: str = "tar.gz"
143
+
144
+
145
+ @dataclass
146
+ class VerificationResult:
147
+ verdict: str = ""
148
+ checks: Optional[Dict[str, str]] = None
149
+ errors: List[str] = field(default_factory=list)
150
+
151
+
152
+ @dataclass
153
+ class ConformanceRequest:
154
+ level: str = "L1"
155
+ profile: str = "full"
156
+
157
+
158
+ @dataclass
159
+ class ConformanceResult:
160
+ report_id: str = ""
161
+ level: str = ""
162
+ verdict: str = ""
163
+ gates: int = 0
164
+ failed: int = 0
165
+ details: Optional[Dict[str, str]] = None
166
+
167
+
168
+ @dataclass
169
+ class VersionInfo:
170
+ version: str = ""
171
+ commit: str = ""
172
+ build_time: str = ""
173
+ go_version: str = ""
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: helm-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for HELM — fail-closed tool calling for AI agents
5
+ Author-email: Mindburn Labs <oss@mindburn.org>
6
+ License: BSL-1.1
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.25.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7.0; extra == "dev"
12
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
13
+ Requires-Dist: mypy>=1.0; extra == "dev"
14
+ Requires-Dist: types-requests; extra == "dev"
15
+
16
+ # HELM SDK — Python
17
+
18
+ Typed Python client for the HELM kernel API. One dependency: `httpx`.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install helm-sdk
24
+ ```
25
+
26
+ ## Quick Example
27
+
28
+ ```python
29
+ from helm_sdk import HelmClient, HelmApiError, ChatCompletionRequest, ChatMessage
30
+
31
+ helm = HelmClient(base_url="http://localhost:8080")
32
+
33
+ # OpenAI-compatible chat (tool calls governed by HELM)
34
+ try:
35
+ res = helm.chat_completions(ChatCompletionRequest(
36
+ model="gpt-4",
37
+ messages=[ChatMessage(role="user", content="List files in /tmp")],
38
+ ))
39
+ print(res.choices[0].message.content)
40
+ except HelmApiError as e:
41
+ print(f"Denied: {e.reason_code}") # e.g. DENY_TOOL_NOT_FOUND
42
+
43
+ # Export + verify evidence pack
44
+ pack = helm.export_evidence()
45
+ result = helm.verify_evidence(pack)
46
+ print(result.verdict) # PASS
47
+
48
+ # Conformance
49
+ from helm_sdk import ConformanceRequest
50
+ conf = helm.conformance_run(ConformanceRequest(level="L2"))
51
+ print(conf.verdict, conf.gates, "gates")
52
+ ```
53
+
54
+ ## API
55
+
56
+ | Method | Endpoint |
57
+ |--------|----------|
58
+ | `chat_completions(req)` | `POST /v1/chat/completions` |
59
+ | `approve_intent(req)` | `POST /api/v1/kernel/approve` |
60
+ | `list_sessions()` | `GET /api/v1/proofgraph/sessions` |
61
+ | `get_receipts(session_id)` | `GET /api/v1/proofgraph/sessions/{id}/receipts` |
62
+ | `export_evidence(session_id?)` | `POST /api/v1/evidence/export` |
63
+ | `verify_evidence(bundle)` | `POST /api/v1/evidence/verify` |
64
+ | `replay_verify(bundle)` | `POST /api/v1/replay/verify` |
65
+ | `conformance_run(req)` | `POST /api/v1/conformance/run` |
66
+ | `health()` | `GET /healthz` |
67
+ | `version()` | `GET /version` |
68
+
69
+ ## Error Handling
70
+
71
+ All errors raise `HelmApiError` with a typed `reason_code`:
72
+ ```python
73
+ try: helm.chat_completions(req)
74
+ except HelmApiError as e: print(e.reason_code)
75
+ ```
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ helm_sdk/__init__.py
4
+ helm_sdk/client.py
5
+ helm_sdk/types_gen.py
6
+ helm_sdk.egg-info/PKG-INFO
7
+ helm_sdk.egg-info/SOURCES.txt
8
+ helm_sdk.egg-info/dependency_links.txt
9
+ helm_sdk.egg-info/requires.txt
10
+ helm_sdk.egg-info/top_level.txt
11
+ tests/test_client.py
@@ -0,0 +1,7 @@
1
+ httpx>=0.25.0
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ ruff>=0.1.0
6
+ mypy>=1.0
7
+ types-requests
@@ -0,0 +1 @@
1
+ helm_sdk
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "helm-sdk"
3
+ version = "0.1.0"
4
+ description = "Python SDK for HELM — fail-closed tool calling for AI agents"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "BSL-1.1" }
8
+ authors = [{ name = "Mindburn Labs", email = "oss@mindburn.org" }]
9
+ dependencies = ["httpx>=0.25.0"]
10
+
11
+ [build-system]
12
+ requires = ["setuptools>=68.0"]
13
+ build-backend = "setuptools.build_meta"
14
+
15
+ [tool.setuptools.packages.find]
16
+ include = ["helm_sdk*"]
17
+
18
+ [project.optional-dependencies]
19
+ dev = ["pytest>=7.0", "ruff>=0.1.0", "mypy>=1.0", "types-requests"]
20
+
21
+ [tool.pytest.ini_options]
22
+ testpaths = ["tests"]
23
+ python_files = ["test_*.py"]
24
+
25
+ [tool.ruff]
26
+ line-length = 88
27
+ target-version = "py39"
28
+
29
+ [tool.mypy]
30
+ python_version = "3.9"
31
+ strict = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,261 @@
1
+ """HELM Python SDK — Unit Tests
2
+
3
+ Tests for HelmClient, HelmApiError, and generated types.
4
+ Uses unittest.mock to mock httpx.Client.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from unittest.mock import MagicMock, patch
11
+ from dataclasses import asdict
12
+
13
+ import pytest
14
+
15
+ from helm_sdk.client import HelmClient, HelmApiError
16
+ from helm_sdk.types_gen import (
17
+ ApprovalRequest,
18
+ ChatCompletionRequest,
19
+ ChatMessage,
20
+ ConformanceRequest,
21
+ ConformanceResult,
22
+ Receipt,
23
+ Session,
24
+ VerificationResult,
25
+ VersionInfo,
26
+ REASON_CODES,
27
+ )
28
+
29
+
30
+ # ── Helpers ──────────────────────────────────────────────
31
+
32
+ def mock_response(status_code: int = 200, json_data: object = None, content: bytes = b"") -> MagicMock:
33
+ """Create a mock httpx.Response."""
34
+ resp = MagicMock()
35
+ resp.status_code = status_code
36
+ resp.json.return_value = json_data if json_data is not None else {}
37
+ resp.text = json.dumps(json_data) if json_data else ""
38
+ resp.content = content
39
+ return resp
40
+
41
+
42
+ RECEIPT_DATA = {
43
+ "receipt_id": "r1",
44
+ "decision_id": "d1",
45
+ "effect_id": "e1",
46
+ "status": "APPROVED",
47
+ "reason_code": "ALLOW",
48
+ "output_hash": "h",
49
+ "blob_hash": "b",
50
+ "prev_hash": "p",
51
+ "lamport_clock": 1,
52
+ "signature": "s",
53
+ "timestamp": "2026-01-01T00:00:00Z",
54
+ "principal": "pr",
55
+ }
56
+
57
+
58
+ # ── HelmApiError ─────────────────────────────────────────
59
+
60
+ class TestHelmApiError:
61
+ def test_stores_status_and_reason(self) -> None:
62
+ err = HelmApiError(403, "denied", "DENY_POLICY_VIOLATION", {"policy": "no-writes"})
63
+ assert err.status == 403
64
+ assert err.reason_code == "DENY_POLICY_VIOLATION"
65
+ assert err.details == {"policy": "no-writes"}
66
+ assert str(err) == "denied"
67
+
68
+ def test_str_representation(self) -> None:
69
+ err = HelmApiError(500, "internal error", "ERROR_INTERNAL")
70
+ assert "internal error" in str(err)
71
+
72
+
73
+ # ── HelmClient Constructor ───────────────────────────────
74
+
75
+ class TestHelmClientConstructor:
76
+ def test_strips_trailing_slash(self) -> None:
77
+ client = HelmClient(base_url="http://localhost:8080/")
78
+ assert client.base_url == "http://localhost:8080"
79
+
80
+ def test_default_base_url(self) -> None:
81
+ client = HelmClient()
82
+ assert client.base_url == "http://localhost:8080"
83
+
84
+ def test_context_manager(self) -> None:
85
+ with HelmClient() as client:
86
+ assert client is not None
87
+
88
+
89
+ # ── Chat Completions ─────────────────────────────────────
90
+
91
+ class TestChatCompletions:
92
+ @patch("helm_sdk.client.httpx.Client")
93
+ def test_posts_to_correct_endpoint(self, mock_client_cls: MagicMock) -> None:
94
+ mock_client = mock_client_cls.return_value
95
+ mock_client.post.return_value = mock_response(200, {
96
+ "id": "chatcmpl-1",
97
+ "object": "chat.completion",
98
+ "created": 1,
99
+ "model": "gpt-4",
100
+ "choices": [],
101
+ })
102
+
103
+ client = HelmClient(base_url="http://h")
104
+ req = ChatCompletionRequest(model="gpt-4", messages=[ChatMessage(role="user", content="hi")])
105
+ result = client.chat_completions(req)
106
+
107
+ mock_client.post.assert_called_once()
108
+ call_args = mock_client.post.call_args
109
+ assert call_args[0][0] == "/v1/chat/completions"
110
+ assert result.id == "chatcmpl-1"
111
+
112
+
113
+ # ── Approve Intent ───────────────────────────────────────
114
+
115
+ class TestApproveIntent:
116
+ @patch("helm_sdk.client.httpx.Client")
117
+ def test_posts_approval_request(self, mock_client_cls: MagicMock) -> None:
118
+ mock_client = mock_client_cls.return_value
119
+ mock_client.post.return_value = mock_response(200, RECEIPT_DATA)
120
+
121
+ client = HelmClient(base_url="http://h")
122
+ req = ApprovalRequest(intent_hash="abc", signature_b64="s", public_key_b64="pk")
123
+ result = client.approve_intent(req)
124
+
125
+ mock_client.post.assert_called_once()
126
+ assert result.receipt_id == "r1"
127
+ assert result.status == "APPROVED"
128
+
129
+
130
+ # ── ProofGraph ───────────────────────────────────────────
131
+
132
+ class TestProofGraph:
133
+ @patch("helm_sdk.client.httpx.Client")
134
+ def test_list_sessions(self, mock_client_cls: MagicMock) -> None:
135
+ mock_client = mock_client_cls.return_value
136
+ mock_client.get.return_value = mock_response(200, [
137
+ {"session_id": "s1", "created_at": "t", "receipt_count": 1, "last_lamport_clock": 1},
138
+ ])
139
+
140
+ client = HelmClient(base_url="http://h")
141
+ result = client.list_sessions(10, 5)
142
+
143
+ mock_client.get.assert_called_once()
144
+ assert len(result) == 1
145
+ assert result[0].session_id == "s1"
146
+
147
+ @patch("helm_sdk.client.httpx.Client")
148
+ def test_get_receipts(self, mock_client_cls: MagicMock) -> None:
149
+ mock_client = mock_client_cls.return_value
150
+ mock_client.get.return_value = mock_response(200, [RECEIPT_DATA])
151
+
152
+ client = HelmClient(base_url="http://h")
153
+ result = client.get_receipts("sess-1")
154
+
155
+ assert len(result) == 1
156
+ assert result[0].receipt_id == "r1"
157
+
158
+ @patch("helm_sdk.client.httpx.Client")
159
+ def test_get_receipt(self, mock_client_cls: MagicMock) -> None:
160
+ mock_client = mock_client_cls.return_value
161
+ mock_client.get.return_value = mock_response(200, RECEIPT_DATA)
162
+
163
+ client = HelmClient(base_url="http://h")
164
+ result = client.get_receipt("hash-abc")
165
+ assert result.receipt_id == "r1"
166
+
167
+
168
+ # ── Error Handling ───────────────────────────────────────
169
+
170
+ class TestErrorHandling:
171
+ @patch("helm_sdk.client.httpx.Client")
172
+ def test_raises_helm_api_error_on_4xx(self, mock_client_cls: MagicMock) -> None:
173
+ mock_client = mock_client_cls.return_value
174
+ mock_client.get.return_value = mock_response(422, {
175
+ "error": {
176
+ "message": "bad schema",
177
+ "type": "invalid_request",
178
+ "code": "ERR",
179
+ "reason_code": "DENY_SCHEMA_MISMATCH",
180
+ }
181
+ })
182
+
183
+ client = HelmClient(base_url="http://h")
184
+ with pytest.raises(HelmApiError) as exc_info:
185
+ client.health()
186
+
187
+ assert exc_info.value.status == 422
188
+ assert exc_info.value.reason_code == "DENY_SCHEMA_MISMATCH"
189
+
190
+ @patch("helm_sdk.client.httpx.Client")
191
+ def test_raises_on_malformed_error_body(self, mock_client_cls: MagicMock) -> None:
192
+ mock_client = mock_client_cls.return_value
193
+ resp = MagicMock()
194
+ resp.status_code = 500
195
+ resp.json.side_effect = ValueError("no JSON")
196
+ resp.text = "Internal Server Error"
197
+ mock_client.get.return_value = resp
198
+
199
+ client = HelmClient(base_url="http://h")
200
+ with pytest.raises(HelmApiError) as exc_info:
201
+ client.health()
202
+
203
+ assert exc_info.value.status == 500
204
+ assert exc_info.value.reason_code == "ERROR_INTERNAL"
205
+
206
+
207
+ # ── System Endpoints ─────────────────────────────────────
208
+
209
+ class TestSystemEndpoints:
210
+ @patch("helm_sdk.client.httpx.Client")
211
+ def test_health(self, mock_client_cls: MagicMock) -> None:
212
+ mock_client = mock_client_cls.return_value
213
+ mock_client.get.return_value = mock_response(200, {"status": "ok", "version": "0.1.0"})
214
+
215
+ client = HelmClient(base_url="http://h")
216
+ result = client.health()
217
+ assert result["status"] == "ok"
218
+
219
+ @patch("helm_sdk.client.httpx.Client")
220
+ def test_version(self, mock_client_cls: MagicMock) -> None:
221
+ mock_client = mock_client_cls.return_value
222
+ mock_client.get.return_value = mock_response(200, {
223
+ "version": "0.1.0",
224
+ "commit": "abc123",
225
+ "build_time": "2026-01-01T00:00:00Z",
226
+ "go_version": "1.24",
227
+ })
228
+
229
+ client = HelmClient(base_url="http://h")
230
+ result = client.version()
231
+ assert result.version == "0.1.0"
232
+ assert result.commit == "abc123"
233
+
234
+
235
+ # ── Generated Types ──────────────────────────────────────
236
+
237
+ class TestGeneratedTypes:
238
+ def test_reason_codes_cover_all_variants(self) -> None:
239
+ assert "ALLOW" in REASON_CODES
240
+ assert "ERROR_INTERNAL" in REASON_CODES
241
+ assert len(REASON_CODES) == 15
242
+
243
+ def test_receipt_dataclass(self) -> None:
244
+ r = Receipt(**RECEIPT_DATA)
245
+ assert r.receipt_id == "r1"
246
+ d = asdict(r)
247
+ assert d["status"] == "APPROVED"
248
+
249
+ def test_conformance_request_defaults(self) -> None:
250
+ req = ConformanceRequest()
251
+ assert req.level == "L1"
252
+ assert req.profile == "full"
253
+
254
+ def test_verification_result(self) -> None:
255
+ vr = VerificationResult(
256
+ verdict="PASS",
257
+ checks={"signatures": "PASS", "causal_chain": "PASS"},
258
+ errors=[],
259
+ )
260
+ assert vr.verdict == "PASS"
261
+ assert len(vr.errors) == 0