implicant 0.2.0__tar.gz → 0.6.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.
- implicant-0.6.0/.github/workflows/publish-pypi.yml +87 -0
- implicant-0.6.0/.github/workflows/release.yml +66 -0
- {implicant-0.2.0 → implicant-0.6.0}/.gitignore +10 -0
- implicant-0.6.0/PKG-INFO +229 -0
- implicant-0.6.0/README.md +210 -0
- implicant-0.6.0/pyproject.toml +79 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/__init__.py +15 -1
- implicant-0.6.0/src/implicant/binarize.py +614 -0
- implicant-0.6.0/src/implicant/cli/main.py +358 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/client.py +97 -20
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/config.py +13 -0
- implicant-0.6.0/src/implicant/demo_keys/README.txt +8 -0
- implicant-0.6.0/src/implicant/demo_keys/bgv-n16384-L7-a394f2ee7032/public_key.bin +0 -0
- implicant-0.6.0/src/implicant/demo_keys/bgv-n16384-L7-a394f2ee7032/secret_key.bin +0 -0
- implicant-0.6.0/src/implicant/demo_keys/bgv-n16384-L8-5f657f27f77c/public_key.bin +0 -0
- implicant-0.6.0/src/implicant/demo_keys/bgv-n16384-L8-5f657f27f77c/secret_key.bin +0 -0
- implicant-0.6.0/src/implicant/keys.py +376 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/manifest.py +31 -2
- implicant-0.6.0/src/implicant/models.py +99 -0
- implicant-0.6.0/src/implicant/samples/bankfraud.json +17 -0
- implicant-0.6.0/src/implicant/samples/cancer.json +32 -0
- implicant-0.6.0/src/implicant/samples/credit.json +46 -0
- implicant-0.6.0/src/implicant/samples/diabetes.json +11 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/transport/base.py +0 -4
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/transport/http.py +40 -24
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/transport/mock.py +5 -49
- implicant-0.2.0/PKG-INFO +0 -85
- implicant-0.2.0/README.md +0 -63
- implicant-0.2.0/docs/INSTALL_SMOKE_TEST.md +0 -47
- implicant-0.2.0/docs/architecture.md +0 -247
- implicant-0.2.0/docs/binarization-spec-format.md +0 -230
- implicant-0.2.0/pyproject.toml +0 -79
- implicant-0.2.0/src/implicant/__main__.py +0 -5
- implicant-0.2.0/src/implicant/app/__init__.py +0 -8
- implicant-0.2.0/src/implicant/app/__main__.py +0 -61
- implicant-0.2.0/src/implicant/app/catalog.py +0 -32
- implicant-0.2.0/src/implicant/app/credentials.py +0 -40
- implicant-0.2.0/src/implicant/app/main_window.py +0 -258
- implicant-0.2.0/src/implicant/app/messages.py +0 -145
- implicant-0.2.0/src/implicant/app/preflight.py +0 -73
- implicant-0.2.0/src/implicant/app/wizard.py +0 -71
- implicant-0.2.0/src/implicant/app/worker.py +0 -72
- implicant-0.2.0/src/implicant/binarize.py +0 -169
- implicant-0.2.0/src/implicant/cli/main.py +0 -133
- implicant-0.2.0/src/implicant/keys.py +0 -172
- implicant-0.2.0/tests/_helut_caps.py +0 -16
- implicant-0.2.0/tests/fixtures/binarization/cancer_golden.json +0 -2230
- implicant-0.2.0/tests/fixtures/binarization/cancer_spec.json +0 -517
- implicant-0.2.0/tests/fixtures/binarization/diabetes_golden.json +0 -624
- implicant-0.2.0/tests/fixtures/binarization/diabetes_spec.json +0 -143
- implicant-0.2.0/tests/test_app_catalog.py +0 -21
- implicant-0.2.0/tests/test_app_credentials.py +0 -52
- implicant-0.2.0/tests/test_app_main_window.py +0 -115
- implicant-0.2.0/tests/test_app_messages.py +0 -85
- implicant-0.2.0/tests/test_app_preflight.py +0 -36
- implicant-0.2.0/tests/test_app_startup.py +0 -32
- implicant-0.2.0/tests/test_app_wizard.py +0 -32
- implicant-0.2.0/tests/test_app_worker.py +0 -60
- implicant-0.2.0/tests/test_binarize.py +0 -79
- implicant-0.2.0/tests/test_binarize_parity.py +0 -33
- implicant-0.2.0/tests/test_client_flow.py +0 -78
- implicant-0.2.0/tests/test_client_staged.py +0 -105
- implicant-0.2.0/tests/test_e2e_xbits.py +0 -117
- implicant-0.2.0/tests/test_envelope.py +0 -37
- implicant-0.2.0/tests/test_http_transport.py +0 -165
- implicant-0.2.0/tests/test_keys.py +0 -52
- implicant-0.2.0/tests/test_manifest.py +0 -75
- implicant-0.2.0/tests/test_postprocess.py +0 -44
- implicant-0.2.0/tests/test_seeded_key_size.py +0 -63
- {implicant-0.2.0 → implicant-0.6.0}/LICENSE +0 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/cli/__init__.py +0 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/envelope.py +0 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/postprocess.py +0 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/transport/__init__.py +0 -0
- {implicant-0.2.0 → implicant-0.6.0}/src/implicant/transport/errors.py +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Mirrors implicant-fhe's publish workflow. Publishing is deliberately a
|
|
4
|
+
# separate, manual act — this never fires on a tag push. A PyPI upload is
|
|
5
|
+
# irreversible (the version number is burned even if the files are deleted),
|
|
6
|
+
# so building/tagging (`release.yml`) and publishing are kept apart.
|
|
7
|
+
#
|
|
8
|
+
# The gate is the dispatch itself rather than a protected environment: this org
|
|
9
|
+
# is on GitHub Free, where required-reviewers protection is a paid feature, and
|
|
10
|
+
# an unprotected `environment:` block would look like a gate while enforcing
|
|
11
|
+
# nothing.
|
|
12
|
+
on:
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
inputs:
|
|
15
|
+
tag:
|
|
16
|
+
description: "Release tag whose artifacts to publish (e.g. v0.5.0)"
|
|
17
|
+
required: true
|
|
18
|
+
type: string
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
publish:
|
|
25
|
+
name: publish ${{ inputs.tag }}
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read # read the release assets
|
|
29
|
+
id-token: write # OIDC token for PyPI Trusted Publishing
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.12"
|
|
35
|
+
|
|
36
|
+
- name: Download artifacts from the release
|
|
37
|
+
env:
|
|
38
|
+
GH_TOKEN: ${{ github.token }}
|
|
39
|
+
run: |
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
mkdir -p dist
|
|
42
|
+
version="${{ inputs.tag }}"
|
|
43
|
+
version="${version#v}"
|
|
44
|
+
# Pinned to the exact tag version: a release still carrying assets
|
|
45
|
+
# from an earlier build cannot be published by accident.
|
|
46
|
+
gh release download "${{ inputs.tag }}" \
|
|
47
|
+
--repo "${{ github.repository }}" \
|
|
48
|
+
--pattern "implicant-${version}*" \
|
|
49
|
+
--dir dist
|
|
50
|
+
ls -l dist/
|
|
51
|
+
|
|
52
|
+
- name: Refuse to publish artifacts that disagree with the tag
|
|
53
|
+
run: |
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
version="${{ inputs.tag }}"
|
|
56
|
+
version="${version#v}"
|
|
57
|
+
|
|
58
|
+
count="$(find dist -type f \( -name '*.whl' -o -name '*.tar.gz' \) | wc -l)"
|
|
59
|
+
if [ "$count" -eq 0 ]; then
|
|
60
|
+
echo "::error::no artifacts attached to ${{ inputs.tag }}"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
mismatched="$(find dist -type f ! -name "implicant-${version}-*.whl" \
|
|
65
|
+
! -name "implicant-${version}.tar.gz")"
|
|
66
|
+
if [ -n "$mismatched" ]; then
|
|
67
|
+
echo "::error::artifacts do not match tag ${{ inputs.tag }}:"
|
|
68
|
+
echo "$mismatched"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
echo "${count} artifact(s) staged for ${{ inputs.tag }}"
|
|
72
|
+
|
|
73
|
+
# The SDK is the client. It must never carry the evaluation engine's
|
|
74
|
+
# worker-only surface, nor a runtime requirement or index setting that
|
|
75
|
+
# would let a client resolve the private worker build. Same check the
|
|
76
|
+
# release build and the test suite run — repeated here because this is
|
|
77
|
+
# the last point before the upload becomes irreversible.
|
|
78
|
+
- name: Refuse to publish anything carrying the server surface
|
|
79
|
+
run: python scripts/check_no_server_surface.py dist
|
|
80
|
+
|
|
81
|
+
# No `password:` input — authentication is PyPI Trusted Publishing over
|
|
82
|
+
# OIDC, so there is no long-lived token stored in this repo. Configure the
|
|
83
|
+
# publisher once at
|
|
84
|
+
# https://pypi.org/manage/project/implicant/settings/publishing/
|
|
85
|
+
# with workflow name `publish-pypi.yml` and no environment.
|
|
86
|
+
- name: Publish to PyPI
|
|
87
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: build release artifacts
|
|
2
|
+
|
|
3
|
+
# Mirrors implicant-fhe's `wheels.yml`: a tag push builds the distribution and
|
|
4
|
+
# cuts a GitHub release with the artifacts attached. It never publishes — that
|
|
5
|
+
# is a separate, manually dispatched act (`publish-pypi.yml`), because a PyPI
|
|
6
|
+
# upload is irreversible: the version number is burned even if the files are
|
|
7
|
+
# later deleted.
|
|
8
|
+
#
|
|
9
|
+
# This package is pure Python (hatchling), so there is no build matrix: one
|
|
10
|
+
# `py3-none-any` wheel plus an sdist, built once.
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
tags:
|
|
14
|
+
- "v*"
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
build:
|
|
22
|
+
name: build wheel + sdist
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Build
|
|
31
|
+
run: |
|
|
32
|
+
python -m pip install --upgrade build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
# The SDK must never ship the evaluation engine's server surface. This is
|
|
36
|
+
# already asserted against the built wheel by
|
|
37
|
+
# `tests/test_no_server_surface.py`; repeat the check here so a release
|
|
38
|
+
# cannot be cut from a tree whose tests were skipped.
|
|
39
|
+
- name: Refuse to release anything carrying the server surface
|
|
40
|
+
run: python scripts/check_no_server_surface.py dist
|
|
41
|
+
|
|
42
|
+
- name: Check metadata
|
|
43
|
+
run: |
|
|
44
|
+
python -m pip install --upgrade twine
|
|
45
|
+
twine check dist/*
|
|
46
|
+
|
|
47
|
+
- uses: actions/upload-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: dist
|
|
50
|
+
path: dist/*
|
|
51
|
+
|
|
52
|
+
release:
|
|
53
|
+
name: attach to release
|
|
54
|
+
needs: [build]
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
57
|
+
permissions:
|
|
58
|
+
contents: write
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/download-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
path: dist
|
|
63
|
+
merge-multiple: true
|
|
64
|
+
- uses: softprops/action-gh-release@v2
|
|
65
|
+
with:
|
|
66
|
+
files: dist/*
|
|
@@ -45,3 +45,13 @@ CLAUDE.local.md
|
|
|
45
45
|
|
|
46
46
|
# Staged implicant-fhe wheels (downloaded via `gh release download`, not committed)
|
|
47
47
|
wheels/
|
|
48
|
+
|
|
49
|
+
# Briefcase / build logs
|
|
50
|
+
logs/
|
|
51
|
+
|
|
52
|
+
# Demo handoff bundle (large public-key blobs sent to platform; not committed).
|
|
53
|
+
# The .zip needs its own rule: the directory pattern above does not match it, so
|
|
54
|
+
# it stayed untracked-but-committable (460 MB) and hatchling swept it into the
|
|
55
|
+
# sdist (489 MB, past PyPI's 100 MB limit) until it was excluded explicitly.
|
|
56
|
+
/demo_handoff/
|
|
57
|
+
/demo_handoff.zip
|
implicant-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: implicant
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Python client SDK for the Implicant FHE inference platform
|
|
5
|
+
Author: Implicant
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: implicant-fhe<0.6,>=0.5
|
|
11
|
+
Requires-Dist: numpy>=1.26
|
|
12
|
+
Requires-Dist: pydantic>=2.6
|
|
13
|
+
Requires-Dist: rich>=13.8
|
|
14
|
+
Requires-Dist: typer>=0.12
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Implicant SDK
|
|
21
|
+
|
|
22
|
+
Python **client** for the Implicant FHE inference platform. Encrypts one row
|
|
23
|
+
locally under BGV, sends the ciphertext to the platform for homomorphic
|
|
24
|
+
evaluation, and decrypts the result. The server never sees plaintext; the
|
|
25
|
+
secret key never leaves the process.
|
|
26
|
+
|
|
27
|
+
The classifier head runs **server-side under FHE** (Variant B): the platform
|
|
28
|
+
returns one ciphertext with the class scores packed contiguously, which the
|
|
29
|
+
client decrypts (unsigned) and decodes with `decode_class_scores` before
|
|
30
|
+
threshold/argmax. `W` / `b` never reach the client.
|
|
31
|
+
|
|
32
|
+
Pure Python — all crypto comes from [`implicant-fhe`](https://github.com/implicant/implicant-fhe)
|
|
33
|
+
(`helut.client`). Contracts: [`docs/architecture.md`](docs/architecture.md)
|
|
34
|
+
and `platform/docs/FHE_INTERFACE_SPEC.md`.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install implicant
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Pulls `implicant-fhe` (the `helut` crypto wheel) from PyPI automatically. Wheels
|
|
43
|
+
ship for **CPython 3.10–3.14** on **macOS-arm64** and **manylinux x86_64** only.
|
|
44
|
+
On any other target — Windows, Linux aarch64, macOS x86_64 — there is no wheel
|
|
45
|
+
and no source fallback, so pip stops with `No matching distribution found for
|
|
46
|
+
implicant-fhe`.
|
|
47
|
+
|
|
48
|
+
### Developers
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python3.13 -m venv .venv313
|
|
52
|
+
.venv313/bin/pip install -e ".[dev]"
|
|
53
|
+
.venv313/bin/pytest tests/ -v # 100 tests, ~3.5 min (real BGV keygen)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
To pin an unreleased `implicant-fhe` build, stage its wheel in `./wheels/` and
|
|
57
|
+
add `--find-links wheels/`; the published versions resolve from PyPI without it.
|
|
58
|
+
|
|
59
|
+
## Running an inference (CLI)
|
|
60
|
+
|
|
61
|
+
The `implicant` CLI is the client-side workflow: configure once, then run
|
|
62
|
+
encrypted predictions. Everything stays local except the ciphertext and the
|
|
63
|
+
public keys — the secret key never leaves your machine.
|
|
64
|
+
|
|
65
|
+
### 1. Point the client at the platform
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
implicant init --api-url https://api.implicant.example
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Writes `~/.implicant/config.toml`. Run `implicant status` to confirm the
|
|
72
|
+
configured `api_url` and list any locally cached keys.
|
|
73
|
+
|
|
74
|
+
### 2. Set your API token
|
|
75
|
+
|
|
76
|
+
The bearer token is read from the environment, never stored on disk:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export IMPLICANT_API_KEY="imp_..."
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Discover the available models
|
|
83
|
+
|
|
84
|
+
The SDK ships a hand-maintained registry of the models servable on the
|
|
85
|
+
platform (there is no server-side listing endpoint yet):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
$ implicant models list
|
|
89
|
+
cancer Predicts whether a breast tumor is malignant or benign
|
|
90
|
+
diabetes Predicts whether a patient has diabetes
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The listed slug is the `--model` value for `predict`.
|
|
94
|
+
|
|
95
|
+
### 4. Prepare the input row
|
|
96
|
+
|
|
97
|
+
One row of raw feature values, as a JSON object (or a single-row CSV). The keys
|
|
98
|
+
are the feature names the model expects. Each known model ships a bundled
|
|
99
|
+
example row — the fastest way to get a correct starting point:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
implicant models sample diabetes --out row.json # or no --out to print to stdout
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"preg": 6,
|
|
108
|
+
"plas": 148,
|
|
109
|
+
"pres": 72,
|
|
110
|
+
"skin": 35,
|
|
111
|
+
"insu": 0,
|
|
112
|
+
"mass": 33.6,
|
|
113
|
+
"pedi": 0.627,
|
|
114
|
+
"age": 50
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Edit the values by hand to test different inputs, then feed the file to
|
|
119
|
+
`predict`. (`--out` refuses to overwrite an existing file unless you pass
|
|
120
|
+
`--force`.)
|
|
121
|
+
|
|
122
|
+
### 5. Predict
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
implicant predict --model diabetes --input row.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or skip the file entirely and run on the bundled example row:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
implicant predict --model diabetes --sample
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This fetches the model manifest, generates (or reloads) your keys, uploads the
|
|
135
|
+
**public** keys on first use, encrypts the row locally, sends the ciphertext,
|
|
136
|
+
then decrypts and decodes the returned class scores. Progress streams to
|
|
137
|
+
stderr (with a spinner on the current step in a terminal) and the decision
|
|
138
|
+
lands on stdout:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
✓ Fetching model configuration
|
|
142
|
+
✓ Preparing encryption keys
|
|
143
|
+
✓ Encrypting input data
|
|
144
|
+
✓ Evaluating on the server (encrypted)
|
|
145
|
+
✓ Decrypting server response
|
|
146
|
+
Final decision: tested_negative
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Add `--json` for the machine-readable result instead of the decision line
|
|
150
|
+
(progress stays on stderr, so piping stdout to `jq` is safe):
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"key_id": "bgv-n32768-L4-a1b2c3d4e5f6",
|
|
155
|
+
"label_index": 0,
|
|
156
|
+
"label": "tested_negative",
|
|
157
|
+
"scores": [-6684]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Add `--no-persist-key` to use an ephemeral secret key for a single run (nothing
|
|
162
|
+
written to disk; keys are regenerated and re-uploaded next time).
|
|
163
|
+
|
|
164
|
+
### Managing keys
|
|
165
|
+
|
|
166
|
+
The first prediction for a `key_id` persists your secret key to
|
|
167
|
+
`~/.implicant/keys/<key_id>/secret_key.bin` (mode `0600`) so later runs skip
|
|
168
|
+
keygen. Inspect and clean up the local store:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
implicant keys list # list key_ids; flags which have a persisted SK
|
|
172
|
+
implicant keys rm <key_id> # delete one stored key (secret + public)
|
|
173
|
+
implicant keys purge --yes # delete all stored keys
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Use (Python)
|
|
177
|
+
|
|
178
|
+
The same flow is available programmatically:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from implicant import ImplicantClient, list_known_models, sample_row
|
|
182
|
+
from implicant.transport import HttpxTransport
|
|
183
|
+
|
|
184
|
+
for m in list_known_models(): # discover: (slug, description) pairs
|
|
185
|
+
print(m.slug, "—", m.name)
|
|
186
|
+
|
|
187
|
+
row = sample_row("diabetes") # fresh mutable copy of the bundled example
|
|
188
|
+
row["age"] = 61 # hand-tweak values for testing
|
|
189
|
+
|
|
190
|
+
client = ImplicantClient(
|
|
191
|
+
HttpxTransport(base_url="https://api.implicant.example", api_key="imp_..."),
|
|
192
|
+
key_cache_dir="~/.implicant/keys",
|
|
193
|
+
)
|
|
194
|
+
result = client.predict("diabetes", row, class_names=("negative", "positive"))
|
|
195
|
+
print(result.prediction.label)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Demo mode
|
|
199
|
+
|
|
200
|
+
Demo mode skips FHE key generation by loading a pre-generated keypair shipped
|
|
201
|
+
inside the package, and skips the public-key upload (the platform already holds
|
|
202
|
+
the matching public bundle). It exists so a prediction can run immediately in a
|
|
203
|
+
live demo without the ~250 s keygen delay.
|
|
204
|
+
|
|
205
|
+
Activate with an environment variable (applies to the whole session):
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
export IMPLICANT_DEMO=1
|
|
209
|
+
implicant predict -m cancer -i row.json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or per-command — combined with `--sample`, this is a zero-setup end-to-end run:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
implicant predict -m cancer --sample --demo
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Only the provisioned demo models (`cancer`, `diabetes`) have bundled keys; other
|
|
219
|
+
model IDs raise an error in demo mode.
|
|
220
|
+
|
|
221
|
+
> **Security warning — demo mode is not private.** The demo secret key is
|
|
222
|
+
> shipped inside the package and is therefore public: anyone with the wheel can
|
|
223
|
+
> decrypt anything encrypted in demo mode. **Never use demo mode for real or
|
|
224
|
+
> sensitive data.** Normal mode (the default) generates a secret key that never
|
|
225
|
+
> leaves your machine.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
Apache-2.0.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Implicant SDK
|
|
2
|
+
|
|
3
|
+
Python **client** for the Implicant FHE inference platform. Encrypts one row
|
|
4
|
+
locally under BGV, sends the ciphertext to the platform for homomorphic
|
|
5
|
+
evaluation, and decrypts the result. The server never sees plaintext; the
|
|
6
|
+
secret key never leaves the process.
|
|
7
|
+
|
|
8
|
+
The classifier head runs **server-side under FHE** (Variant B): the platform
|
|
9
|
+
returns one ciphertext with the class scores packed contiguously, which the
|
|
10
|
+
client decrypts (unsigned) and decodes with `decode_class_scores` before
|
|
11
|
+
threshold/argmax. `W` / `b` never reach the client.
|
|
12
|
+
|
|
13
|
+
Pure Python — all crypto comes from [`implicant-fhe`](https://github.com/implicant/implicant-fhe)
|
|
14
|
+
(`helut.client`). Contracts: [`docs/architecture.md`](docs/architecture.md)
|
|
15
|
+
and `platform/docs/FHE_INTERFACE_SPEC.md`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install implicant
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Pulls `implicant-fhe` (the `helut` crypto wheel) from PyPI automatically. Wheels
|
|
24
|
+
ship for **CPython 3.10–3.14** on **macOS-arm64** and **manylinux x86_64** only.
|
|
25
|
+
On any other target — Windows, Linux aarch64, macOS x86_64 — there is no wheel
|
|
26
|
+
and no source fallback, so pip stops with `No matching distribution found for
|
|
27
|
+
implicant-fhe`.
|
|
28
|
+
|
|
29
|
+
### Developers
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3.13 -m venv .venv313
|
|
33
|
+
.venv313/bin/pip install -e ".[dev]"
|
|
34
|
+
.venv313/bin/pytest tests/ -v # 100 tests, ~3.5 min (real BGV keygen)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
To pin an unreleased `implicant-fhe` build, stage its wheel in `./wheels/` and
|
|
38
|
+
add `--find-links wheels/`; the published versions resolve from PyPI without it.
|
|
39
|
+
|
|
40
|
+
## Running an inference (CLI)
|
|
41
|
+
|
|
42
|
+
The `implicant` CLI is the client-side workflow: configure once, then run
|
|
43
|
+
encrypted predictions. Everything stays local except the ciphertext and the
|
|
44
|
+
public keys — the secret key never leaves your machine.
|
|
45
|
+
|
|
46
|
+
### 1. Point the client at the platform
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
implicant init --api-url https://api.implicant.example
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Writes `~/.implicant/config.toml`. Run `implicant status` to confirm the
|
|
53
|
+
configured `api_url` and list any locally cached keys.
|
|
54
|
+
|
|
55
|
+
### 2. Set your API token
|
|
56
|
+
|
|
57
|
+
The bearer token is read from the environment, never stored on disk:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export IMPLICANT_API_KEY="imp_..."
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Discover the available models
|
|
64
|
+
|
|
65
|
+
The SDK ships a hand-maintained registry of the models servable on the
|
|
66
|
+
platform (there is no server-side listing endpoint yet):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
$ implicant models list
|
|
70
|
+
cancer Predicts whether a breast tumor is malignant or benign
|
|
71
|
+
diabetes Predicts whether a patient has diabetes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The listed slug is the `--model` value for `predict`.
|
|
75
|
+
|
|
76
|
+
### 4. Prepare the input row
|
|
77
|
+
|
|
78
|
+
One row of raw feature values, as a JSON object (or a single-row CSV). The keys
|
|
79
|
+
are the feature names the model expects. Each known model ships a bundled
|
|
80
|
+
example row — the fastest way to get a correct starting point:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
implicant models sample diabetes --out row.json # or no --out to print to stdout
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"preg": 6,
|
|
89
|
+
"plas": 148,
|
|
90
|
+
"pres": 72,
|
|
91
|
+
"skin": 35,
|
|
92
|
+
"insu": 0,
|
|
93
|
+
"mass": 33.6,
|
|
94
|
+
"pedi": 0.627,
|
|
95
|
+
"age": 50
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Edit the values by hand to test different inputs, then feed the file to
|
|
100
|
+
`predict`. (`--out` refuses to overwrite an existing file unless you pass
|
|
101
|
+
`--force`.)
|
|
102
|
+
|
|
103
|
+
### 5. Predict
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
implicant predict --model diabetes --input row.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Or skip the file entirely and run on the bundled example row:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
implicant predict --model diabetes --sample
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This fetches the model manifest, generates (or reloads) your keys, uploads the
|
|
116
|
+
**public** keys on first use, encrypts the row locally, sends the ciphertext,
|
|
117
|
+
then decrypts and decodes the returned class scores. Progress streams to
|
|
118
|
+
stderr (with a spinner on the current step in a terminal) and the decision
|
|
119
|
+
lands on stdout:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
✓ Fetching model configuration
|
|
123
|
+
✓ Preparing encryption keys
|
|
124
|
+
✓ Encrypting input data
|
|
125
|
+
✓ Evaluating on the server (encrypted)
|
|
126
|
+
✓ Decrypting server response
|
|
127
|
+
Final decision: tested_negative
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Add `--json` for the machine-readable result instead of the decision line
|
|
131
|
+
(progress stays on stderr, so piping stdout to `jq` is safe):
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"key_id": "bgv-n32768-L4-a1b2c3d4e5f6",
|
|
136
|
+
"label_index": 0,
|
|
137
|
+
"label": "tested_negative",
|
|
138
|
+
"scores": [-6684]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Add `--no-persist-key` to use an ephemeral secret key for a single run (nothing
|
|
143
|
+
written to disk; keys are regenerated and re-uploaded next time).
|
|
144
|
+
|
|
145
|
+
### Managing keys
|
|
146
|
+
|
|
147
|
+
The first prediction for a `key_id` persists your secret key to
|
|
148
|
+
`~/.implicant/keys/<key_id>/secret_key.bin` (mode `0600`) so later runs skip
|
|
149
|
+
keygen. Inspect and clean up the local store:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
implicant keys list # list key_ids; flags which have a persisted SK
|
|
153
|
+
implicant keys rm <key_id> # delete one stored key (secret + public)
|
|
154
|
+
implicant keys purge --yes # delete all stored keys
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Use (Python)
|
|
158
|
+
|
|
159
|
+
The same flow is available programmatically:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from implicant import ImplicantClient, list_known_models, sample_row
|
|
163
|
+
from implicant.transport import HttpxTransport
|
|
164
|
+
|
|
165
|
+
for m in list_known_models(): # discover: (slug, description) pairs
|
|
166
|
+
print(m.slug, "—", m.name)
|
|
167
|
+
|
|
168
|
+
row = sample_row("diabetes") # fresh mutable copy of the bundled example
|
|
169
|
+
row["age"] = 61 # hand-tweak values for testing
|
|
170
|
+
|
|
171
|
+
client = ImplicantClient(
|
|
172
|
+
HttpxTransport(base_url="https://api.implicant.example", api_key="imp_..."),
|
|
173
|
+
key_cache_dir="~/.implicant/keys",
|
|
174
|
+
)
|
|
175
|
+
result = client.predict("diabetes", row, class_names=("negative", "positive"))
|
|
176
|
+
print(result.prediction.label)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Demo mode
|
|
180
|
+
|
|
181
|
+
Demo mode skips FHE key generation by loading a pre-generated keypair shipped
|
|
182
|
+
inside the package, and skips the public-key upload (the platform already holds
|
|
183
|
+
the matching public bundle). It exists so a prediction can run immediately in a
|
|
184
|
+
live demo without the ~250 s keygen delay.
|
|
185
|
+
|
|
186
|
+
Activate with an environment variable (applies to the whole session):
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
export IMPLICANT_DEMO=1
|
|
190
|
+
implicant predict -m cancer -i row.json
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Or per-command — combined with `--sample`, this is a zero-setup end-to-end run:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
implicant predict -m cancer --sample --demo
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Only the provisioned demo models (`cancer`, `diabetes`) have bundled keys; other
|
|
200
|
+
model IDs raise an error in demo mode.
|
|
201
|
+
|
|
202
|
+
> **Security warning — demo mode is not private.** The demo secret key is
|
|
203
|
+
> shipped inside the package and is therefore public: anyone with the wheel can
|
|
204
|
+
> decrypt anything encrypted in demo mode. **Never use demo mode for real or
|
|
205
|
+
> sensitive data.** Normal mode (the default) generates a secret key that never
|
|
206
|
+
> leaves your machine.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
Apache-2.0.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "implicant"
|
|
7
|
+
version = "0.6.0"
|
|
8
|
+
description = "Python client SDK for the Implicant FHE inference platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Implicant" }]
|
|
12
|
+
license = { text = "Apache-2.0" }
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
# FHE primitives (keygen, encrypt, decrypt, Params, key_id) live here.
|
|
16
|
+
# The SDK is a pure-Python client over `helut.client`. Floor is 0.5.0:
|
|
17
|
+
# that release moves the kernel to SEAL 4.4.3, which rotates every
|
|
18
|
+
# canonical key_id (seal_version "4.3" -> "4.4" in the params hash), so
|
|
19
|
+
# the bundled demo keys only exist under 0.5-era key_ids. The 0.4-era
|
|
20
|
+
# client/server wheel split still holds: both distributions satisfy this
|
|
21
|
+
# specifier, and only the public client build is reachable from PyPI.
|
|
22
|
+
"implicant-fhe>=0.5,<0.6",
|
|
23
|
+
"httpx>=0.27",
|
|
24
|
+
"pydantic>=2.6",
|
|
25
|
+
"typer>=0.12",
|
|
26
|
+
# Progress rendering in the CLI (typer already requires it; declared
|
|
27
|
+
# because we import it directly).
|
|
28
|
+
"rich>=13.8",
|
|
29
|
+
"numpy>=1.26",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
# The worker (server-capable) build of implicant-fhe — carrying the
|
|
34
|
+
# `+server` local version once implicant-fhe's client/server wheel split
|
|
35
|
+
# lands — belongs ONLY here, never in [project.dependencies]. The SDK's own
|
|
36
|
+
# test suite legitimately drives the real engine (tests/_server_harness.py)
|
|
37
|
+
# on a dev machine; a client installing `implicant` from PyPI must never be
|
|
38
|
+
# able to resolve the worker build even by accident. Enforced against the
|
|
39
|
+
# BUILT wheel by tests/test_no_server_surface.py. See
|
|
40
|
+
# implicant-fhe/docs/superpowers/plans/2026-07-31-client-server-wheel-split.md
|
|
41
|
+
# Task W5. The runtime floor is 0.5.0 (see [project.dependencies]); both
|
|
42
|
+
# distributions satisfy it, and PyPI structurally refuses the worker build's
|
|
43
|
+
# local version, so a client cannot resolve it.
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=8",
|
|
46
|
+
"pytest-cov>=5",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
implicant = "implicant.cli.main:app"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["src/implicant"]
|
|
54
|
+
# Demo keypairs are large .bin blobs (typically VCS-ignored); force-include them
|
|
55
|
+
# so demo mode can load them at runtime. Public material only + a PUBLIC demo SK.
|
|
56
|
+
artifacts = ["src/implicant/demo_keys/**/*.bin"]
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.sdist]
|
|
59
|
+
# Keep internal dev context, local tooling, and the non-functional demo stub
|
|
60
|
+
# out of the published source dist. `demo_handoff.zip` is a 482 MB bundle of
|
|
61
|
+
# public key blobs staged for the platform; the sibling `demo_handoff/` dir is
|
|
62
|
+
# VCS-ignored but the zip is not, so without this it lands in the sdist and
|
|
63
|
+
# pushes it to ~489 MB — far past PyPI's 100 MB per-file limit.
|
|
64
|
+
# `tests/` and `scripts/` are excluded for a SECURITY reason, not tidiness:
|
|
65
|
+
# `tests/_server_harness.py` and the w6_* probes wire the engine's worker-only
|
|
66
|
+
# facade into a working local round trip. That recipe was
|
|
67
|
+
# deliberately removed from the shipped package (audit finding B1, wheel-split
|
|
68
|
+
# task W5); shipping it in the sdist would hand it straight back. `docs/` goes
|
|
69
|
+
# too — it references the private repo and internal design notes.
|
|
70
|
+
# `scripts/check_no_server_surface.py` enforces this on every release.
|
|
71
|
+
exclude = [
|
|
72
|
+
"CLAUDE.md",
|
|
73
|
+
".claude",
|
|
74
|
+
"docs",
|
|
75
|
+
"tests",
|
|
76
|
+
"scripts",
|
|
77
|
+
"examples",
|
|
78
|
+
"demo_handoff.zip",
|
|
79
|
+
]
|