Skip to content

Commit 0d69bc1

Browse files
committed
Initial commit: CGOS Python SDK
0 parents  commit 0d69bc1

10 files changed

Lines changed: 375 additions & 0 deletions

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# NerveMind CGOS Python SDK
2+
3+
Developer-oriented client for **decision intake (v2)**, **proof validation**, and **execution invoke** (gateway).
4+
5+
## Install (local / editable)
6+
7+
```bash
8+
cd sdks/python
9+
pip install -e .
10+
```
11+
12+
## Quick start
13+
14+
```python
15+
from cgos_sdk import CGOSClient
16+
17+
client = CGOSClient(
18+
base_url="https://cgos-api.example.com",
19+
api_key="your-api-key",
20+
internal_service_token="...", # for verify_proof / invoke_execution
21+
)
22+
23+
out = client.submit_decision(
24+
source_system="core-payments",
25+
sector="banking",
26+
decision_type="limit_increase",
27+
decision_id="dec-001",
28+
context={"amount": 5000},
29+
policy_set="ORGANIZATION_POLICY_V1",
30+
callback_url="https://your-bank.example/callbacks/cgos",
31+
correlation_id="trace-abc",
32+
)
33+
34+
proof_check = client.verify_proof(out.get("proof_id") or "prf_...")
35+
exec_resp = client.invoke_execution(
36+
proof_id="prf_...",
37+
path="/api/v1/payments/transfer",
38+
organization_id="org_123",
39+
http_method="POST",
40+
json_body={"to": "x", "amount": 1},
41+
)
42+
```
43+
44+
## Auth
45+
46+
| Call | Header |
47+
|------|--------|
48+
| Intake v2 | `X-API-Key` or `Authorization: Bearer <api_key>` (matches brain-api) |
49+
| Proof validate / execution | `X-CGOS-Internal-Token` (S2S) |
50+
51+
Optional **bearer JWT** (UI / ops) enables `wait_for_decision()` polling on `GET /api/v1/cgos/decisions/{id}`.
52+
53+
## Reliability
54+
55+
`CGOSClient` supports `timeout_s`, `max_retries`, `Idempotency-Key` on intake (passed as header when provided), and optional `traceparent` / `correlation_id` on every request.
56+
57+
## External attestation
58+
59+
This SDK cannot prove mesh or gateway posture. Pair with SPIFFE/SPIRE, signed policy bundles, and CI validators — see `docs/CGOS_EXTERNAL_ATTESTATION_AND_SDK.md` at repo root.

cgos_sdk/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""NerveMind CGOS Python SDK."""
2+
3+
from cgos_sdk.client import CGOSClient, CGOSError
4+
5+
__all__ = ["CGOSClient", "CGOSError"]
328 Bytes
Binary file not shown.
13.9 KB
Binary file not shown.

cgos_sdk/client.py

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""
2+
CGOS HTTP client — intake v2, proof validate, execution invoke.
3+
4+
Intended ergonomics: submit_decision() → verify_proof() → invoke_execution()
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import logging
10+
import time
11+
from typing import Any, Callable, Dict, List, Optional
12+
13+
import requests
14+
15+
log = logging.getLogger("cgos_sdk")
16+
17+
18+
class CGOSError(Exception):
19+
"""HTTP or contract error from CGOS."""
20+
21+
def __init__(self, message: str, *, status_code: Optional[int] = None, body: Optional[str] = None):
22+
super().__init__(message)
23+
self.status_code = status_code
24+
self.body = body
25+
26+
27+
class CGOSClient:
28+
def __init__(
29+
self,
30+
base_url: str,
31+
*,
32+
api_key: Optional[str] = None,
33+
internal_service_token: Optional[str] = None,
34+
bearer_token: Optional[str] = None,
35+
timeout_s: float = 60.0,
36+
max_retries: int = 2,
37+
session: Optional[requests.Session] = None,
38+
user_agent: str = "nervemind-cgos-sdk/0.1",
39+
trace_hook: Optional[Callable[[str, str, int, float], None]] = None,
40+
):
41+
self.base_url = base_url.rstrip("/")
42+
self.api_key = (api_key or "").strip() or None
43+
self.internal_service_token = (internal_service_token or "").strip() or None
44+
self.bearer_token = (bearer_token or "").strip() or None
45+
self.timeout_s = timeout_s
46+
self.max_retries = max(0, int(max_retries))
47+
self._session = session or requests.Session()
48+
self.user_agent = user_agent
49+
self.trace_hook = trace_hook
50+
51+
def _headers(
52+
self,
53+
*,
54+
for_intake: bool = False,
55+
for_internal: bool = False,
56+
idempotency_key: Optional[str] = None,
57+
correlation_id: Optional[str] = None,
58+
traceparent: Optional[str] = None,
59+
extra: Optional[Dict[str, str]] = None,
60+
) -> Dict[str, str]:
61+
h: Dict[str, str] = {"Content-Type": "application/json", "User-Agent": self.user_agent}
62+
if for_intake and self.api_key:
63+
h["X-API-Key"] = self.api_key
64+
elif for_intake and self.bearer_token:
65+
h["Authorization"] = f"Bearer {self.bearer_token}"
66+
if for_internal:
67+
if self.internal_service_token:
68+
h["X-CGOS-Internal-Token"] = self.internal_service_token
69+
elif self.bearer_token and not for_intake:
70+
h["Authorization"] = f"Bearer {self.bearer_token}"
71+
if idempotency_key:
72+
h["Idempotency-Key"] = idempotency_key
73+
if correlation_id:
74+
h["X-Correlation-ID"] = correlation_id
75+
if traceparent:
76+
h["traceparent"] = traceparent
77+
if extra:
78+
h.update(extra)
79+
return h
80+
81+
def _request(
82+
self,
83+
method: str,
84+
path: str,
85+
*,
86+
json: Any = None,
87+
headers: Optional[Dict[str, str]] = None,
88+
) -> requests.Response:
89+
url = f"{self.base_url}{path}"
90+
hdrs = dict(headers or {})
91+
last_exc: Optional[Exception] = None
92+
for attempt in range(self.max_retries + 1):
93+
t0 = time.perf_counter()
94+
try:
95+
r = self._session.request(
96+
method,
97+
url,
98+
json=json,
99+
headers=hdrs,
100+
timeout=self.timeout_s,
101+
)
102+
elapsed = time.perf_counter() - t0
103+
if self.trace_hook:
104+
self.trace_hook(method, url, r.status_code, elapsed)
105+
if r.status_code >= 500 and attempt < self.max_retries:
106+
time.sleep(0.25 * (2**attempt))
107+
continue
108+
return r
109+
except requests.RequestException as e:
110+
last_exc = e
111+
if attempt < self.max_retries:
112+
time.sleep(0.25 * (2**attempt))
113+
continue
114+
raise CGOSError(f"request failed: {e}") from e
115+
raise CGOSError(f"request failed after retries: {last_exc}")
116+
117+
def _raise_for_status(self, r: requests.Response, ctx: str) -> None:
118+
if r.status_code < 400:
119+
return
120+
body = r.text[:4000] if r.text else ""
121+
raise CGOSError(
122+
f"{ctx} failed: HTTP {r.status_code}",
123+
status_code=r.status_code,
124+
body=body,
125+
)
126+
127+
def submit_decision(
128+
self,
129+
*,
130+
source_system: str,
131+
sector: str,
132+
decision_type: str,
133+
decision_id: str,
134+
context: Dict[str, Any],
135+
policy_set: str,
136+
callback_url: str,
137+
priority: Optional[str] = None,
138+
sla_seconds: Optional[int] = None,
139+
correlation_id: Optional[str] = None,
140+
idempotency_key: Optional[str] = None,
141+
traceparent: Optional[str] = None,
142+
) -> Dict[str, Any]:
143+
"""POST /api/v1/cgos/v2/decisions (EXTERNAL_GOVERNED). Requires API key (or bearer)."""
144+
if not self.api_key and not self.bearer_token:
145+
raise CGOSError("submit_decision requires api_key or bearer_token")
146+
body = {
147+
"decision_class": "EXTERNAL_GOVERNED",
148+
"source_system": source_system,
149+
"sector": sector,
150+
"decision_type": decision_type,
151+
"decision_id": decision_id,
152+
"context": context or {},
153+
"policy_set": policy_set,
154+
"callback_url": callback_url,
155+
"priority": priority,
156+
"sla_seconds": sla_seconds,
157+
"correlation_id": correlation_id,
158+
}
159+
hdrs = self._headers(
160+
for_intake=True,
161+
idempotency_key=idempotency_key,
162+
correlation_id=correlation_id,
163+
traceparent=traceparent,
164+
)
165+
r = self._request("POST", "/api/v1/cgos/v2/decisions", json=body, headers=hdrs)
166+
self._raise_for_status(r, "submit_decision")
167+
return r.json()
168+
169+
def verify_proof(
170+
self,
171+
proof_id: str,
172+
*,
173+
organization_id: Optional[str] = None,
174+
intended_action: Optional[Dict[str, Any]] = None,
175+
traceparent: Optional[str] = None,
176+
) -> Dict[str, Any]:
177+
"""POST /api/v1/cgos/internal/proofs/validate — requires internal token."""
178+
if not self.internal_service_token:
179+
raise CGOSError("verify_proof requires internal_service_token")
180+
body: Dict[str, Any] = {"proof_id": proof_id}
181+
if organization_id:
182+
body["organization_id"] = organization_id
183+
if intended_action is not None:
184+
body["intended_action"] = intended_action
185+
hdrs = self._headers(for_internal=True, traceparent=traceparent)
186+
r = self._request("POST", "/api/v1/cgos/internal/proofs/validate", json=body, headers=hdrs)
187+
self._raise_for_status(r, "verify_proof")
188+
return r.json()
189+
190+
def mint_proof_token(
191+
self,
192+
proof_id: str,
193+
*,
194+
organization_id: Optional[str] = None,
195+
intended_action: Optional[Dict[str, Any]] = None,
196+
traceparent: Optional[str] = None,
197+
) -> Dict[str, Any]:
198+
"""POST /api/v1/cgos/internal/proofs/token — HS256 for core-local checks."""
199+
if not self.internal_service_token:
200+
raise CGOSError("mint_proof_token requires internal_service_token")
201+
body: Dict[str, Any] = {"proof_id": proof_id}
202+
if organization_id:
203+
body["organization_id"] = organization_id
204+
if intended_action is not None:
205+
body["intended_action"] = intended_action
206+
hdrs = self._headers(for_internal=True, traceparent=traceparent)
207+
r = self._request("POST", "/api/v1/cgos/internal/proofs/token", json=body, headers=hdrs)
208+
self._raise_for_status(r, "mint_proof_token")
209+
return r.json()
210+
211+
def invoke_execution(
212+
self,
213+
*,
214+
proof_id: str,
215+
path: str,
216+
organization_id: Optional[str] = None,
217+
http_method: str = "POST",
218+
headers: Optional[Dict[str, str]] = None,
219+
json_body: Optional[Dict[str, Any]] = None,
220+
intended_action: Optional[Dict[str, Any]] = None,
221+
traceparent: Optional[str] = None,
222+
) -> Dict[str, Any]:
223+
"""
224+
POST /api/v1/cgos/execution/invoke — proof-gated forward to bank core.
225+
Prefer internal_service_token + organization_id for gateway-style calls.
226+
"""
227+
if not self.internal_service_token and not self.api_key and not self.bearer_token:
228+
raise CGOSError("invoke_execution requires internal_service_token, api_key, or bearer_token")
229+
body: Dict[str, Any] = {
230+
"proof_id": proof_id,
231+
"path": path,
232+
"http_method": http_method,
233+
"headers": headers,
234+
"json_body": json_body,
235+
"intended_action": intended_action,
236+
}
237+
if organization_id:
238+
body["organization_id"] = organization_id
239+
base = {"traceparent": traceparent} if traceparent else {}
240+
if self.internal_service_token:
241+
hdrs = {**base, "Content-Type": "application/json", "User-Agent": self.user_agent}
242+
hdrs["X-CGOS-Internal-Token"] = self.internal_service_token
243+
elif self.api_key:
244+
hdrs = self._headers(for_intake=True, traceparent=traceparent)
245+
else:
246+
hdrs = {**base, "Content-Type": "application/json", "User-Agent": self.user_agent, "Authorization": f"Bearer {self.bearer_token}"}
247+
r = self._request("POST", "/api/v1/cgos/execution/invoke", json=body, headers=hdrs)
248+
self._raise_for_status(r, "invoke_execution")
249+
return r.json()
250+
251+
def verify_auth(self) -> Dict[str, Any]:
252+
"""GET /api/v1/cgos/decision/auth/verify — API key sanity check."""
253+
if not self.api_key and not self.bearer_token:
254+
raise CGOSError("verify_auth requires api_key or bearer_token")
255+
hdrs = self._headers(for_intake=True)
256+
r = self._request("GET", "/api/v1/cgos/decision/auth/verify", headers=hdrs)
257+
self._raise_for_status(r, "verify_auth")
258+
return r.json()
259+
260+
def get_decision(self, decision_internal_id: str) -> Dict[str, Any]:
261+
"""GET /api/v1/cgos/decisions/{id} — requires admin JWT (bearer_token)."""
262+
if not self.bearer_token:
263+
raise CGOSError("get_decision requires bearer_token (admin UI JWT)")
264+
hdrs = self._headers(for_intake=True)
265+
r = self._request("GET", f"/api/v1/cgos/decisions/{decision_internal_id}", headers=hdrs)
266+
self._raise_for_status(r, "get_decision")
267+
return r.json()
268+
269+
def wait_for_decision(
270+
self,
271+
decision_internal_id: str,
272+
*,
273+
terminal_statuses: Optional[List[str]] = None,
274+
poll_interval_s: float = 2.0,
275+
timeout_s: Optional[float] = None,
276+
) -> Dict[str, Any]:
277+
"""
278+
Poll get_decision until status settles. Requires bearer_token.
279+
External integrators should prefer callback_url instead of polling.
280+
"""
281+
terminal = {s.upper() for s in (terminal_statuses or ["APPROVED", "REJECTED", "CONDITIONAL", "CALLBACK_SENT"])}
282+
deadline = None if timeout_s is None else (time.monotonic() + float(timeout_s))
283+
while True:
284+
d = self.get_decision(decision_internal_id)
285+
st = str(d.get("status") or "").upper()
286+
fs = str(d.get("final_status") or "").upper()
287+
if st in terminal or fs in terminal:
288+
return d
289+
if deadline is not None and time.monotonic() > deadline:
290+
raise CGOSError(f"wait_for_decision timeout after {timeout_s}s")
291+
time.sleep(poll_interval_s)
5.17 KB
Binary file not shown.

dist/nervemind_cgos-0.1.0.tar.gz

4.77 KB
Binary file not shown.
5.2 KB
Binary file not shown.
4.79 KB
Binary file not shown.

pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[build-system]
2+
requires = ["setuptools>=61"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "nervemind-cgos"
7+
version = "0.1.0"
8+
description = "Official CGOS client: intake, proof validation, execution gateway, observability hooks"
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = { text = "Proprietary" }
12+
authors = [{ name = "NerveMind" }]
13+
dependencies = ["requests>=2.28.0,<3"]
14+
15+
[project.optional-dependencies]
16+
dev = ["pytest>=7.0", "responses>=0.25"]
17+
18+
[tool.setuptools.packages.find]
19+
where = ["."]
20+
include = ["cgos_sdk*"]

0 commit comments

Comments
 (0)