Offline Mode
Offline mode provides full cryptographic attestation without requiring an API key. Perfect for development, testing, and air-gapped environments.
Quick Start
Section titled “Quick Start”import osfrom glacis import Glacis
# No API key needed — just a 32-byte signing seedglacis = Glacis(mode="offline", signing_seed=os.urandom(32))
receipt = glacis.attest( service_id="local-dev", operation_type="inference", input={"prompt": "What is 2+2?"}, output={"response": "4"},)
print(f"Receipt ID: {receipt.id}") # oatt_xxx (offline attestation)print(f"Witness status: {receipt.witness_status}") # "UNVERIFIED"print(f"Signature: {receipt.signature[:32]}...") # Ed25519 signatureHow It Works
Section titled “How It Works”In offline mode, the SDK:
- Generates a local Ed25519 keypair from the
signing_seedyou provide - Hashes payloads using SHA-256 with RFC 8785 canonical JSON
- Signs receipts locally with the Ed25519 private key
- Stores receipts in a local SQLite database at
~/.glacis/glacis.db
Offline vs Online
Section titled “Offline vs Online”| Feature | Offline | Online |
|---|---|---|
| API key required | No | Yes |
| Signing seed required | Yes (32 bytes) | No |
| Signing | Local Ed25519 | Glacis witness network |
| Merkle proofs | No | Yes |
| Transparency log | No | Yes |
| Verification URL | No | Yes |
| Witness status | "UNVERIFIED" | "WITNESSED" |
Providing Your Own Seed
Section titled “Providing Your Own Seed”The signing_seed parameter is required for offline mode and must be exactly 32 bytes. You can generate one randomly or derive it deterministically for testing:
import osfrom glacis import Glacis
# Provide a cryptographically secure random seed (recommended for production)seed = os.urandom(32)glacis = Glacis(mode="offline", signing_seed=seed)
# Or derive from a passphrase (for testing only!)import hashlibseed = hashlib.sha256(b"my-test-seed").digest()glacis = Glacis(mode="offline", signing_seed=seed)Seed Persistence
Section titled “Seed Persistence”For production use, generate the seed once and store it in a secrets manager or environment variable. Reuse the same seed across sessions so you can always verify previously signed receipts.
# Generate once, store securely:import osprint(os.urandom(32).hex())Then load it at runtime:
import osfrom glacis import Glacis
# Production: load seed from environment or secrets managerseed = bytes.fromhex(os.environ["GLACIS_SIGNING_SEED"])
glacis = Glacis(mode="offline", signing_seed=seed)Local Storage
Section titled “Local Storage”Offline receipts are stored in SQLite at ~/.glacis/glacis.db. You can retrieve the most recent receipt using get_last_receipt():
import osfrom glacis import Glacis
glacis = Glacis(mode="offline", signing_seed=os.urandom(32))
# Create a receiptreceipt = glacis.attest( service_id="local-dev", operation_type="inference", input={"prompt": "Hello"}, output={"response": "Hi!"},)
# Retrieve the last receiptlast = glacis.get_last_receipt()if last: print(f"{last.id}: {last.timestamp}")For more storage options including JSONL and custom paths, see Storage Backends.
Verifying Offline Receipts
Section titled “Verifying Offline Receipts”import osfrom glacis import Glacis
glacis = Glacis(mode="offline", signing_seed=os.urandom(32))
# Create a receiptreceipt = glacis.attest( service_id="test", operation_type="inference", input={"prompt": "test"}, output={"response": "result"},)
# Verify the signatureresult = glacis.verify(receipt)print(f"Signature valid: {result.signature_valid}") # Trueprint(f"Overall valid: {result.valid}") # TrueThe verify() method returns an OfflineVerifyResult with these fields:
| Field | Type | Description |
|---|---|---|
valid | bool | Whether the attestation is valid overall |
witness_status | str | Always "UNVERIFIED" for offline receipts |
signature_valid | bool | Whether the Ed25519 signature is valid |
attestation | Attestation | None | The verified attestation object |
error | str | None | Error message if verification failed |
CLI Verification
Section titled “CLI Verification”Verify offline receipts from the command line:
# Save receipt to filepython -c "import os, jsonfrom glacis import Glacis
glacis = Glacis(mode='offline', signing_seed=os.urandom(32))receipt = glacis.attest( service_id='test', operation_type='test', input={'test': True}, output={'ok': True})print(json.dumps(receipt.model_dump(), indent=2))" > receipt.json
# Verifypython -m glacis verify receipt.jsonOutput:
Receipt: oatt_abc123...Type: Offline
Status: VALID Signature: PASSWhen to Use Offline Mode
Section titled “When to Use Offline Mode”| Scenario | Recommendation |
|---|---|
| Local development | Offline mode |
| CI/CD testing | Offline mode |
| Air-gapped environments | Offline mode |
| Internal audits only | Offline mode (acceptable) |
| External audits | Online mode (recommended) |
| Customer due diligence | Online mode (required) |
| Published research | Online mode (recommended) |
Upgrading to Online Mode
Section titled “Upgrading to Online Mode”When you need third-party verifiability:
import osfrom glacis import Glacis
# Before: offlineglacis = Glacis(mode="offline", signing_seed=os.urandom(32))
# After: onlineglacis = Glacis(api_key="glsk_live_...")That’s it. The attest() and verify() API is identical — only the witness status changes from "UNVERIFIED" to "WITNESSED".