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.
- helm_sdk-0.1.0/PKG-INFO +75 -0
- helm_sdk-0.1.0/README.md +60 -0
- helm_sdk-0.1.0/helm_sdk/__init__.py +28 -0
- helm_sdk-0.1.0/helm_sdk/client.py +156 -0
- helm_sdk-0.1.0/helm_sdk/types_gen.py +173 -0
- helm_sdk-0.1.0/helm_sdk.egg-info/PKG-INFO +75 -0
- helm_sdk-0.1.0/helm_sdk.egg-info/SOURCES.txt +11 -0
- helm_sdk-0.1.0/helm_sdk.egg-info/dependency_links.txt +1 -0
- helm_sdk-0.1.0/helm_sdk.egg-info/requires.txt +7 -0
- helm_sdk-0.1.0/helm_sdk.egg-info/top_level.txt +1 -0
- helm_sdk-0.1.0/pyproject.toml +31 -0
- helm_sdk-0.1.0/setup.cfg +4 -0
- helm_sdk-0.1.0/tests/test_client.py +261 -0
helm_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
helm_sdk-0.1.0/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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
|
helm_sdk-0.1.0/setup.cfg
ADDED
|
@@ -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
|