From 5040d181d279e25e884c25eb39522806244b2a27 Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Mon, 13 Apr 2026 17:48:10 +0200 Subject: [PATCH 01/19] fix: revert workflows to reach any platform and any arch --- .forgejo/workflows/publish.yml | 28 +++++++--------------------- pyproject.toml | 11 +++++++---- setup.py | 31 ------------------------------- 3 files changed, 14 insertions(+), 56 deletions(-) delete mode 100644 setup.py diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml index 17f700c..9fdee68 100644 --- a/.forgejo/workflows/publish.yml +++ b/.forgejo/workflows/publish.yml @@ -7,16 +7,10 @@ on: workflow_dispatch: jobs: - build-and-publish: - name: Build & Publish (${{ matrix.runner }}, py${{ matrix.python }}) - runs-on: ${{ matrix.runner }} + publish: + runs-on: docker-amd64 container: - image: python:${{ matrix.python }}-bookworm - - strategy: - matrix: - python: ["3.10", "3.11", "3.12"] - runner: [docker-amd64, docker-arm64] + image: python:3.12-bookworm steps: - name: Checkout repository @@ -26,21 +20,13 @@ jobs: . - name: Install build tools - run: | - apt-get update -qq && apt-get install -y patchelf - pip install build Cython twine auditwheel + run: pip install build twine - - name: Build wheel - run: python -m build --wheel - - - name: Repair wheel to manylinux - run: auditwheel repair dist/*.whl --wheel-dir wheelhouse/ - - - name: Check wheel metadata - run: twine check wheelhouse/*.whl + - name: Build package + run: python -m build - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: twine upload --verbose wheelhouse/*.whl + run: twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml index e0902c3..681ba9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools>=68", "wheel", "Cython>=3.0"] -build-backend = "setuptools.build_meta" +requires = ["hatchling>=1.0.0", "wheel"] +build-backend = "hatchling.build" [project] name = "nomyo" @@ -46,5 +46,8 @@ Documentation = "https://bitfreedom.net/code/nomyo-ai/nomyo/wiki/NOMYO-Secure-Cl Repository = "https://bitfreedom.net/code/nomyo-ai/nomyo" Issues = "https://bitfreedom.net/code/nomyo-ai/nomyo/issues" -[tool.setuptools.packages.find] -include = ["nomyo*"] +[tool.hatch.build.targets.wheel] +packages = ["nomyo"] + +[tool.hatch.build.targets.sdist] +exclude = ["test/", "build.sh", "dist/"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 92d0bd3..0000000 --- a/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -from setuptools import setup -from setuptools.command.build_py import build_py as _build_py -from Cython.Build import cythonize - -# Modules compiled to .so — exclude their .py source from the wheel -COMPILED_MODULES = {"nomyo", "SecureCompletionClient", "SecureMemory"} - - -class BuildPyNoPy(_build_py): - """Skip copying .py source files for cythonized modules.""" - - def find_package_modules(self, package, package_dir): - modules = super().find_package_modules(package, package_dir) - return [ - (pkg, mod, path) - for pkg, mod, path in modules - if not (pkg == "nomyo" and mod in COMPILED_MODULES) - ] - - -setup( - ext_modules=cythonize( - [ - "nomyo/nomyo.py", - "nomyo/SecureCompletionClient.py", - "nomyo/SecureMemory.py", - ], - compiler_directives={"language_level": "3"}, - ), - cmdclass={"build_py": BuildPyNoPy}, -) From 93adb6c45c8cf89227b359f94622ca227bcef3a5 Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Wed, 15 Apr 2026 12:08:21 +0200 Subject: [PATCH 02/19] feat: add automatic client retry logic with exponential backoff --- README.md | 6 +- doc/api-reference.md | 11 +++- doc/rate-limits.md | 20 +++--- nomyo/SecureCompletionClient.py | 106 ++++++++++++++++++-------------- nomyo/__init__.py | 2 +- nomyo/nomyo.py | 6 +- pyproject.toml | 2 +- 7 files changed, 87 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 1846d2e..c10b898 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,8 @@ SecureChatCompletion( base_url: str = "https://api.nomyo.ai", allow_http: bool = False, api_key: Optional[str] = None, - secure_memory: bool = True + secure_memory: bool = True, + max_retries: int = 2 ) ``` @@ -359,6 +360,7 @@ SecureChatCompletion( - `allow_http`: Allow HTTP connections (ONLY for local development, never in production) - `api_key`: Optional API key for bearer authentication - `secure_memory`: Enable secure memory protection (default: True) +- `max_retries`: Retries on retryable errors (429, 500, 502, 503, 504, network errors) with exponential backoff. Default: 2 #### Methods @@ -370,7 +372,7 @@ SecureChatCompletion( #### Constructor ```python -SecureCompletionClient(router_url: str = "https://api.nomyo.ai") +SecureCompletionClient(router_url: str = "https://api.nomyo.ai", allow_http: bool = False, max_retries: int = 2) ``` #### Methods diff --git a/doc/api-reference.md b/doc/api-reference.md index 439e471..363c865 100644 --- a/doc/api-reference.md +++ b/doc/api-reference.md @@ -11,7 +11,8 @@ SecureChatCompletion( base_url: str = "https://api.nomyo.ai", allow_http: bool = False, api_key: Optional[str] = None, - secure_memory: bool = True + secure_memory: bool = True, + max_retries: int = 2 ) ``` @@ -21,6 +22,7 @@ SecureChatCompletion( - `allow_http` (bool): Allow HTTP connections (ONLY for local development, never in production) - `api_key` (Optional[str]): Optional API key for bearer authentication - `secure_memory` (bool): Enable secure memory protection (default: True) +- `max_retries` (int): Number of retries on retryable errors (429, 500, 502, 503, 504, network errors). Uses exponential backoff. Default: 2 ### Methods @@ -92,13 +94,18 @@ The `SecureCompletionClient` class handles the underlying encryption, key manage ### Constructor ```python -SecureCompletionClient(router_url: str = "https://api.nomyo.ai", allow_http: bool = False) +SecureCompletionClient( + router_url: str = "https://api.nomyo.ai", + allow_http: bool = False, + max_retries: int = 2 +) ``` **Parameters:** - `router_url` (str): Base URL of the NOMYO Router (must use HTTPS for production) - `allow_http` (bool): Allow HTTP connections (ONLY for local development, never in production) +- `max_retries` (int): Number of retries on retryable errors (429, 500, 502, 503, 504, network errors). Uses exponential backoff. Default: 2 ### Methods diff --git a/doc/rate-limits.md b/doc/rate-limits.md index 7d9f85c..2c18da2 100644 --- a/doc/rate-limits.md +++ b/doc/rate-limits.md @@ -48,20 +48,14 @@ HTTP/1.1 503 Service Unavailable - **Implement exponential backoff** when you receive a `429` response. Start with a short delay (e.g. 500 ms) and double it on each subsequent failure, up to a reasonable maximum. - **Monitor for `503` responses** — repeated occurrences indicate that your usage pattern is triggering the abuse threshold. Refactor your request logic before the cool-down expires. -## Example: Exponential Backoff +## Retry Behaviour + +The client retries automatically on `429`, `500`, `502`, `503`, `504`, and network errors using exponential backoff (1 s, 2 s, …). The default is **2 retries**. You can raise or disable this per client: ```python -import asyncio -import httpx +# More retries for high-throughput workloads +client = SecureChatCompletion(api_key="...", max_retries=5) -async def request_with_backoff(client, *args, max_retries=5, **kwargs): - delay = 0.5 - for attempt in range(max_retries): - response = await client.create(*args, **kwargs) - if response.status_code == 429: - await asyncio.sleep(delay) - delay = min(delay * 2, 30) - continue - return response - raise RuntimeError("Rate limit exceeded after maximum retries") +# Disable retries entirely +client = SecureChatCompletion(api_key="...", max_retries=0) ``` diff --git a/nomyo/SecureCompletionClient.py b/nomyo/SecureCompletionClient.py index ee81942..4c1d96d 100644 --- a/nomyo/SecureCompletionClient.py +++ b/nomyo/SecureCompletionClient.py @@ -1,4 +1,4 @@ -import ctypes, json, base64, urllib.parse, httpx, os, secrets, sys, warnings, logging +import asyncio, ctypes, json, base64, urllib.parse, httpx, os, secrets, sys, warnings, logging from typing import Dict, Any, Optional from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import rsa, padding @@ -76,7 +76,7 @@ class SecureCompletionClient: - Response parsing """ - def __init__(self, router_url: str = "https://api.nomyo.ai", allow_http: bool = False, secure_memory: bool = True): + def __init__(self, router_url: str = "https://api.nomyo.ai", allow_http: bool = False, secure_memory: bool = True, max_retries: int = 2): """ Initialize the secure completion client. @@ -84,6 +84,9 @@ class SecureCompletionClient: router_url: Base URL of the NOMYO Router (must use HTTPS for production) allow_http: Allow HTTP connections (ONLY for local development, never in production) secure_memory: Whether to use secure memory operations for this instance. + max_retries: Number of retries on retryable errors (429, 500, 502, 503, 504, + network errors). Uses exponential backoff. Default 2, matching + the OpenAI Python SDK default. """ self.router_url = router_url.rstrip('/') self.private_key = None @@ -91,6 +94,7 @@ class SecureCompletionClient: self.key_size = 4096 # RSA key size self.allow_http = allow_http # Store for use in fetch_server_public_key self._use_secure_memory = _SECURE_MEMORY_AVAILABLE and secure_memory + self.max_retries = max_retries # Validate HTTPS for security if not self.router_url.startswith("https://"): @@ -659,13 +663,22 @@ class SecureCompletionClient: url = f"{self.router_url}/v1/chat/secure_completion" logger.debug("Target URL: %s", url) - try: - async with httpx.AsyncClient(timeout=60.0) as client: - response = await client.post( - url, - headers=headers, - content=encrypted_payload - ) + _RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504} + last_exc: Exception = APIConnectionError("Request failed") + + for attempt in range(self.max_retries + 1): + if attempt > 0: + delay = 2 ** (attempt - 1) # 1s, 2s, 4s, … + logger.warning("Retrying request (attempt %d/%d) after %.1fs...", attempt, self.max_retries, delay) + await asyncio.sleep(delay) + + try: + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post( + url, + headers=headers, + content=encrypted_payload + ) logger.debug("HTTP Status: %d", response.status_code) @@ -676,7 +689,6 @@ class SecureCompletionClient: return decrypted_response elif response.status_code == 400: - # Bad request try: error = response.json() raise InvalidRequestError( @@ -688,7 +700,6 @@ class SecureCompletionClient: raise InvalidRequestError("Bad request: Invalid response format") elif response.status_code == 401: - # Unauthorized - authentication failed try: error = response.json() error_message = error.get('detail', 'Invalid API key or authentication failed') @@ -701,7 +712,6 @@ class SecureCompletionClient: raise AuthenticationError("Invalid API key or authentication failed") elif response.status_code == 403: - # Forbidden - model not allowed for security tier try: error = response.json() raise ForbiddenError( @@ -713,7 +723,6 @@ class SecureCompletionClient: raise ForbiddenError("Forbidden: Model not allowed for the requested security tier") elif response.status_code == 404: - # Endpoint not found try: error = response.json() raise APIError( @@ -724,44 +733,47 @@ class SecureCompletionClient: except (json.JSONDecodeError, ValueError): raise APIError("Endpoint not found: Secure inference not enabled") - elif response.status_code == 429: - # Rate limit exceeded + elif response.status_code in _RETRYABLE_STATUS_CODES: try: error = response.json() - raise RateLimitError( - f"Rate limit exceeded: {error.get('detail', 'Too many requests')}", + if not isinstance(error, dict): + error = {"detail": "unknown"} + detail_msg = error.get("detail", "unknown") + except (json.JSONDecodeError, ValueError): + error = {} + detail_msg = "unknown" + + if response.status_code == 429: + last_exc = RateLimitError( + f"Rate limit exceeded: {detail_msg}", status_code=429, error_details=error ) - except (json.JSONDecodeError, ValueError): - raise RateLimitError("Rate limit exceeded: Too many requests") - - elif response.status_code == 500: - # Server error - try: - error = response.json() - raise ServerError( - f"Server error: {error.get('detail', 'Internal server error')}", + elif response.status_code == 500: + last_exc = ServerError( + f"Server error: {detail_msg}", status_code=500, error_details=error ) - except (json.JSONDecodeError, ValueError): - raise ServerError("Server error: Internal server error") - - elif response.status_code == 503: - # Service unavailable - inference backend is down - try: - error = response.json() - raise ServiceUnavailableError( - f"Service unavailable: {error.get('detail', 'Inference backend is unavailable')}", + elif response.status_code == 503: + last_exc = ServiceUnavailableError( + f"Service unavailable: {detail_msg}", status_code=503, error_details=error ) - except (json.JSONDecodeError, ValueError): - raise ServiceUnavailableError("Service unavailable: Inference backend is unavailable") + else: + last_exc = APIError( + f"Unexpected status code: {response.status_code} {detail_msg}", + status_code=response.status_code, + error_details=error + ) + + if attempt < self.max_retries: + logger.warning("Got retryable status %d: %s", response.status_code, detail_msg) + continue + raise last_exc else: - # Unexpected status code try: unexp_detail = response.json() if not isinstance(unexp_detail, dict): @@ -774,13 +786,17 @@ class SecureCompletionClient: status_code=response.status_code ) - except httpx.NetworkError as e: - raise APIConnectionError(f"Failed to connect to router: {e}") - except (SecurityError, APIError, AuthenticationError, InvalidRequestError, ForbiddenError, RateLimitError, ServerError, ServiceUnavailableError, APIConnectionError): - raise # Re-raise known exceptions - except Exception: - logger.exception("Unexpected error in send_secure_request") - raise APIConnectionError("Request failed due to an unexpected error") + except httpx.NetworkError as e: + last_exc = APIConnectionError(f"Failed to connect to router: {e}") + if attempt < self.max_retries: + logger.warning("Network error on attempt %d: %s", attempt, e) + continue + raise last_exc + except (SecurityError, APIError, AuthenticationError, InvalidRequestError, ForbiddenError, RateLimitError, ServerError, ServiceUnavailableError, APIConnectionError): + raise # Non-retryable — propagate immediately + except Exception: + logger.exception("Unexpected error in send_secure_request") + raise APIConnectionError("Request failed due to an unexpected error") def _validate_rsa_key(self, key, key_type: str = "private") -> None: """ diff --git a/nomyo/__init__.py b/nomyo/__init__.py index 0a81157..5773045 100644 --- a/nomyo/__init__.py +++ b/nomyo/__init__.py @@ -51,6 +51,6 @@ try: except ImportError: pass -__version__ = "0.2.5" +__version__ = "0.2.6" __author__ = "NOMYO AI" __license__ = "Apache-2.0" diff --git a/nomyo/nomyo.py b/nomyo/nomyo.py index 682997b..95709b2 100644 --- a/nomyo/nomyo.py +++ b/nomyo/nomyo.py @@ -52,7 +52,7 @@ class SecureChatCompletion: ``` """ - def __init__(self, base_url: str = "https://api.nomyo.ai", allow_http: bool = False, api_key: Optional[str] = None, secure_memory: bool = True, key_dir: Optional[str] = None): + def __init__(self, base_url: str = "https://api.nomyo.ai", allow_http: bool = False, api_key: Optional[str] = None, secure_memory: bool = True, key_dir: Optional[str] = None, max_retries: int = 2): """ Initialize the secure chat completion client. @@ -68,8 +68,10 @@ class SecureChatCompletion: Set to False for testing or when security is not required. key_dir: Directory to load/save RSA keys. If None, ephemeral keys are generated in memory for this session only. + max_retries: Number of retries on retryable errors (429, 500, 502, 503, 504, + network errors). Uses exponential backoff. Default 2. """ - self.client = SecureCompletionClient(router_url=base_url, allow_http=allow_http, secure_memory=secure_memory) + self.client = SecureCompletionClient(router_url=base_url, allow_http=allow_http, secure_memory=secure_memory, max_retries=max_retries) self._keys_initialized = False self._keys_lock = asyncio.Lock() self.api_key = api_key diff --git a/pyproject.toml b/pyproject.toml index 681ba9a..5576a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "nomyo" -version = "0.2.5" +version = "0.2.6" description = "OpenAI-compatible secure chat client with end-to-end encryption for NOMYO Inference Endpoints" authors = [ {name = "NOMYO.AI", email = "ichi@nomyo.ai"}, From 1fad4f15f07af79853a85810a06a1dc942c999ab Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Fri, 17 Apr 2026 17:59:14 +0200 Subject: [PATCH 03/19] doc: demo credentials updated --- README.md | 11 +++++++++++ doc/getting-started.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/README.md b/README.md index c10b898..32c6c81 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,17 @@ ## 🚀 Quick Start +### 0. Try It Now (Demo Credentials) + +No account needed — use these public demo credentials to test immediately: + +| | | +|---|---| +| **API key** | `NOMYO_AI_E2EE_INFERENCE` | +| **Model** | `Qwen/Qwen3-0.6B` | + +> **Note:** The demo endpoint uses a fixed 256-token context window and is intended for evaluation only. + ### 1. Install methods via pip (recommended): diff --git a/doc/getting-started.md b/doc/getting-started.md index 4ccdf82..1cf78a4 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -1,5 +1,33 @@ # Getting Started +## Try It Now (Demo Credentials) + +You can test the client immediately using these public demo credentials — no sign-up required: + +| | | +|---|---| +| **API key** | `NOMYO_AI_E2EE_INFERENCE` | +| **Model** | `Qwen/Qwen3-0.6B` | + +> **Note:** The demo endpoint uses a fixed 256-token context window and is intended for evaluation only. + +```python +import asyncio +from nomyo import SecureChatCompletion + +async def main(): + client = SecureChatCompletion(api_key="NOMYO_AI_E2EE_INFERENCE") + + response = await client.create( + model="Qwen/Qwen3-0.6B", + messages=[{"role": "user", "content": "Hello!"}] + ) + + print(response['choices'][0]['message']['content']) + +asyncio.run(main()) +``` + ## Basic Usage The NOMYO client provides end-to-end encryption (E2E) for all communications between your application and the NOMYO inference endpoints. This ensures that your prompts and responses are protected from unauthorized access or interception. From e440a7c43b6234864cd8d6a105d7fd3fefc9634a Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Sat, 18 Apr 2026 15:38:03 +0200 Subject: [PATCH 04/19] fix: adding missing import doc: added TPM attestation related information --- doc/api-reference.md | 20 +++++++++ doc/security-guide.md | 79 ++++++++++++++++++++++++++++++++- nomyo/SecureCompletionClient.py | 2 +- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/doc/api-reference.md b/doc/api-reference.md index 363c865..1069082 100644 --- a/doc/api-reference.md +++ b/doc/api-reference.md @@ -75,10 +75,30 @@ A dictionary containing the chat completion response with the following structur "prompt_tokens": int, "completion_tokens": int, "total_tokens": int + }, + "_metadata": { + "payload_id": str, + "processed_at": int, # Unix timestamp + "is_encrypted": bool, + "response_status": str, + "security_tier": str, # "standard", "high", or "maximum" + "memory_protection": dict, # server-side memory protection info + "cuda_device": dict, # privacy-safe GPU info (hashed identifiers) + "tpm_attestation": { # TPM 2.0 hardware attestation (see Security Guide) + "is_available": bool, + # Present only when is_available is True: + "pcr_banks": str, # e.g. "sha256:0,7,10" + "pcr_values": dict, # {bank: {pcr_index: hex_digest}} + "quote_b64": str, # base64-encoded TPMS_ATTEST (signed by AIK) + "signature_b64": str, # base64-encoded TPMT_SIGNATURE + "aik_pubkey_b64": str, # base64-encoded TPM2B_PUBLIC (ephemeral AIK) + } } } ``` +The `_metadata` field is added by the client library and is not part of the OpenAI API response format. See the [Security Guide](security-guide.md) for how to interpret and verify `tpm_attestation`. + #### acreate(model, messages, **kwargs) Async alias for create() method. diff --git a/doc/security-guide.md b/doc/security-guide.md index 6c34f71..6e4abdc 100644 --- a/doc/security-guide.md +++ b/doc/security-guide.md @@ -162,6 +162,81 @@ Secure memory features: - Guarantees zeroing of sensitive memory - Prevents memory dumps from containing sensitive data +## Hardware Attestation (TPM 2.0) + +### What it is + +When the server has a TPM 2.0 chip, every response includes a `tpm_attestation` block in `_metadata`. This is a cryptographically signed hardware quote proving: + +- Which firmware and Secure Boot state the server is running (PCR 0, 7) +- Which application binary is running, when IMA is active (PCR 10) + +The quote is signed by an ephemeral AIK (Attestation Identity Key) generated fresh for each request and tied to the `payload_id` nonce, so it cannot be replayed for a different request. + +### Reading the attestation + +```python +response = await client.create( + model="Qwen/Qwen3-0.6B", + messages=[{"role": "user", "content": "..."}], + security_tier="maximum" +) + +tpm = response["_metadata"].get("tpm_attestation", {}) + +if tpm.get("is_available"): + print("PCR banks:", tpm["pcr_banks"]) # e.g. "sha256:0,7,10" + print("PCR values:", tpm["pcr_values"]) # {bank: {index: hex}} + print("AIK key:", tpm["aik_pubkey_b64"][:32], "...") +else: + print("TPM not available on this server") +``` + +### Verifying the quote + +The response is self-contained: `aik_pubkey_b64` is the full public key of the AIK that signed the quote, so no separate key-fetch round-trip is needed. + +Verification steps using `tpm2-pytss`: + +```python +import base64 +from tpm2_pytss.types import TPM2B_PUBLIC, TPMT_SIGNATURE, TPM2B_ATTEST + +# 1. Decode the quote components +aik_pub = TPM2B_PUBLIC.unmarshal(base64.b64decode(tpm["aik_pubkey_b64"]))[0] +quote = TPM2B_ATTEST.unmarshal(base64.b64decode(tpm["quote_b64"]))[0] +sig = TPMT_SIGNATURE.unmarshal(base64.b64decode(tpm["signature_b64"]))[0] + +# 2. Verify the signature over the quote using the AIK public key +# (use a TPM ESAPI verify_signature call or an offline RSA verify) + +# 3. Inspect the qualifying_data inside the quote — it must match +# SHA-256(payload_id.encode())[:16] to confirm this quote is for this request + +# 4. Check pcr_values against your known-good baseline +``` + +> Full verification requires `tpm2-pytss` on the client side (`pip install tpm2-pytss` + `sudo apt install libtss2-dev`). It is optional — the attestation is informational unless your deployment policy requires verification. + +### Behaviour per security tier + +| Tier | TPM unavailable | +|------|----------------| +| `standard` | `tpm_attestation: {"is_available": false}` — request proceeds | +| `high` | same as standard | +| `maximum` | `ServiceUnavailableError` (HTTP 503) — request rejected | + +For `maximum` tier, the server enforces TPM availability as a hard requirement. If your server has no TPM and you request `maximum`, catch the error explicitly: + +```python +from nomyo import ServiceUnavailableError + +try: + response = await client.create(..., security_tier="maximum") +except ServiceUnavailableError as e: + print("Server does not meet TPM requirements for maximum tier:", e) +``` + ## Compliance Considerations ### HIPAA Compliance @@ -207,9 +282,11 @@ response = await client.create( messages=[{"role": "user", "content": "Hello"}] ) -print(response["_metadata"]) # Contains security-related information +print(response["_metadata"]) # Contains security_tier, memory_protection, tpm_attestation, etc. ``` +See [Hardware Attestation](#hardware-attestation-tpm-20) for details on the `tpm_attestation` field. + ### Logging Enable logging to see security operations: diff --git a/nomyo/SecureCompletionClient.py b/nomyo/SecureCompletionClient.py index 4c1d96d..6aa5379 100644 --- a/nomyo/SecureCompletionClient.py +++ b/nomyo/SecureCompletionClient.py @@ -1,5 +1,5 @@ import asyncio, ctypes, json, base64, urllib.parse, httpx, os, secrets, sys, warnings, logging -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, Union from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.backends import default_backend From 1330a1068f9ad5484e525c9c77d1d2a9fb8ce39d Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Sat, 18 Apr 2026 15:48:21 +0200 Subject: [PATCH 05/19] bump: version --- nomyo/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nomyo/__init__.py b/nomyo/__init__.py index 5773045..6fb55fe 100644 --- a/nomyo/__init__.py +++ b/nomyo/__init__.py @@ -51,6 +51,6 @@ try: except ImportError: pass -__version__ = "0.2.6" +__version__ = "0.2.7" __author__ = "NOMYO AI" __license__ = "Apache-2.0" diff --git a/pyproject.toml b/pyproject.toml index 5576a31..d0f08cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "nomyo" -version = "0.2.6" +version = "0.2.7" description = "OpenAI-compatible secure chat client with end-to-end encryption for NOMYO Inference Endpoints" authors = [ {name = "NOMYO.AI", email = "ichi@nomyo.ai"}, From 4cfc98de9dd556ced880d91beb721cd85dee2152 Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Tue, 5 May 2026 09:18:19 +0200 Subject: [PATCH 06/19] fix: timeouts --- nomyo/SecureCompletionClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nomyo/SecureCompletionClient.py b/nomyo/SecureCompletionClient.py index 6aa5379..9c503a8 100644 --- a/nomyo/SecureCompletionClient.py +++ b/nomyo/SecureCompletionClient.py @@ -301,7 +301,7 @@ class SecureCompletionClient: verify_ssl = self.router_url.startswith("https://") async with httpx.AsyncClient( - timeout=60.0, + timeout=900.0, verify=verify_ssl, # Verify SSL/TLS certificates for HTTPS ) as client: response = await client.get(url) @@ -673,7 +673,7 @@ class SecureCompletionClient: await asyncio.sleep(delay) try: - async with httpx.AsyncClient(timeout=60.0) as client: + async with httpx.AsyncClient(timeout=900.0) as client: response = await client.post( url, headers=headers, From a9c3562088b91f6b916b70409f90aaab332bc782 Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Tue, 5 May 2026 09:18:45 +0200 Subject: [PATCH 07/19] bump: version --- nomyo/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nomyo/__init__.py b/nomyo/__init__.py index 6fb55fe..a4aa26a 100644 --- a/nomyo/__init__.py +++ b/nomyo/__init__.py @@ -51,6 +51,6 @@ try: except ImportError: pass -__version__ = "0.2.7" +__version__ = "0.2.8" __author__ = "NOMYO AI" __license__ = "Apache-2.0" diff --git a/pyproject.toml b/pyproject.toml index d0f08cb..74c4203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "nomyo" -version = "0.2.7" +version = "0.2.8" description = "OpenAI-compatible secure chat client with end-to-end encryption for NOMYO Inference Endpoints" authors = [ {name = "NOMYO.AI", email = "ichi@nomyo.ai"}, From 2cb2cc6f77295c734c4fec3cbd07dd276224cbd6 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Sat, 9 May 2026 20:14:19 +0000 Subject: [PATCH 08/19] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..6af7325 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>nomyo-ai/renovate-config" + ] +} From 0b8c52e0daae1c7c89e2afccdbb70d3072222b6a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 9 May 2026 20:17:43 +0000 Subject: [PATCH 09/19] fix(deps): update dependency cryptography to v46.0.7 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74c4203..3c4bbcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "anyio==4.12.0", "certifi==2025.11.12", "cffi==2.0.0", - "cryptography==46.0.6", + "cryptography==46.0.7", "exceptiongroup==1.3.1", "h11==0.16.0", "httpcore==1.0.9", diff --git a/requirements.txt b/requirements.txt index 868c9f0..74240a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ anyio==4.12.0 certifi==2025.11.12 cffi==2.0.0 -cryptography==46.0.6 +cryptography==46.0.7 exceptiongroup==1.3.1 h11==0.16.0 httpcore==1.0.9 From e44c11e8965ca355523e5ce6d8a73890eecd468c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 9 May 2026 20:18:02 +0000 Subject: [PATCH 10/19] fix(deps): update dependency anyio to v4.13.0 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74c4203..fd2129c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] requires-python = ">=3.10" dependencies = [ - "anyio==4.12.0", + "anyio==4.13.0", "certifi==2025.11.12", "cffi==2.0.0", "cryptography==46.0.6", diff --git a/requirements.txt b/requirements.txt index 868c9f0..affcdad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -anyio==4.12.0 +anyio==4.13.0 certifi==2025.11.12 cffi==2.0.0 cryptography==46.0.6 From 8c3de5bfe40842e8da23585fbbe308eff2c2326f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 9 May 2026 20:18:10 +0000 Subject: [PATCH 11/19] fix(deps): update dependency idna to v3.13 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74c4203..3c815f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "h11==0.16.0", "httpcore==1.0.9", "httpx==0.28.1", - "idna==3.11", + "idna==3.13", "pycparser==2.23", "typing_extensions==4.15.0", ] diff --git a/requirements.txt b/requirements.txt index 868c9f0..4ff952c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ exceptiongroup==1.3.1 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 -idna==3.11 +idna==3.13 pycparser==2.23 typing_extensions==4.15.0 From c65f01b59cc2743fd0d9480ab0a0c9a3f4a2243f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 9 May 2026 20:19:39 +0000 Subject: [PATCH 12/19] fix(deps): update dependency certifi to v2026 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e924a8b..a4beec6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "anyio==4.13.0", - "certifi==2025.11.12", + "certifi==2026.4.22", "cffi==2.0.0", "cryptography==46.0.7", "exceptiongroup==1.3.1", diff --git a/requirements.txt b/requirements.txt index 18b3715..655c010 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ anyio==4.13.0 -certifi==2025.11.12 +certifi==2026.4.22 cffi==2.0.0 cryptography==46.0.7 exceptiongroup==1.3.1 From 1452795e9ab124fdd5dcc4cd17ad83530f424341 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 9 May 2026 21:00:48 +0000 Subject: [PATCH 13/19] fix(deps): update dependency cryptography to v48 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e924a8b..c4bdc3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "anyio==4.13.0", "certifi==2025.11.12", "cffi==2.0.0", - "cryptography==46.0.7", + "cryptography==48.0.0", "exceptiongroup==1.3.1", "h11==0.16.0", "httpcore==1.0.9", diff --git a/requirements.txt b/requirements.txt index 18b3715..e6493c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ anyio==4.13.0 certifi==2025.11.12 cffi==2.0.0 -cryptography==46.0.7 +cryptography==48.0.0 exceptiongroup==1.3.1 h11==0.16.0 httpcore==1.0.9 From 81892b3e4e035953593ad2c08603e84898f38127 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 9 May 2026 21:00:57 +0000 Subject: [PATCH 14/19] fix(deps): update dependency pycparser to v3 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e924a8b..715bbf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "httpcore==1.0.9", "httpx==0.28.1", "idna==3.13", - "pycparser==2.23", + "pycparser==3.0", "typing_extensions==4.15.0", ] diff --git a/requirements.txt b/requirements.txt index 18b3715..cd4d585 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,5 @@ h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.13 -pycparser==2.23 +pycparser==3.0 typing_extensions==4.15.0 From c76e48c7388411ae86707ac892e05b46982484ee Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 10 May 2026 20:39:38 +0000 Subject: [PATCH 15/19] fix(deps): update dependency idna to v3.14 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 927020f..2199bb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "h11==0.16.0", "httpcore==1.0.9", "httpx==0.28.1", - "idna==3.13", + "idna==3.14", "pycparser==3.0", "typing_extensions==4.15.0", ] diff --git a/requirements.txt b/requirements.txt index 46688c9..5ac5bfb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ exceptiongroup==1.3.1 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 -idna==3.13 +idna==3.14 pycparser==3.0 typing_extensions==4.15.0 From 8b90f9224f8aca769929c1f1bf5668fe988e191d Mon Sep 17 00:00:00 2001 From: alpha nerd Date: Mon, 11 May 2026 10:27:33 +0200 Subject: [PATCH 16/19] bump: version --- nomyo/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nomyo/__init__.py b/nomyo/__init__.py index a4aa26a..38d9bef 100644 --- a/nomyo/__init__.py +++ b/nomyo/__init__.py @@ -51,6 +51,6 @@ try: except ImportError: pass -__version__ = "0.2.8" +__version__ = "0.2.9" __author__ = "NOMYO AI" __license__ = "Apache-2.0" diff --git a/pyproject.toml b/pyproject.toml index 2199bb2..f5820c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "nomyo" -version = "0.2.8" +version = "0.2.9" description = "OpenAI-compatible secure chat client with end-to-end encryption for NOMYO Inference Endpoints" authors = [ {name = "NOMYO.AI", email = "ichi@nomyo.ai"}, From daa277243902717af1d2e04cf9f1fbf094693651 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 13 May 2026 08:29:06 +0000 Subject: [PATCH 17/19] fix(deps): update dependency idna to v3.15 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f5820c2..a5f2124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "h11==0.16.0", "httpcore==1.0.9", "httpx==0.28.1", - "idna==3.14", + "idna==3.15", "pycparser==3.0", "typing_extensions==4.15.0", ] diff --git a/requirements.txt b/requirements.txt index 5ac5bfb..c53cfd7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ exceptiongroup==1.3.1 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 -idna==3.14 +idna==3.15 pycparser==3.0 typing_extensions==4.15.0 From b9fbf4ef729ccef6b0d65a07b32d3b7dc64dd38d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 20 May 2026 11:53:55 +0000 Subject: [PATCH 18/19] fix(deps): update dependency certifi to v2026.5.20 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a5f2124..c3b83bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "anyio==4.13.0", - "certifi==2026.4.22", + "certifi==2026.5.20", "cffi==2.0.0", "cryptography==48.0.0", "exceptiongroup==1.3.1", diff --git a/requirements.txt b/requirements.txt index c53cfd7..d5cbcea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ anyio==4.13.0 -certifi==2026.4.22 +certifi==2026.5.20 cffi==2.0.0 cryptography==48.0.0 exceptiongroup==1.3.1 From ed25554b45706973fde6a3ebd18c4a928a308536 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 22 May 2026 00:24:17 +0000 Subject: [PATCH 19/19] fix(deps): update dependency idna to v3.16 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c3b83bb..c42fef8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "h11==0.16.0", "httpcore==1.0.9", "httpx==0.28.1", - "idna==3.15", + "idna==3.16", "pycparser==3.0", "typing_extensions==4.15.0", ] diff --git a/requirements.txt b/requirements.txt index d5cbcea..2e17834 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ exceptiongroup==1.3.1 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 -idna==3.15 +idna==3.16 pycparser==3.0 typing_extensions==4.15.0