iceberg-subzero 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.
- iceberg_subzero-0.1.0/.gitattributes +2 -0
- iceberg_subzero-0.1.0/PKG-INFO +153 -0
- iceberg_subzero-0.1.0/README.md +136 -0
- iceberg_subzero-0.1.0/examples/_bootstrap.py +42 -0
- iceberg_subzero-0.1.0/examples/elements_demo.py +67 -0
- iceberg_subzero-0.1.0/examples/hero_demo.py +84 -0
- iceberg_subzero-0.1.0/examples/vault_loop.py +87 -0
- iceberg_subzero-0.1.0/pyproject.toml +33 -0
- iceberg_subzero-0.1.0/src/subzero/__init__.py +43 -0
- iceberg_subzero-0.1.0/src/subzero/_http.py +195 -0
- iceberg_subzero-0.1.0/src/subzero/admin.py +282 -0
- iceberg_subzero-0.1.0/src/subzero/auth.py +232 -0
- iceberg_subzero-0.1.0/src/subzero/client.py +96 -0
- iceberg_subzero-0.1.0/src/subzero/exceptions.py +43 -0
- iceberg_subzero-0.1.0/src/subzero/members.py +71 -0
- iceberg_subzero-0.1.0/src/subzero/openai.py +29 -0
- iceberg_subzero-0.1.0/src/subzero/proxy.py +49 -0
- iceberg_subzero-0.1.0/src/subzero/types.py +177 -0
- iceberg_subzero-0.1.0/tests/test_admin.py +98 -0
- iceberg_subzero-0.1.0/tests/test_audit.py +46 -0
- iceberg_subzero-0.1.0/tests/test_auth.py +113 -0
- iceberg_subzero-0.1.0/tests/test_client.py +109 -0
- iceberg_subzero-0.1.0/tests/test_proxy.py +60 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iceberg-subzero
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Subzero tokenization vault and LLM proxy
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.28.0
|
|
7
|
+
Provides-Extra: auth
|
|
8
|
+
Requires-Dist: pyotp>=2.9.0; extra == 'auth'
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: openai>=1.0.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: pyotp>=2.9.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: respx>=0.22.0; extra == 'dev'
|
|
14
|
+
Provides-Extra: openai
|
|
15
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Subzero Python SDK
|
|
19
|
+
|
|
20
|
+
Thin Python client for the [Subzero](../api/) tokenization vault and LLM proxy.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd python-sdk
|
|
26
|
+
pip install -e ".[dev,openai,auth]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The optional `auth` extra adds `pyotp` for TOTP code generation during dashboard login.
|
|
30
|
+
|
|
31
|
+
## Two auth planes
|
|
32
|
+
|
|
33
|
+
| Plane | Credential | Use for |
|
|
34
|
+
|-------|------------|---------|
|
|
35
|
+
| **Server integration** | API key (`sz_live_...`) | `tokenize`, `search`, `reveal`, `proxy`, tenant admin after bootstrap |
|
|
36
|
+
| **Dashboard / human** | JWT access token | `auth.*`, `members.*`, `admin.create_tenant`, `admin.get_tenant` |
|
|
37
|
+
|
|
38
|
+
Pass an API key, an access token, or both when constructing the client:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from subzero import SubzeroClient
|
|
42
|
+
|
|
43
|
+
# Vault + proxy (server-side)
|
|
44
|
+
vault = SubzeroClient(api_key="sz_live_...", base_url="http://127.0.0.1:8000")
|
|
45
|
+
|
|
46
|
+
# Dashboard session (platform or tenant admin JWT)
|
|
47
|
+
dashboard = SubzeroClient(access_token="eyJ...", base_url="http://127.0.0.1:8000")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start (vault)
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from subzero import SubzeroClient
|
|
54
|
+
|
|
55
|
+
client = SubzeroClient(
|
|
56
|
+
api_key="sz_live_...",
|
|
57
|
+
base_url="http://127.0.0.1:8000",
|
|
58
|
+
)
|
|
59
|
+
client.ready()
|
|
60
|
+
|
|
61
|
+
token = client.tokenize("SSN", "123-45-6789").token
|
|
62
|
+
value = client.reveal(token).value
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Platform admin login (tenant provisioning)
|
|
66
|
+
|
|
67
|
+
`POST /v1/tenants` requires a platform-admin JWT. Log in with MFA, then create tenants:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from subzero import SubzeroClient
|
|
71
|
+
|
|
72
|
+
client = SubzeroClient(base_url="http://127.0.0.1:8000")
|
|
73
|
+
client.ready()
|
|
74
|
+
|
|
75
|
+
client.auth.login_platform(
|
|
76
|
+
email="admin@iceberg.local",
|
|
77
|
+
password="...",
|
|
78
|
+
totp_code="123456", # or totp_secret="BASE32..." with pip install 'subzero[auth]'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
tenant = client.admin.create_tenant(name="Acme", slug="acme")
|
|
82
|
+
admin_key = tenant.bootstrap_api_key
|
|
83
|
+
|
|
84
|
+
# Switch to tenant admin API key for day-to-day vault/proxy setup
|
|
85
|
+
admin = SubzeroClient(api_key=admin_key, base_url="http://127.0.0.1:8000")
|
|
86
|
+
admin.admin.create_entity_type(tenant.id, name="SSN", deterministic=True)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Tenant member invites and management use `client.members` with a tenant-admin or platform-admin JWT.
|
|
90
|
+
|
|
91
|
+
## API key scopes
|
|
92
|
+
|
|
93
|
+
| Scope | Vault methods | Notes |
|
|
94
|
+
|-------|---------------|-------|
|
|
95
|
+
| `tokenize` | `tokenize`, `search` | No plaintext return |
|
|
96
|
+
| `reveal` | `reveal` | Server-side plaintext reveal; requires matching policy rule |
|
|
97
|
+
| `reveal_grant` | `create_reveal_grant` | Mint browser reveal grants only; requires matching policy rule |
|
|
98
|
+
| `proxy` | `proxy.chat.completions` | In-flight tokenization |
|
|
99
|
+
| `admin` | All of the above + `admin.*` + `delete_token` | Bypasses reveal policy |
|
|
100
|
+
|
|
101
|
+
**Browser reveal:** the iframe calls `POST /v1/browser/reveal` with a server-minted grant. Your BFF uses a **`reveal_grant`** key to call `create_reveal_grant(token, client_public_key_jwk=..., allowed_origin=...)`. Keep **`reveal`** keys for server pipelines that need `client.reveal(token).value`.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
grant = client.create_reveal_grant(
|
|
105
|
+
token,
|
|
106
|
+
client_public_key_jwk=jwk_from_iframe,
|
|
107
|
+
allowed_origin="https://app.yourcompany.com",
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Delete requires admin.** There is no delegatable delete-scoped API key at the HTTP layer. `delete_token()` needs an admin key even though policy rules support a `delete` action at the service layer.
|
|
112
|
+
|
|
113
|
+
## Proxy vs reveal vs detokenize
|
|
114
|
+
|
|
115
|
+
- **Vault reveal:** `client.reveal(token)` — `POST /v1/reveal`, reveal-scoped key + policy
|
|
116
|
+
- **Proxy chat:** `client.proxy.chat.completions(...)` — tokenizes declared patterns in-flight
|
|
117
|
+
- **Proxy detokenize:** pass `detokenize=True` or use OpenAI helper below — governed by reveal policy for the proxy key, not the reveal endpoint
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from subzero import create_openai_client
|
|
121
|
+
|
|
122
|
+
client = create_openai_client(
|
|
123
|
+
api_key="sz_live_...", # Subzero proxy key
|
|
124
|
+
base_url="http://127.0.0.1:8000/v1",
|
|
125
|
+
detokenize=True, # X-Subzero-Detokenize: true via default_headers
|
|
126
|
+
)
|
|
127
|
+
client.chat.completions.create(model="gpt-4o", stream=False, messages=[...])
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
With the API running (`docker compose up` or `uvicorn`):
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
export SUBZERO_PLATFORM_EMAIL=admin@iceberg.local
|
|
136
|
+
export SUBZERO_PLATFORM_PASSWORD=...
|
|
137
|
+
export SUBZERO_TOTP_CODE=123456 # or SUBZERO_TOTP_SECRET / SUBZERO_ACCESS_TOKEN
|
|
138
|
+
|
|
139
|
+
python examples/hero_demo.py
|
|
140
|
+
python examples/vault_loop.py
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Set `SUBZERO_BASE_URL` and `OPENAI_API_KEY` as needed.
|
|
144
|
+
|
|
145
|
+
## Tests
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pytest
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## PyPI
|
|
152
|
+
|
|
153
|
+
Local editable install only for now. `# TODO: twine upload` when ready to publish.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Subzero Python SDK
|
|
2
|
+
|
|
3
|
+
Thin Python client for the [Subzero](../api/) tokenization vault and LLM proxy.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd python-sdk
|
|
9
|
+
pip install -e ".[dev,openai,auth]"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The optional `auth` extra adds `pyotp` for TOTP code generation during dashboard login.
|
|
13
|
+
|
|
14
|
+
## Two auth planes
|
|
15
|
+
|
|
16
|
+
| Plane | Credential | Use for |
|
|
17
|
+
|-------|------------|---------|
|
|
18
|
+
| **Server integration** | API key (`sz_live_...`) | `tokenize`, `search`, `reveal`, `proxy`, tenant admin after bootstrap |
|
|
19
|
+
| **Dashboard / human** | JWT access token | `auth.*`, `members.*`, `admin.create_tenant`, `admin.get_tenant` |
|
|
20
|
+
|
|
21
|
+
Pass an API key, an access token, or both when constructing the client:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from subzero import SubzeroClient
|
|
25
|
+
|
|
26
|
+
# Vault + proxy (server-side)
|
|
27
|
+
vault = SubzeroClient(api_key="sz_live_...", base_url="http://127.0.0.1:8000")
|
|
28
|
+
|
|
29
|
+
# Dashboard session (platform or tenant admin JWT)
|
|
30
|
+
dashboard = SubzeroClient(access_token="eyJ...", base_url="http://127.0.0.1:8000")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start (vault)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from subzero import SubzeroClient
|
|
37
|
+
|
|
38
|
+
client = SubzeroClient(
|
|
39
|
+
api_key="sz_live_...",
|
|
40
|
+
base_url="http://127.0.0.1:8000",
|
|
41
|
+
)
|
|
42
|
+
client.ready()
|
|
43
|
+
|
|
44
|
+
token = client.tokenize("SSN", "123-45-6789").token
|
|
45
|
+
value = client.reveal(token).value
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Platform admin login (tenant provisioning)
|
|
49
|
+
|
|
50
|
+
`POST /v1/tenants` requires a platform-admin JWT. Log in with MFA, then create tenants:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from subzero import SubzeroClient
|
|
54
|
+
|
|
55
|
+
client = SubzeroClient(base_url="http://127.0.0.1:8000")
|
|
56
|
+
client.ready()
|
|
57
|
+
|
|
58
|
+
client.auth.login_platform(
|
|
59
|
+
email="admin@iceberg.local",
|
|
60
|
+
password="...",
|
|
61
|
+
totp_code="123456", # or totp_secret="BASE32..." with pip install 'subzero[auth]'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
tenant = client.admin.create_tenant(name="Acme", slug="acme")
|
|
65
|
+
admin_key = tenant.bootstrap_api_key
|
|
66
|
+
|
|
67
|
+
# Switch to tenant admin API key for day-to-day vault/proxy setup
|
|
68
|
+
admin = SubzeroClient(api_key=admin_key, base_url="http://127.0.0.1:8000")
|
|
69
|
+
admin.admin.create_entity_type(tenant.id, name="SSN", deterministic=True)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Tenant member invites and management use `client.members` with a tenant-admin or platform-admin JWT.
|
|
73
|
+
|
|
74
|
+
## API key scopes
|
|
75
|
+
|
|
76
|
+
| Scope | Vault methods | Notes |
|
|
77
|
+
|-------|---------------|-------|
|
|
78
|
+
| `tokenize` | `tokenize`, `search` | No plaintext return |
|
|
79
|
+
| `reveal` | `reveal` | Server-side plaintext reveal; requires matching policy rule |
|
|
80
|
+
| `reveal_grant` | `create_reveal_grant` | Mint browser reveal grants only; requires matching policy rule |
|
|
81
|
+
| `proxy` | `proxy.chat.completions` | In-flight tokenization |
|
|
82
|
+
| `admin` | All of the above + `admin.*` + `delete_token` | Bypasses reveal policy |
|
|
83
|
+
|
|
84
|
+
**Browser reveal:** the iframe calls `POST /v1/browser/reveal` with a server-minted grant. Your BFF uses a **`reveal_grant`** key to call `create_reveal_grant(token, client_public_key_jwk=..., allowed_origin=...)`. Keep **`reveal`** keys for server pipelines that need `client.reveal(token).value`.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
grant = client.create_reveal_grant(
|
|
88
|
+
token,
|
|
89
|
+
client_public_key_jwk=jwk_from_iframe,
|
|
90
|
+
allowed_origin="https://app.yourcompany.com",
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Delete requires admin.** There is no delegatable delete-scoped API key at the HTTP layer. `delete_token()` needs an admin key even though policy rules support a `delete` action at the service layer.
|
|
95
|
+
|
|
96
|
+
## Proxy vs reveal vs detokenize
|
|
97
|
+
|
|
98
|
+
- **Vault reveal:** `client.reveal(token)` — `POST /v1/reveal`, reveal-scoped key + policy
|
|
99
|
+
- **Proxy chat:** `client.proxy.chat.completions(...)` — tokenizes declared patterns in-flight
|
|
100
|
+
- **Proxy detokenize:** pass `detokenize=True` or use OpenAI helper below — governed by reveal policy for the proxy key, not the reveal endpoint
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from subzero import create_openai_client
|
|
104
|
+
|
|
105
|
+
client = create_openai_client(
|
|
106
|
+
api_key="sz_live_...", # Subzero proxy key
|
|
107
|
+
base_url="http://127.0.0.1:8000/v1",
|
|
108
|
+
detokenize=True, # X-Subzero-Detokenize: true via default_headers
|
|
109
|
+
)
|
|
110
|
+
client.chat.completions.create(model="gpt-4o", stream=False, messages=[...])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Examples
|
|
114
|
+
|
|
115
|
+
With the API running (`docker compose up` or `uvicorn`):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
export SUBZERO_PLATFORM_EMAIL=admin@iceberg.local
|
|
119
|
+
export SUBZERO_PLATFORM_PASSWORD=...
|
|
120
|
+
export SUBZERO_TOTP_CODE=123456 # or SUBZERO_TOTP_SECRET / SUBZERO_ACCESS_TOKEN
|
|
121
|
+
|
|
122
|
+
python examples/hero_demo.py
|
|
123
|
+
python examples/vault_loop.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Set `SUBZERO_BASE_URL` and `OPENAI_API_KEY` as needed.
|
|
127
|
+
|
|
128
|
+
## Tests
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pytest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## PyPI
|
|
135
|
+
|
|
136
|
+
Local editable install only for now. `# TODO: twine upload` when ready to publish.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared bootstrap helpers for SDK examples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from subzero import SubzeroClient
|
|
9
|
+
from subzero.exceptions import SubzeroAPIError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def platform_bootstrap_client(base_url: str) -> SubzeroClient:
|
|
13
|
+
"""Return a client authenticated as platform admin (JWT on client.access_token)."""
|
|
14
|
+
access_token = os.environ.get("SUBZERO_ACCESS_TOKEN")
|
|
15
|
+
if access_token:
|
|
16
|
+
client = SubzeroClient(access_token=access_token, base_url=base_url)
|
|
17
|
+
client.ready()
|
|
18
|
+
return client
|
|
19
|
+
|
|
20
|
+
email = os.environ.get("SUBZERO_PLATFORM_EMAIL")
|
|
21
|
+
password = os.environ.get("SUBZERO_PLATFORM_PASSWORD")
|
|
22
|
+
if not email or not password:
|
|
23
|
+
print(
|
|
24
|
+
"Set SUBZERO_PLATFORM_EMAIL and SUBZERO_PLATFORM_PASSWORD, "
|
|
25
|
+
"or SUBZERO_ACCESS_TOKEN for an existing session.",
|
|
26
|
+
file=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
client = SubzeroClient(base_url=base_url)
|
|
31
|
+
client.ready()
|
|
32
|
+
try:
|
|
33
|
+
client.auth.login_platform(
|
|
34
|
+
email=email,
|
|
35
|
+
password=password,
|
|
36
|
+
totp_code=os.environ.get("SUBZERO_TOTP_CODE"),
|
|
37
|
+
totp_secret=os.environ.get("SUBZERO_TOTP_SECRET"),
|
|
38
|
+
)
|
|
39
|
+
except SubzeroAPIError as exc:
|
|
40
|
+
print(f"Platform login failed: {exc}", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
return client
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Elements SDK setup: publishable key + tokenize + reveal grant mint."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
6
|
+
|
|
7
|
+
from subzero import SubzeroClient
|
|
8
|
+
|
|
9
|
+
from _bootstrap import admin_client, tenant_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _rsa_public_jwk() -> dict:
|
|
13
|
+
public_key = rsa.generate_private_key(public_exponent=65537, key_size=2048).public_key()
|
|
14
|
+
numbers = public_key.public_numbers()
|
|
15
|
+
|
|
16
|
+
def int_to_b64url(val: int) -> str:
|
|
17
|
+
byte_length = (val.bit_length() + 7) // 8
|
|
18
|
+
return base64.urlsafe_b64encode(val.to_bytes(byte_length, "big")).rstrip(b"=").decode()
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
"kty": "RSA",
|
|
22
|
+
"n": int_to_b64url(numbers.n),
|
|
23
|
+
"e": int_to_b64url(numbers.e),
|
|
24
|
+
"alg": "RSA-OAEP-256",
|
|
25
|
+
"ext": True,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
admin = admin_client()
|
|
31
|
+
admin.ready()
|
|
32
|
+
tid = tenant_id(admin)
|
|
33
|
+
|
|
34
|
+
pub = admin.admin.create_publishable_key(
|
|
35
|
+
tid,
|
|
36
|
+
name="demo-browser",
|
|
37
|
+
allowed_origins=["http://localhost:3000", "http://localhost"],
|
|
38
|
+
)
|
|
39
|
+
print(f"Publishable key: {pub.key}")
|
|
40
|
+
|
|
41
|
+
token = admin.tokenize("SSN", "123-45-6789").token
|
|
42
|
+
print(f"Token (server-side): {token}")
|
|
43
|
+
|
|
44
|
+
grant_key = admin.admin.create_api_key(tid, name="reveal-grant-demo", scope="reveal_grant")
|
|
45
|
+
policies = admin.admin.list_policies(tid)
|
|
46
|
+
if not any(p.principal == f"api_key:{grant_key.id}" for p in policies):
|
|
47
|
+
admin.admin.create_policy(
|
|
48
|
+
tid,
|
|
49
|
+
api_key_id=grant_key.id,
|
|
50
|
+
entity_type="SSN",
|
|
51
|
+
action="reveal",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
grant_client = SubzeroClient(api_key=grant_key.secret, base_url=admin._base_url)
|
|
55
|
+
grant = grant_client.create_reveal_grant(
|
|
56
|
+
token,
|
|
57
|
+
client_public_key_jwk=_rsa_public_jwk(),
|
|
58
|
+
allowed_origin="http://localhost:3000",
|
|
59
|
+
)
|
|
60
|
+
print(f"Reveal grant (one-time): {grant.grant_id} expires {grant.expires_at}")
|
|
61
|
+
|
|
62
|
+
grant_client.close()
|
|
63
|
+
admin.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Subzero LLM proxy hero demo using the Python SDK."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from subzero import SubzeroClient
|
|
11
|
+
|
|
12
|
+
from _bootstrap import platform_bootstrap_client
|
|
13
|
+
|
|
14
|
+
BASE_URL = os.environ.get("SUBZERO_BASE_URL", "http://127.0.0.1:8000")
|
|
15
|
+
UPSTREAM_OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "sk-demo-replace-me")
|
|
16
|
+
SSN_PATTERN = r"\b\d{3}-\d{2}-\d{4}\b"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
bootstrap = platform_bootstrap_client(BASE_URL)
|
|
21
|
+
|
|
22
|
+
tenant = bootstrap.admin.create_tenant(name="Proxy Demo", slug="proxy-demo-sdk")
|
|
23
|
+
tenant_id = tenant.id
|
|
24
|
+
admin_key = tenant.bootstrap_api_key
|
|
25
|
+
if not admin_key:
|
|
26
|
+
print("Expected bootstrap_api_key from tenant signup", file=sys.stderr)
|
|
27
|
+
return 1
|
|
28
|
+
|
|
29
|
+
admin = SubzeroClient(api_key=admin_key, base_url=BASE_URL)
|
|
30
|
+
admin.admin.create_entity_type(
|
|
31
|
+
tenant_id,
|
|
32
|
+
name="SSN",
|
|
33
|
+
deterministic=True,
|
|
34
|
+
match_pattern=SSN_PATTERN,
|
|
35
|
+
)
|
|
36
|
+
admin.admin.create_llm_config(
|
|
37
|
+
tenant_id,
|
|
38
|
+
base_url="https://api.openai.com/v1",
|
|
39
|
+
api_key=UPSTREAM_OPENAI_KEY,
|
|
40
|
+
)
|
|
41
|
+
proxy_key_result = admin.admin.create_api_key(tenant_id, name="proxy", scope="proxy")
|
|
42
|
+
proxy_key = proxy_key_result.secret
|
|
43
|
+
if not proxy_key:
|
|
44
|
+
print("Expected proxy key secret", file=sys.stderr)
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
print("Subzero proxy key (use as OpenAI client api_key):")
|
|
48
|
+
print(f" {proxy_key}")
|
|
49
|
+
print()
|
|
50
|
+
print("OpenAI Python client one-liner change:")
|
|
51
|
+
print(f' base_url="{BASE_URL}/v1"')
|
|
52
|
+
print(f' api_key="{proxy_key[:16]}..."')
|
|
53
|
+
print(" stream=False # required for v1")
|
|
54
|
+
print()
|
|
55
|
+
|
|
56
|
+
proxy_client = SubzeroClient(api_key=proxy_key, base_url=BASE_URL)
|
|
57
|
+
try:
|
|
58
|
+
response = proxy_client.proxy.chat.completions(
|
|
59
|
+
model="gpt-4o",
|
|
60
|
+
messages=[
|
|
61
|
+
{
|
|
62
|
+
"role": "user",
|
|
63
|
+
"content": "Summarize account for SSN 123-45-6789 in one sentence.",
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
print(f"POST /v1/chat/completions failed: {exc}", file=sys.stderr)
|
|
69
|
+
if UPSTREAM_OPENAI_KEY == "sk-demo-replace-me":
|
|
70
|
+
print(
|
|
71
|
+
"Note: set OPENAI_API_KEY for a live upstream call; "
|
|
72
|
+
"tokenization still runs before forward.",
|
|
73
|
+
file=sys.stderr,
|
|
74
|
+
)
|
|
75
|
+
return 0
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
print("POST /v1/chat/completions -> 200")
|
|
79
|
+
print(json.dumps(response, indent=2)[:500])
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Vault loop demo: tokenize → search → reveal → delete.
|
|
3
|
+
|
|
4
|
+
Delete is the one vault step that requires an admin-scoped API key — there is no
|
|
5
|
+
delegatable delete-scoped key at the HTTP layer (per Phase 1B).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from subzero import SubzeroClient
|
|
14
|
+
from subzero.exceptions import AuthorizationError, NotFoundError
|
|
15
|
+
|
|
16
|
+
from _bootstrap import platform_bootstrap_client
|
|
17
|
+
|
|
18
|
+
BASE_URL = os.environ.get("SUBZERO_BASE_URL", "http://127.0.0.1:8000")
|
|
19
|
+
SSN_VALUE = "123-45-6789"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> int:
|
|
23
|
+
bootstrap = platform_bootstrap_client(BASE_URL)
|
|
24
|
+
|
|
25
|
+
tenant = bootstrap.admin.create_tenant(name="Vault Loop Demo", slug="vault-loop-sdk")
|
|
26
|
+
tenant_id = tenant.id
|
|
27
|
+
admin_key = tenant.bootstrap_api_key
|
|
28
|
+
if not admin_key:
|
|
29
|
+
print("Expected bootstrap_api_key", file=sys.stderr)
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
admin = SubzeroClient(api_key=admin_key, base_url=BASE_URL)
|
|
33
|
+
admin.admin.create_entity_type(tenant_id, name="SSN", deterministic=True)
|
|
34
|
+
|
|
35
|
+
tokenize_key = admin.admin.create_api_key(tenant_id, name="tokenize", scope="tokenize").secret
|
|
36
|
+
reveal_key_record = admin.admin.create_api_key(tenant_id, name="reveal", scope="reveal")
|
|
37
|
+
reveal_key = reveal_key_record.secret
|
|
38
|
+
if not tokenize_key or not reveal_key:
|
|
39
|
+
print("Expected scoped key secrets", file=sys.stderr)
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
admin.admin.create_policy(
|
|
43
|
+
tenant_id,
|
|
44
|
+
api_key_id=reveal_key_record.id,
|
|
45
|
+
entity_type="SSN",
|
|
46
|
+
action="reveal",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
tokenize_client = SubzeroClient(api_key=tokenize_key, base_url=BASE_URL)
|
|
50
|
+
reveal_client = SubzeroClient(api_key=reveal_key, base_url=BASE_URL)
|
|
51
|
+
admin_client = SubzeroClient(api_key=admin_key, base_url=BASE_URL)
|
|
52
|
+
|
|
53
|
+
token = tokenize_client.tokenize("SSN", SSN_VALUE).token
|
|
54
|
+
print(f"tokenize -> {token}")
|
|
55
|
+
|
|
56
|
+
found = tokenize_client.search("SSN", SSN_VALUE).token
|
|
57
|
+
assert found == token
|
|
58
|
+
print("search -> hit")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
tokenize_client.reveal(token)
|
|
62
|
+
except AuthorizationError:
|
|
63
|
+
print("reveal with tokenize key -> denied (expected)")
|
|
64
|
+
else:
|
|
65
|
+
print("expected reveal denial with tokenize key", file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
plaintext = reveal_client.reveal(token).value
|
|
69
|
+
assert plaintext == SSN_VALUE
|
|
70
|
+
print(f"reveal with reveal key -> {plaintext}")
|
|
71
|
+
|
|
72
|
+
admin_client.delete_token(token)
|
|
73
|
+
print("delete with admin key -> ok")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
tokenize_client.search("SSN", SSN_VALUE)
|
|
77
|
+
except NotFoundError:
|
|
78
|
+
print("search after delete -> 404 (expected)")
|
|
79
|
+
else:
|
|
80
|
+
print("expected search miss after delete", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "iceberg-subzero"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for the Subzero tokenization vault and LLM proxy"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.28.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
auth = [
|
|
13
|
+
"pyotp>=2.9.0",
|
|
14
|
+
]
|
|
15
|
+
openai = [
|
|
16
|
+
"openai>=1.0.0",
|
|
17
|
+
]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.3.0",
|
|
20
|
+
"respx>=0.22.0",
|
|
21
|
+
"openai>=1.0.0",
|
|
22
|
+
"pyotp>=2.9.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/subzero"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from subzero.auth import AuthResource
|
|
2
|
+
from subzero.client import SubzeroClient
|
|
3
|
+
from subzero.exceptions import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
AuthorizationError,
|
|
6
|
+
ConflictError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
PolicyDeniedError,
|
|
9
|
+
SubzeroAPIError,
|
|
10
|
+
SubzeroNotReadyError,
|
|
11
|
+
)
|
|
12
|
+
from subzero.members import MembersResource
|
|
13
|
+
from subzero.openai import create_openai_client
|
|
14
|
+
from subzero.types import (
|
|
15
|
+
AcceptInviteResult,
|
|
16
|
+
LoginResult,
|
|
17
|
+
MeResult,
|
|
18
|
+
MemberResult,
|
|
19
|
+
MfaEnrollConfirmResult,
|
|
20
|
+
MfaEnrollStartResult,
|
|
21
|
+
TokenResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"SubzeroClient",
|
|
26
|
+
"AuthResource",
|
|
27
|
+
"MembersResource",
|
|
28
|
+
"create_openai_client",
|
|
29
|
+
"LoginResult",
|
|
30
|
+
"TokenResult",
|
|
31
|
+
"MfaEnrollStartResult",
|
|
32
|
+
"MfaEnrollConfirmResult",
|
|
33
|
+
"AcceptInviteResult",
|
|
34
|
+
"MeResult",
|
|
35
|
+
"MemberResult",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"AuthorizationError",
|
|
38
|
+
"ConflictError",
|
|
39
|
+
"NotFoundError",
|
|
40
|
+
"PolicyDeniedError",
|
|
41
|
+
"SubzeroAPIError",
|
|
42
|
+
"SubzeroNotReadyError",
|
|
43
|
+
]
|