Develop for Enclave: WebAssembly app guide, architecture, CLI, MCP, and API reference
Develop
From idea to enclave.
Everything needed to understand and ship a confidential app: follow the guide from an empty directory to an attested endpoint, inspect the architecture and protection levels, drive every operation with the CLI or MCP, and use the API reference for every call the platform answers.
Build your first Wasm app.
This guide takes you from an empty directory to a paid, attested, publicly reachable app: first the Hello walkthrough end to end, then a worked example for every capability the platform gives your code. That covers HTTP handling, the /data scratch filesystem, raw TCP/UDP service ports, GPU inference, user-held-key encrypted volumes, and attestation your users can check themselves. Everything here runs against the same public API the store's Deploy button uses. The package you build is the same on every backend; the protection level belongs to the host it lands on, and the Architecture tab says what each one guarantees.
01The app model
An Enclave app is a WebAssembly component that targets wasi:http, the format wasmtime serve runs. It exports one interface, wasi:http/incoming-handler: the runtime hands your code an HTTP request, your code returns a response. Like a serverless function, except it runs inside a hardware-attested confidential enclave, and anyone can verify that.
Inside the enclave your app runs in a strict capability sandbox, in its own OS process, launched by the exact same wasmtime command you can run locally:
Filesystem: a single private, writable /data directory (RAM-backed scratch, torn down with the deployment). Nothing else is visible, and paths that escape it (/data/../..) are refused by the runtime. Chapter 08 →
Environment: empty, except what the platform hands you deliberately - ENCLAVE_PORTS for service apps, ENCLAVE_HOSTS with every hostname this deployment answers on (Chapter 14 →), ENCLAVE_MEM_MB and ENCLAVE_AVAILABLE_PARALLELISM with the memory and thread count this deployment's share actually carries, and the config/secrets channels described later. No host env ever leaks in.
Network: none by default beyond the HTTP socket the runtime serves for you. Declaring open ports switches the app to service mode with wasi:sockets. Chapter 09 →
GPU: none by default. A deployment that buys a GPU share gets wasi-nn - ONNX, GGUF (llama.cpp) and image-diffusion (stable-diffusion.cpp) inference through the enclave's runtime; on today's hosts the card is reached through Enclave Shield's masked offload. Chapter 12 →
Memory: the guest's linear memory is capped at the deployment's CPU share of the node's RAM (cpuShare × 64 GB), enforced by the runtime - always at least the memMb the app's specs declare, since the specs floor the share.
No host reach: no processes, no exec, no SSH (a Wasm app isn't an OS; there's nothing to SSH into, and the platform ships no SSH channel at all).
Two kinds of app, one format:
Web apps (the default, and this guide's walkthrough): export the handler; the platform owns the listener and routes requests to you.
Service apps: declare open ports (http:N / tcp:N / udp:N) and run as a long-lived command component that binds its own sockets: databases, IRC servers, DNS resolvers. Chapter 09 →
And one catalog every app reference points at:
The on-chain app store: the Apps tab: anyone publishes, bytes live on IPFS addressed by CID, listings live in the EnclaveAppCatalog contract on Base. First-party samples (hello-world, nn-demo, llm-chat) are published there too - the enclave image ships no deployable apps, so every deploy resolves to a CID the enclave fetches and hash-verifies. Chapter 04 →
The contract
If wasmtime serve runs your .wasm locally, the enclave runs it too: the manager launches the identical command per tenant. That's the whole compatibility story, and it's why every chapter here ends with a local command you can verify before spending a cent. The same component also runs, interpreted, inside the Windows host's enclave; a versioned domain contract shared by every backend is in development and described in the Architecture tab.
02Toolchain setup
The walkthrough uses Rust, the most mature component toolchain today (other languages in chapter 15). You need the WASI 0.2 target, cargo-component, and, for local testing, the same wasmtime runtime the enclave uses (WASI 0.3 async components are also served - their pinned toolchain is at the end of chapter 03):
terminal
# Rust component toolchain
rustup target add wasm32-wasip2
cargo install cargo-component
# the runtime the enclave runs (wasmtime 49 line), for local testing
curl https://wasmtime.dev/install.sh -sSf | bash
wasmtime --version
03Hello, enclave
Five steps from nothing to a running app: scaffold, declare the world, write the handler, build, run. The result is byte-for-byte the kind of component the platform's own hello-world sample is.
Step 1: scaffold a component crate
terminal
cargo component new hello --lib && cd hello
cargo add wasi # WASI 0.2 bindings
Step 2: declare the world
The world tells the toolchain what your component imports and exports. An Enclave web app exports exactly one thing:
wit/world.wit
package enclave:hello;
world hello {
export wasi:http/incoming-handler@0.2.0;
}
Step 3: write the handler
src/lib.rs
use wasi::http::types::{
Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam,
};
wasi::http::proxy::export!(Component);
struct Component;
impl wasi::exports::http::incoming_handler::Guest for Component {
fn handle(_req: IncomingRequest, out: ResponseOutparam) {
// 1. make a response and take its body handle
let resp = OutgoingResponse::new(Fields::new());
let body = resp.body().unwrap();
// 2. hand the response to the host FIRST, then stream the body
ResponseOutparam::set(out, Ok(resp));
let stream = body.write().unwrap();
stream.blocking_write_and_flush(b"hello from Enclave\n").unwrap();
drop(stream);
// 3. finishing the body is what completes the response
OutgoingBody::finish(body, None).unwrap();
}
}
Version drift
The wasi crate's binding APIs shift between releases. Pin the version documented for your cargo-component and let the compiler guide the exact handler signature; the shape above (proxy::export!, set the outparam, write, finish) is the stable wasi:http pattern.
This emits a component (layer 1 in the wasm preamble), not a plain core module. The upload gateway and the enclave both check that field and reject core modules, so anything that builds this way is already in the right format.
Works? Then it works in the enclave. For a full dress rehearsal, add the exact flags the enclave launches with, giving you the same sandbox, memory cap, and /data preopen your deployment will get (the cap is your deployment's CPU share of the node's RAM - at least your published memMb; the 128 MB minimum shown here):
wasm32-wasip3 is the component model's native-async target: one instance serves many in-flight requests, imports don't block it, and request/response bodies are first-class streams - the right substrate for agent apps whose time goes to model and tool calls. The enclave serves both worlds side by side. The simplest guest route is the wasip3 crate - no WIT files in your repo at all. Match the WIT the fleet serves: the pinned wasmtime (the 49 line) serves wasi:http@0.3.0 final, so use a crate line whose build metadata reads +wasi-0.3.0 (0.7.x and newer); 0.6.0 targets the older 0.3.0-rc-2026-03-15 snapshot and will not instantiate against it.
Cargo.toml
[lib]
crate-type = ["cdylib"]
[dependencies]
wasip3 = { version = "0.7", features = ["async-spawn"] } # 0.7.x+ target wasi 0.3.0 final
src/lib.rs · the whole app
use wasip3::http::types::{ErrorCode, Fields, Request, Response};
use wasip3::{wit_future, wit_stream};
wasip3::http::service::export!(Hello);
struct Hello;
impl wasip3::exports::http::handler::Guest for Hello {
async fn handle(_req: Request) -> Result<Response, ErrorCode> {
let (mut body_tx, body_rx) = wit_stream::new();
let (result_tx, result_rx) = wit_future::new(|| Ok(None));
let (resp, _transmit) = Response::new(Fields::new(), Some(body_rx), result_rx);
drop(result_tx);
wasip3::spawn(async move { // stream the body after returning the response
let leftover = body_tx.write_all(b"hello from wasip3\n".to_vec()).await;
assert!(leftover.is_empty());
});
Ok(resp)
}
}
Stable Rust lists the target but ships no std for it (and the shipped linkers predate a flag the builtin target emits), so p3 builds go through the platform's blessed toolchain container (wasm/Dockerfile.wasip3-build in the platform repo). It carries a pinned nightly with rust-src, a wasip3 wasi-libc it builds itself from the upstream-pinned revision, a stripped copy of rustc's own target spec, and every determinism flag baked in - the same source and lockfile give everyone the same bytes, so commit your Cargo.lock:
terminal
docker build -f wasm/Dockerfile.wasip3-build -t enclave-wasip3-build wasm/
docker run --rm -v "$PWD":/src --entrypoint cargo enclave-wasip3-build generate-lockfile # once
docker run --rm -v "$PWD":/src enclave-wasip3-build
# artifact: target/wasm32-wasip3/release/<app>.wasm - no custom RUSTFLAGS,
# no shim crates, no spec files in YOUR repo; the container carries all of it
wasmtime serve -Scli -Shttp -Sp3 -W component-model-async \
--addr 127.0.0.1:8080 target/wasm32-wasip3/release/hello.wasm
for i in $(seq 8); do curl -s 127.0.0.1:8080/ & done; wait # concurrent on ONE instance
Publishing is unchanged - the upload path reads your component's exports and stamps the detected world ("wasi": "0.3") into the version config, so only p3-capable enclaves claim it (per-box p3 on /availability; ANDed fleet-wide when every runner serves it). Notes:
Instances are reused and concurrent. Up to 16 requests share one instance (and its linear memory and globals) before it's recycled after ~128 requests or 1s idle. That's the feature - warm state across an agent session - but if your handler assumes per-request isolation, dial it down per version in the config: {"p3": {"maxConcurrent": 1, "maxReuse": 256, "idleSeconds": 5}}. Billing is unchanged either way: you pay per second for the share, never per request.
wasi-nn works via the 0.2 bridge. Import wasi:nn@0.2.0-rc-2024-10-28 from your p3 world exactly as from a p2 one - same backends, same /models preloads. Its compute() is a sync hostcall that stalls the instance's other requests for the duration, so nn apps on the bridge should set "p3": {"maxConcurrent": 1}. The async, streaming wasi:nn@0.3-draft (host pushes tokens over a stream<u8>) is published in the platform repo at wasm/wit/wasi-nn-0.3-draft/ and lands with the next runtime toolchain cut.
Streams still need bytes. The gateway's ~180s cut is an idle timer per hop - any body byte resets it, and SSE comment heartbeats pass through untouched (the proxies are content-type-blind). Same rules as p2: emit early, emit often, and send content-encoding: identity on event streams.
Don't block - it aborts (Rust builds). Until the toolchain moves to an LLVM-23 rustc, the wasip3 libc's futex has the same semantics std documents for p1/p2: a wait that would actually block aborts the instance. In a single-threaded async world that means: never hold a std::sync lock or channel across an .await, and never call blocking std APIs from the handler - use the async primitives (wit_stream/wit_future, wasip3::spawn). Uncontended locks are fine; contention only exists if you parked one across an await. (Cooperative-threads C/C++ builds are the exception: their futex suspends the thread instead - see the threads note below.)
Cooperative threads (C/C++, rolling out). The C image enclave-wasip3c-build compiles pthread code to a wasip3 component whose threads run cooperatively on the instance's async runtime: pthread_create/join, mutexes, condvars and blocking waits all behave - threads interleave (concurrency, not extra cores; a spin-loop never yields, use the primitives). The upload path detects the thread imports and stamps "threads": true so only thread-capable enclaves claim it (per-box coopThreads on /availability, same pattern as p3); until the fleet's runtime cut lands, expect the capability to read false fleet-wide. Rust std::thread follows automatically when rustc lands on LLVM 23 - no action on your side.
Release LTO is forced to thin by the container (a cargo config override, so it wins over your Cargo.toml): fat LTO reproducibly fails with a duplicate cabi_realloc between std's wit-bindgen copy and the app's own.
04Publish to the app store
Your app's bytes go on IPFS, addressed by their CID (a hash of the exact wasm). The listing goes on Base, in the EnclaveAppCatalog contract. Publishing is one wallet transaction on the Apps tab:
Connect your wallet (header button) and hit + Publish app.
Pick a slug, your app's stable id, e.g. my-hello (a–z, 0–9, hyphens, ≤ 40 chars). Slugs live in your wallet's namespace: the on-chain app id is keccak256(publisher, slug), so nobody can squat or write to your lineage. The slug doesn't have to match your crate name.
Set a version label (e.g. 1.0.0) and choose your hello.wasm. The file is validated (component check, ≤ 2 GB), its world contract is detected from the exports (wasip2 or wasip3 - stamped into the version config as wasi so runners route p3 work to p3-capable boxes), uploaded, pinned to IPFS, and the CID field fills in (or paste a CID you pinned yourself).
Set the display name, a description, and the app's exact minimum resources on four axes: the VRAM (GB) and GPU compute (TFLOPS) it needs on one card (both 0 for CPU-only apps, which is most of them), and the RAM (MB - also the runtime-enforced memory cap; the Hello app is fine at 128) and CPU compute (GFLOPS, at least 1 - every app computes something - and note the unit: a whole 16-vCPU node is only ≈1000 GFLOPS ≈ 1 TFLOPS, so declaring the node's full GFLOPS claims the entire node and forces a 100% CPU share; most small apps want 1-50) it needs on the node. These specs set the MINIMUM shares on the deploy page's two dials (spec ÷ server spec, the larger of the memory and compute axes); deploys below them are refused. Only for service apps, add the open ports the version may bind (chapter 09). Leave ports empty for a standard web app.
Optionally set an hourly fee for running your app. It is stored on the version record, shown to every deployer up front, and paid straight to your publisher wallet: whenever a deployer funds a deployment of your app, your share of that funding is forwarded to your wallet in the same transaction (non-custodial, like all Enclave payments). The fee is capped on-chain by the platform, immutable per version like the ports and config, and covered by the owner's approval - re-pricing means publishing a new version, so an approved app can never silently change its price. Leave it at 0 for a free app.
Publish to Base: one transaction, signed by your wallet, attributed on-chain to you.
Versioning rules (enforced by the contract)
Versions are append-only history: you never edit a release, you publish the same slug again with a new version label and CID. editApp changes display metadata; setActive delists the app; yankVersion pulls a bad release (kept for history, hidden from browsing).
A release's CID belongs to its app forever: no other app can ever list the same bytes, and yanking or delisting doesn't free it. The same app can re-list its own CID in a new version - that's the metadata fix (same bytes, corrected specs or ports; the add version button pre-fills it) - and the enclaves' deploy gate follows the newest listing of a CID, which starts Pending again (a metadata change is re-approved like any release). After the fix lands, yank the superseded version. Publishing a new version to a delisted slug automatically relists the app.
A CID can be listed once across the whole catalog, so a CID maps unambiguously to one app version.
Each version carries its own open-ports config; it can change from release to release.
Approval gates deploys
Publishing is permissionless, but every version starts Pending and the enclave refuses to deploy its CID (403 not_approved) until the catalog owner approves that exact CID on-chain. A new release starts Pending again. Your app card on the Apps tab shows the per-version status badge. The separate verified badge is a curation signal only: it never gates execution; the CID does.
05Deploy it
A deployment names an app, a slice of the card, and who may reach it. The app reference is always a catalog listing:
reference
meaning
my-hello:1.0.0
A store app by slug and version (or <publisher>/my-hello:1.0.0 to disambiguate). It resolves against the on-chain catalog to that version's exact ipfs://<cid>; the enclave fetches those bytes, verifies every block hashes to the CID (a tampering gateway fails the check), confirms it's a component, and runs it. The verified CID is folded into the attestation.
Unlisted CIDs can't deploy: the catalog listing carries everything a deploy needs on-chain - the resource specs that floor the dials, the open ports, the publisher's hourly fee if the version charges one (shown in the rate before you sign; paid to the publisher's wallet out of each funding), and the owner's approval of that exact CID. Built something new? Publish it first (chapter 04), then deploy the slug.
The easy path is the store: your published app's card has a Deploy button that sizes the shares from the version's specs, applies its open ports and config, and funds it in one step; the model volumes, config, protection and relay of a running deployment are tabs on its dashboard row. No wallet or crypto is needed there: sign in with a passkey (or approve from your phone), add credit by card through Stripe Checkout, and deploy with a dollar budget. Your credit sits in a credit vault that only your passkey can authorize and that can spend only on Enclave compute, and the platform pays every network fee. A wallet adds what credit deployments don't support yet: live logs, the in-browser attestation view, secrets, custom domains, config overrides and apps with a publisher fee.
Agents and direct integrations use the wallet path below, which is the same public API the site speaks:
Create the deployment: one transaction, you own the record
A deployment is an on-chain work item, not one enclave's private state. create() on the EnclaveDeployments contract records the intent - the app's catalog version record (catalog://<appId>/<versionIndex>), the two shares (in 1/1000ths), visibility - under your address, and mints the id (keccak256(sender, nonce), the Created event's first topic). The reference names a version, never bytes: runners take the wasm, config and ports from that immutable record - exactly what the catalog owner approved. (CID references are refused: a CID names bytes, and several versions with different approved configs can share bytes.) Because the ledger holds the spec and the balance, the deployment survives enclave updates and crashes: runners serve it under expiring leases, and when a runner dies, any other enclave picks the work up. Nothing to sign in besides your wallet.
deploy.js · EnclaveDeployments.create()
// create(appRef, gpuMilli, cpuMilli, appPort, ports, isPublic, configCid,
// feeRecipient, feePerSec6)
// appRef = catalog://<appId>/<versionIndex> - the on-chain version record
// shares are 1/1000ths: gpuShare 0.25 -> 250; a GPU app needs gpuMilli >= cpuMilli
// appPort/ports ride along untrusted (the record decides); configCid must be ""
// feeRecipient/feePerSec6 copy the version's publisher fee (its publisher's
// wallet + the per-second fee; 0x0/0 for a free app) - snapshotted like the
// rate, and runners refuse a record that under-declares it (the consoles and
// the CLI fill this in for you)
create("catalog://0x06a4…c13/5", 250, 50, 8080, "", true, "", "0x0…0", 0)
// -> the receipt's Created event: topics[1] is your deployment id (bytes32)
Attach model volumes: big weights, attested, not in the wasm
An enclave can carry attested read-only model volumes: weights declared in the enclave's config become a dm-verity image whose root hash is part of the enclave measurement, so the attestation commits to the exact bytes. A version attaches volumes by name and the runner mounts each read-only at /models/<name> inside the guest - so a multi-GB model reaches your app without riding the app wasm (no include_bytes!, no IPFS weight fetch, none of the 4 GiB linear-memory ceiling on the weights). Volumes ride the version's config - its volumes key - set at publish (the publish form's App config box, or enclave publish --config) and delivered to the app as ENCLAVE_CONFIG straight from the on-chain record. Like the bytes, specs and ports, the config is immutable once published and covered by the version's approval - a publisher cannot change an approved release's behavior; a new default config means a new version and a fresh review. For deployers it is the default, not a straitjacket: on fleets that support it, a deployment may carry its own config override at create (the deploy form's App config box, or enclave deploy --config) - it replaces the version's config as that one deployment's ENCLAVE_CONFIG, and the catalog record and every other deployment stay untouched.
Large configs. A config up to 4096 bytes is stored inline in the version record. Past that it is pinned to IPFS and the record carries its CID instead, which raises the ceiling to 1 MB - the publish path does this for you, so enclave publish --config and the App config box behave the same either way. The guarantee does not change: the CID is part of the immutable, approval-covered version record, exactly like the wasm CID beside it, and the enclave fetches it through the same path that verifies the content hashes back to the CID - so what your app receives is what the owner approved, not whatever a gateway felt like serving. Two details worth knowing if your config is large: a handful of keys (wasi, threads, set, gpuOptional, cpuFallback, volumes, _media) stay on-chain as a small manifest, because a runner reads them to pick a machine - and size the share it must have bought - before it fetches anything; and above 64 KB the config no longer fits in an environment variable (a kernel limit on argument size), so it arrives only as a file. Read ENCLAVE_CONFIG_FILE - it is set at every size, and points at the same JSON ENCLAVE_CONFIG carries when it fits.
the version's config → ENCLAVE_CONFIG
// published WITH the version (enclave publish --config '…'); the runner reads it
// from the catalog record, mounts each volume at /models/<name> (read-only), and
// hands the whole object to the app as ENCLAVE_CONFIG.
{ "volumes": ["qwen2.5-0.5b"], "system_prompt": "…app config…" }
// GET /availability lists the volumes the fleet carries (name, bytes, which enclaves).
Secrets: private values that never touch the chain
Everything above is public by design - the catalog config and a deployment's config override live on an open ledger, so an S3 key pasted into either is published to the world. Real credentials go in the deployment's secrets instead: env-var-shaped values stored on the platform's API relay (encrypted at rest, never on-chain, capped at 64 keys / 16 KB) and handed only to the enclave that holds your deployment's live lease, which injects each one as an environment variable when the app starts - alongside ENCLAVE_CONFIG, so your app just reads its env. Managing them takes no session and no gas: each change is one personal_sign over a canonical string, and the relay honors it only from the deployment's on-chain owner wallet. A running app picks changes up on its next start (enclave restart, or the dashboard's Restart). Plain reality, stated plainly: the relay operator can technically read these values (that is what lets an unattended restart re-inject them) - for credentials even the operator must never see, use encrypted volumes, whose keys derive from your wallet signature client-side.
enclave secrets · private env vars
# stage at deploy (stored before the first claim, so the app boots with them)
enclave deploy encrypted-volumes:1.2 --fund 5 --secrets-file .env
# or manage them any time - one wallet signature per change
enclave secrets set 0x9c41…88d1 S3_SECRET_ACCESS_KEY=… --restart
enclave secrets ls 0x9c41…88d1 # names only; --show reveals
enclave secrets rm 0x9c41…88d1 OLD_KEY # "clear" removes all
Secrets also reach apps that only read ENCLAVE_CONFIG: a $NAME (or ${NAME}) inside any config string resolves to the secret of that name when the enclave launches the app - so the on-chain config carries a harmless placeholder and the private value is spliced in at start, inside the enclave. Only names actually stored as secrets substitute (any other $ stays literal; $$ escapes one before a real name), and the deployment record keeps showing the placeholder - the resolved value exists only in the app's environment.
Fund it: pay-per-second, credited on-chain, no custody
On the site, card credit does this step for you: your credit vault creates and funds the record when you deploy. From a wallet, funding lands in the deployment's on-chain balance6 and the USDC splits in the same transaction: the host's share stays in the ledger as escrow, released to the host only for time it proves, and the rest forwards (ETH fundings forward in full). Your rate is snapshotted at create: gpuShare × $6/hr + cpuShare × $3/hr, plus the app's hourly publisher fee if it charges one. On a paid app every funding is split in that same transaction: the publisher's share (their fee's fraction of the rate) goes straight to their wallet, the host's share to escrow, the rest to Enclave. Pay either way:
EnclaveDeployments · two ways to fund
// USDC: sign an EIP-3009 ReceiveWithAuthorization (gas-free wallet signature,
// to = the EnclaveDeployments contract, nonce prefixed with the id's first 16 bytes), then ONE tx:
fundWithAuthorization(bytes32 id, address from, uint256 value,
uint256 validAfter, uint256 validBefore,
bytes32 nonce, bytes signature)
// ETH: one payable call; credited at the Chainlink ETH/USD rate read IN the contract:
fundEth(bytes32 id) payable
Enclaves sweep the ledger for funded, unleased work about once a minute; POST /v1/claim-hint {"id": …} skips the wait by asking the fleet to claim right now. The claiming enclave fetches your wasm from IPFS, verifies it hashes to the CID, launches it, and renews its lease while healthy - balance / rate seconds of runtime, burned lease by lease, with clean handovers refunding the unused tail. The console does all of the signing and encoding for you.
Watch it come up
The ledger shows the claim (get(id).runner + a live leaseUntil); the runner's status API shows the launch. The SIWE token from above authorizes the owner-only status route - sign in with the same wallet that sent create().
deploy.js · poll until running
let d; // id = the bytes32 from the Created event
do {
await new Promise(r => setTimeout(r, 2000));
d = await (await fetch(`${API}/deployments/${id}`, {
headers: { "Authorization": `Bearer ${token}` },
})).json();
} while (d.status !== "running");
console.log(d.network.endpoint); // https://…/x/0x…
// your app's own origin: the id's first 8 hex chars as a subdomain
console.log(`https://${id.slice(2, 10)}.app.enclave.host`);
Keep it alive, or stop it
Top up: another fundWithAuthorization / fundEth with a fresh nonce, any time - each payment adds amount / rate seconds to the on-chain balance.
Upgrade: setAppRef(id, "catalog://<appId>/<newIndex>") (owner-only) repoints the deployment at another approved version of the app - paid time, shares and the live lease all stay on the record, and the runner restarts the app in place on the new version within about a minute (new wasm fetched before the old instance stops). No second buy-in to pick up a new release; the dashboard's Version control and enclave upgrade are exactly this call. Shares bought at create() are immutable - and so is the publisher-fee snapshot - so a version whose minimums exceed the shares, or whose publisher fee exceeds the snapshot, can't be switched to - deploy that one fresh.
Stop: setActive(false) on the contract (owner-only) takes the work off the queue - the current runner's watcher tears it down and releases the lease, refunding the unused tail to your deployment's balance. That balance stays on the record: setActive(true) later resumes from what's left. To get money back instead, refund(id) (the owner or its delegate) first pays the host for proven time, then returns your own unspent escrow to the owner and deactivates the deployment. Only that escrow comes back - the host's share of what the owner funded in USDC; the publisher's cut, Enclave's share and ETH fundings are final - so fund in small top-ups. DELETE /v1/deployments/{id} alone only stops the CURRENT run; an active, funded deployment gets re-claimed.
Fairness: the quantum of trust is the lease (30 min). A runner that dies mid-lease has burned that lease - that is the worst case; clean shutdowns and enclave updates release() and lose you nothing, and another enclave resumes your app from its CID.
06Call your app
Every deployment serves HTTP on its own path and, through the gateway, its own origin (so one app can never touch another app's cookies or storage, or the site's):
terminal
# public deployment: anyone, no auth
curl https://3xk9f2qa.app.enclave.host/ # per-deployment origin (id without "dep_")
curl https://<enclave-host>/x/dep_3xK9f2Qa/ # same app, direct to its enclave
# private deployment (public: false): owner's bearer token only
curl -H "Authorization: Bearer $LOGIN_TOKEN" https://<enclave-host>/x/dep_3xK9f2Qa/
Any method, any subpath, headers and body pass through to your handler. The one exception: the Authorization header authenticates at the platform and is stripped before your app sees the request; the token never reaches tenant code.
public only governs who may send requests. Confidentiality is unaffected either way; the operator can't see inside the enclave regardless.
Your app's println! / stderr output lands in the deployment log: GET /v1/deployments/{id}/logs (owner-only).
07HTTP in depth: routing, headers, body
Everything a web app needs beyond hello: reading the method, path and headers, consuming the request body, and writing a status, headers, and a streamed response. This drop-in src/lib.rs shows all of it:
src/lib.rs · echo app
use wasi::http::types::{
Fields, IncomingRequest, Method, OutgoingBody, OutgoingResponse, ResponseOutparam,
};
wasi::http::proxy::export!(Component);
struct Component;
fn read_body(req: IncomingRequest) -> Vec<u8> {
let body = req.consume().unwrap();
let stream = body.stream().unwrap();
let mut buf = Vec::new();
while let Ok(chunk) = stream.blocking_read(64 * 1024) {
if chunk.is_empty() { break; }
buf.extend_from_slice(&chunk);
}
buf // a stream error here means end-of-body (Closed)
}
fn respond(out: ResponseOutparam, status: u16, ctype: &str, payload: &[u8]) {
let headers = Fields::new();
headers.set(&"content-type".to_string(), &[ctype.as_bytes().to_vec()]).unwrap();
let resp = OutgoingResponse::new(headers);
resp.set_status_code(status).unwrap();
let body = resp.body().unwrap();
ResponseOutparam::set(out, Ok(resp));
let stream = body.write().unwrap();
for chunk in payload.chunks(4096) { // write in ≤ 4096-byte chunks
stream.blocking_write_and_flush(chunk).unwrap();
}
drop(stream);
OutgoingBody::finish(body, None).unwrap();
}
impl wasi::exports::http::incoming_handler::Guest for Component {
fn handle(req: IncomingRequest, out: ResponseOutparam) {
let method = req.method(); // Method::Get, Method::Post, …
let path = req.path_with_query().unwrap_or_default(); // "/echo?x=1"
match (method, path.as_str()) {
(Method::Get, "/") =>
respond(out, 200, "text/plain", b"routes: GET /headers - POST /echo\n"),
(Method::Get, "/headers") => { // request headers
let mut s = String::new();
for (name, value) in req.headers().entries() {
s.push_str(&format!("{name}: {}\n", String::from_utf8_lossy(&value)));
}
respond(out, 200, "text/plain", s.as_bytes());
}
(Method::Post, p) if p.starts_with("/echo") => { // request body
let body = read_body(req);
respond(out, 200, "application/octet-stream", &body);
}
_ => respond(out, 404, "text/plain", b"not found\n"),
}
}
}
wasmtime serve instantiates your component per request: globals and statics do not survive from one request to the next. State that must outlive a request goes in /data (next chapter). Also note the 4096-byte ceiling per blocking_write_and_flush call; chunk larger payloads as above.
08The /data scratch filesystem
Every deployment gets a private, writable /data directory, so ordinary file-using code (a SQLite database, a cache, config, logs) ports to wasm without a rewrite. In Rust it's plain std::fs; every language's stdlib file I/O maps to the same wasi:filesystem interface. Extending the echo app with a hit counter:
src/lib.rs · add a route
use std::fs;
// (Method::Get, "/hits"): a per-deployment counter that survives across requests
(Method::Get, "/hits") => {
let hits: u64 = fs::read_to_string("/data/hits")
.ok().and_then(|s| s.trim().parse().ok())
.unwrap_or(0) + 1;
fs::write("/data/hits", hits.to_string()).unwrap();
respond(out, 200, "text/plain", format!("visit #{hits}\n").as_bytes());
}
Test locally with the parity flag from chapter 03 (--dir scratch::/data). Know exactly what /data is:
Private: your app sees only its own /data; there is no preopen for anything else, and escaping paths are refused by the runtime. Isolation is the wasi capability model, not a convention.
Ephemeral: it's RAM-backed scratch inside the enclave's already-encrypted memory, torn down when the deployment ends. No persistence, no shared storage: treat it as scratch, not a database of record. For data that persists under a key only your user holds, see encrypted volumes (ch. 13).
Capped: usage is limited by the version's storage_mb (default 256 MB); an app that exceeds it is killed by the audit sweep, since ramdisk files consume RAM the memory cap doesn't cover. Publish with storage_mb: 0 to opt out of /data entirely.
09Service apps: open ports & ENCLAVE_PORTS
Declaring open ports (on the version you publish, applied to every deployment of it from the on-chain record) changes how your app runs: instead of wasmtime serve calling your handler, the enclave launches it with wasmtime run as a long-running command component (a main()) granted wasi:sockets: it binds its declared ports itself, and may also make outbound TCP/UDP connections and DNS lookups. Rust's std::net on wasm32-wasip2 maps straight onto it.
entry
meaning
reached at
http:N
your app serves HTTP on port N itself (max one entry)
/x/{id}/…, the same URL as a web app
tcp:N
a raw TCP listener
/x/{id}/tcp/N bridge · /x/{id}/tls/N + public relay (ch. 10)
Declared ports are logical: your app's stable, advertised interface, like a container's EXPOSE. Each deployment gets its own actual loopback bind, so any number of tenants can run "the tcp:5432 app" simultaneously with zero conflicts; URLs always use the logical number and route by deployment id. The mapping arrives in one environment variable:
the ENCLAVE_PORTS contract
ENCLAVE_PORTS=tcp:5432=31245,udp:9053=31246 # logical=actual; BIND THE ACTUAL
The one rule
Read ENCLAVE_PORTS and bind the actual ports. Never hardcode. The socket grant is coarse, so an audit sweep polices the fine print: an app that binds a port it wasn't assigned is killed on the spot. Labels are 1–49999 (1080/8080/8090/8091 reserved for infrastructure); labels below 1024 (say udp:53) are always remapped to an unprivileged actual. Up to 8 entries per version.
Parsing the contract is a few lines; every service example below uses this helper:
src/main.rs · ENCLAVE_PORTS helper
/// "tcp:7000=31245,udp:9053=31246" → the actual port for a logical entry.
fn actual_port(logical: &str) -> u16 {
std::env::var("ENCLAVE_PORTS").unwrap_or_default()
.split(',')
.find_map(|e| e.trim().strip_prefix(&format!("{logical}="))?.parse().ok())
.unwrap_or_else(|| panic!("{logical} not in ENCLAVE_PORTS"))
}
Readiness: the enclave waits (~20 s) for your first http:/tcp: port to accept before marking the deployment running, so bind early in main(). Both launch modes also pass -S p3 (on runtimes that prove it), so you may target the async WASIp3 API surface instead of wasip2's poll-based one - same sandbox, same rules; the pinned p3 toolchain is at the end of chapter 03.
10TCP: bridge, in-enclave TLS, public relay
A complete TCP echo service. Note it's a command component (no --lib) with a main() that runs for the deployment's lifetime:
src/main.rs · tcp echo
use std::io::{Read, Write};
use std::net::TcpListener;
fn main() {
let port = actual_port("tcp:7000"); // ← the helper from chapter 09
let listener = TcpListener::bind(("0.0.0.0", port)).expect("bind");
println!("echo on {port}"); // shows up in your deployment logs
for conn in listener.incoming() { // wasip2 has no threads:
let Ok(mut sock) = conn else { continue }; // serve connections one at a time
let mut buf = [0u8; 4096];
while let Ok(n) = sock.read(&mut buf) {
if n == 0 || sock.write_all(&buf[..n]).is_err() { break; }
}
}
}
terminal · build & test in run mode
cargo component new tcp-echo && cd tcp-echo # no --lib: a command component
cargo component build --release --target wasm32-wasip2
# exactly how the enclave launches a service app:
wasmtime run -Scli -Sp3 -Stcp -Sudp -Sinherit-network -Sallow-ip-name-lookup \
--env ENCLAVE_PORTS=tcp:7000=7000 \
target/wasm32-wasip2/release/tcp-echo.wasm
printf 'ping\n' | nc 127.0.0.1 7000 # → ping (another terminal)
Publish it with open ports tcp:7000, deploy, and reach it three ways:
terminal · clients
# 1. WebSocket bridge on the attested origin (private deployments: append ?token=<bearer>)
websocat --binary tcp-listen:127.0.0.1:7000 \
"wss://<enclave-host>/x/dep_3xK9f2Qa/tcp/7000"
nc 127.0.0.1 7000 # → your app, end to end
# 2. Direct public TLS via the relay: stock clients, no websocat. Public deployments
# only; hostname = deployment id with its underscore hyphenated (dep_abc123 → dep-abc123)
psql "host=abc12345.app.enclave.host port=5432 sslmode=verify-full"
irssi -c abc12345.app.enclave.host -p 6667 --tls
Your app does nothing for the TLS path: keep speaking plain TCP on the assigned port. The platform mints its key + cert inside the enclave at boot; the untrusted relay only routes ciphertext by SNI, and the session terminates in-enclave at /x/{id}/tls/{port}. Verifying clients can bind the session to the attestation by pinning the cert published over the attested origin:
Same shape as TCP: bind the actual port, speak datagrams:
src/main.rs · udp echo
use std::net::UdpSocket;
fn main() {
let port = actual_port("udp:9053"); // ← the helper from chapter 09
let sock = UdpSocket::bind(("0.0.0.0", port)).expect("bind");
let mut buf = [0u8; 65535];
loop {
let Ok((n, from)) = sock.recv_from(&mut buf) else { continue };
let _ = sock.send_to(&buf[..n], from);
}
}
Every public deployment gets its own dedicated IPv6 address (deterministic from its id, out of the relay's routed /64). Its declared tcp:N and udp:N ports are reachable at [address]:N - at the real port you declared, routed by destination IP, no SNI and no remapping. The address is in the deploy/status response under network.address (and network.tcp / network.udp per protocol). TCP is raw passthrough (any protocol - your app owns TLS or plaintext); publish with udp:9053 or tcp:5432:
terminal · clients
# 1a. UDP, direct to the deployment's own IPv6 (public deployments)
printf 'ping' | socat -t2 - 'UDP6:[2a01:4f9:c013:9b52:9c1e:…]:9053'
# 1b. TCP, direct to the SAME address at the real port (any protocol)
psql 'host=2a01:4f9:c013:9b52:9c1e:… port=5432 …'
nc 2a01:4f9:c013:9b52:9c1e:… 5432
# 2. the WebSocket bridge on the attested origin (1 message = 1 datagram)
websocat --binary udp-listen:127.0.0.1:9053 \
"wss://<enclave-host>/x/dep_3xK9f2Qa/udp/9053?token=<bearer>"
printf 'ping' | socat -t2 - UDP:127.0.0.1:9053
IPv6 only: a box has one IPv4; v4-only clients can't reach the direct endpoint (the bridge still works anywhere WebSockets do).
The relay sees cleartext: unlike the TCP path, the UDP relay is not a confidentiality boundary. If your protocol needs privacy, encrypt at the app layer (e.g. DTLS).
Datagram boundaries are preserved end to end; idle bridge flows close after 120 s.
12GPU inference (wasi-nn)
Buying a GPU share doesn't just reserve silicon - it grants an interface. A deployment with gpuShare > 0 is launched with wasi-nn: your component imports wasi:nn and hands the runtime a model. Three backends sit behind the same interface - ONNX Runtime for ONNX graphs, a bundled llama.cpp (ggml) backend for GGUF language models, and a bundled stable-diffusion.cpp backend for image-generation checkpoints. execution-targetgpu runs on the card (the CUDA provider / CUDA-offloaded llama.cpp / GPU diffusion); cpu runs on the node. A deployment without a GPU share doesn't get the import at all - the component fails to instantiate - so the boundary is structural, not a quota. On today's hosts the card is reached only through Enclave Shield's masked offload, which serves GGUF models through the llama.cpp backend; the ONNX and diffusion GPU paths served only on the retired hosted fleet.
The share is a reservation, not a wall. Your deployment reserves gpuShare of the card's memory and compute on the host's Shield worker, exactly the budget the deployment priced. That is scheduling and accounting between tenants, not a hardware boundary: what protects your data on the card is that the card only ever holds masked values, and every result is checked before your app uses it (Enclave Shield).
GPU means GPU. The platform's runtime is patched so target gpu either runs on CUDA or fails the load() - never a silent fallback to the CPU. On the ONNX path that means the CUDA provider initializes or the load fails (ONNX Runtime's default is a quiet CPU fallback); on the GGUF path the whole model offloads to your slice by default, and the backend refuses to run if the CUDA module didn't actually load; the diffusion backend is strict the same way. Want CPU inference? Ask for cpu.
Isolation is the capability model again: graphs and tensors are opaque handles scoped to your instance. Nothing like a device pointer or a raw kernel ever crosses the boundary.
Import the interfaces in your world (WIT deps vendored from the wasmtime the fleet pins, the 49 line; wasi:nn 0.2.0-rc-2024-10-28):
A WASIp3 app imports the same wasi:nn@0.2 interfaces from its p3 world - the runtime links both API sets into one process, so nothing changes on the nn side (the bridge; see the p3 notes in chapter 03 for the one concurrency caveat). The async, token-streaming wasi:nn@0.3-draft is published in the platform repo and arrives with a future runtime cut.
Load once, compute per request - the whole API is four calls, and it drives all three backends. The ONNX path first. Tensors cross the boundary as little-endian bytes in any ONNX element type - fp16, fp32, fp64, bf16, i32, i64, u8 - and outputs come back in the model's actual dtype. Zero-size dimensions are legal (an empty KV cache bootstrapping a decoder's first step), and compute returns owned tensor handles you can feed straight back as the next step's inputs, so KV-cache bytes never round-trip through your linear memory.
src/lib.rs · inference
use bindings::wasi::nn::graph::{load, ExecutionTarget, GraphEncoding};
use bindings::wasi::nn::tensor::{Tensor, TensorType};
static MODEL: &[u8] = include_bytes!("model.onnx");
let graph = load(&[MODEL.to_vec()], GraphEncoding::Onnx, ExecutionTarget::Gpu)?;
let ctx = graph.init_execution_context()?;
let x = Tensor::new(&[1, 4], TensorType::Fp32, &input_bytes); // little-endian f32s
let out = ctx.compute(vec![("X".into(), x)])?; // named tensors, in and out
Try it without writing code: the nn-demo app in the Apps catalog is exactly this. Deploy it with 1% GPU + 1% CPU and hit /?target=gpu; it runs a tiny ONNX graph and echoes which target served it.
The GGUF path: generative LLMs via llama.cpp.GraphEncoding::Ggml loads any GGUF model - split-GGUF families included - on the runtime's bundled llama.cpp backend. The natural delivery is a model volume (ch. 05): attach a GGUF volume to your version and the runtime preloads it once at process start as a named graph, registered under the volume's name. load_by_name() then hands you the graph without the weights ever entering your linear memory - so model size is bounded by your share of the card, not by wasm32's 4 GiB. (Small GGUFs can still ride load() from embedded bytes.) The inference contract is an autoregressive decode loop: feed "tokens" (i32, dims [1, n], chunk your prefill at 512 tokens per compute), get back "logits" (fp32, [1, n_vocab], for the last token), sample, feed the sampled token back. The KV cache lives inside the execution context and persists across compute() calls - a fresh context is a fresh sequence. Tokenization and sampling are yours: the backend speaks token ids, not strings.
src/lib.rs · GGUF decode loop
use bindings::wasi::nn::graph::load_by_name;
let graph = load_by_name("qwen2.5-0.5b")?; // the attached GGUF volume's name
let ctx = graph.init_execution_context()?; // fresh context = fresh sequence
let mut next = prompt_ids; // Vec<i32>, ≤ 512 per compute
loop {
let t = Tensor::new(&[1, next.len() as u32], TensorType::I32, &le_bytes(&next));
let out = ctx.compute(vec![("tokens".into(), t)])?; // ("logits", fp32 [1, n_vocab])
let id = sample(&out); // your sampler: greedy, top-p, …
if id == EOS { break; }
emit(id); next = vec![id]; // KV cache persists in `ctx`
}
And for the full-scale proof, deploy llm-chat from the same catalog: an OpenAI-compatible LLM service with a web chat UI in one small component - and no weights in the wasm at all. Its versions attach GGUF model volumes; the runtime preloads each as a named graph, and the app drives the whole tokens-in/logits-out decode through wasi:nn on your share of the GPU: chunked prefill, KV cache resident in the execution context, tokenizer and sampler in the guest, target gpu strict.
The image path: diffusion via stable-diffusion.cpp. The same four calls generate images. Attach an image-checkpoint volume - SD-Turbo-class full checkpoints, SDXL, FLUX-family GGUF quants, or split-component DiT families (Qwen-Image, Z-Image) - and the runtime preloads it through the bundled stable-diffusion.cpp backend as a named graph: load_by_name(), weights never in your linear memory, exactly like GGUF volumes. The contract is one compute() per image, all named tensors: prompt (utf-8 bytes) is the only required input; negative_prompt, steps (i32, default 20), seed (i64), width/height (i32, 64-multiples, default 512), cfg (fp32, default 7.0 - set 1.0 for turbo models) and sample_method/scheduler (sd.cpp names) are optional. Back comes image: u8, dims [1, h, w, 3], RGB row-major. Generation blocks inside the call, and requests against the same checkpoint queue - one generation at a time per model.
src/lib.rs · one image
let graph = load_by_name("sd-turbo")?; // the attached checkpoint volume's name
let ctx = graph.init_execution_context()?;
let prompt = b"a lighthouse in a storm, oil painting";
let out = ctx.compute(vec![
("prompt".into(), Tensor::new(&[prompt.len() as u32], TensorType::U8, prompt)),
("steps".into(), Tensor::new(&[1], TensorType::I32, &4i32.to_le_bytes())),
("cfg".into(), Tensor::new(&[1], TensorType::Fp32, &1.0f32.to_le_bytes())), // turbo: guidance off
])?; // ("image", u8 [1, 512, 512, 3], RGB)
Getting the model in. Three routes. Embed it (include_bytes!) for anything small - it ships inside your published CID, so it's attested with your code; the ceilings are the ≤ 2 GB wasm artifact and the ≤ 4 GiB linear memory that load() passes bytes through. Attach a model volume (ch. 05) for anything big: attested read-only weights mounted at /models/<name> - and a GGUF or image-checkpoint volume is additionally preloaded as a named wasi-nn graph for load_by_name(), skipping your linear memory entirely. Or fetch at startup over outbound HTTP into memory and check the hash yourself before loading - the platform verifies your code by CID, a fetched model is your responsibility.
Local testing
Stock wasmtime release binaries don't compile wasi-nn in - and the platform's runtime carries patches you'll want locally too (strict GPU target, the full tensor-dtype set, zero-size tensors, the GGUF and diffusion backends; unpatched wasmtime rejects everything but fp32 and has no ggml or sdcpp at all). Build the same runtime the enclave uses: in a checkout of the wasmtime commit the fleet pins (the 49 line; the exact commit is WASMTIME_VERSION in wasm/Dockerfile.wasmtime), git apply wasm/wasmtime-onnx-gpu-strict.patch (and wasm/wasmtime-nn-ggml.patch for GGUF, plus wasm/wasmtime-nn-sdcpp.patch on top of it for diffusion) from the Enclave repo, point ORT_LIB_LOCATION at the official Microsoft onnxruntime-linux-x64-gpu distribution, and cargo build --release --features "wasi-nn,wasmtime-wasi-nn/onnx-cuda,wasmtime-wasi-nn/ggml" - the ggml feature links the prebuilt enclave-llamacpp libraries from the repo's releases via ELL_LIB_LOCATION (add wasmtime-wasi-nn/sdcpp, linking libenclave_sd via ESD_LIB_LOCATION, for the diffusion backend); then wasmtime serve -S nn app.wasm, adding -S nn-graph=ggml::<dir> to preload a local GGUF (or sd::<dir> for an image checkpoint) under the directory's basename like the platform does with volumes. (Avoid the onnx-download feature: it pulls a prebuilt ONNX Runtime with precompiled kernels for consumer GPUs only - it works on a desktop card but JIT-hangs on datacenter parts. The MS build is exactly how the platform's own runtime is built - see wasm/Dockerfile.wasmtime in the repo.) Without CUDA libraries installed, target gpu fails (by design); test with cpu locally and gpu on the enclave.
13Encrypted volumes
Confidential storage where only your user holds the key. A directory is encrypted client-side with rclone crypt and the ciphertext pushed to any S3-compatible bucket the owner controls; the deployment's App Config names where that ciphertext lives - never a key. At unlock time the key enters through the app, over the deployment's in-enclave-terminated TLS, and the enclave pulls and decrypts into a live mount. The bucket, the network, and the operator's host only ever see ciphertext: file names and contents are both encrypted at rest, and plaintext exists only on the CVM's encrypted ramdisk while the volume is unlocked.
Push data
scripts/enclave-encvol.sh in the Enclave repo is the client half: it encrypts locally and syncs ciphertext up, mirroring exactly what the enclave runs at unlock, so a push here always pulls there.
It prints the exact encVolumes App Config snippet to publish. pull and ls round-trip the volume from anywhere else you hold the key.
Name it in the App Config
Everything in the config is public and non-secret - it says where ciphertext lives and how it was packed:
field
meaning
name
the mount: your app sees /enc/<name>
endpoint · bucket · path
where the ciphertext lives (any S3-compatible endpoint; public hosts only)
provider · region
S3 dialect hints, when the endpoint needs them
filenameEncryption · directoryNameEncryption
how names were packed (defaults: standard, on - must match the push)
maxMb
plaintext cap (default 1024); like /data, it is RAM-backed, and a volume that outgrows it gets the app killed by the audit sweep
readOnly
drop the S3 credentials right after the pull; disables push-back
unlock · keyId
UI hints: lead with wallet or password; keyId is the stable label the wallet signs, so renaming a volume doesn't change its key
credsEnvelope
S3 credentials, sealed under the owner's wallet key - safe to publish (below)
encrypted
default true; false mounts the bucket verbatim, no crypt layer (below)
What your app sees
The deployment starts immediately, with an empty live preopen per volume - no key ceremony on the critical path. After unlock, decrypted files appear in place; your code needs no crypto and no S3 client, just std::fs reads. Three environment variables are the whole contract:
the ENCLAVE_ENC contract
ENCLAVE_ENC=docs,models # volume names; each mounted at /enc/<name>
ENCLAVE_ENC_API=http://… # the in-enclave manager's loopback unlock plane
ENCLAVE_ENC_TOKEN=… # bearer for that plane - only your app holds it
Your app drives the plane over loopback: GET $ENCLAVE_ENC_API for per-volume status (locked / syncing / unlocked / pushing), and POST …/unlock (password, optional salt, optional S3 credentials), …/sync (encrypt local edits back to the bucket), …/lock (wipe plaintext, drop credentials) - all authenticated with the token, which must never reach a browser. The first-party encrypted-volumes app in the store is the reference implementation: a complete unlock UI whose /api/* proxy keeps the token server-side.
Three ways to unlock
Password. The manager takes an opaque password (+ optional salt) and runs the pull. Simplest, and what headless automation uses.
Wallet. No password to keep: the key is derived from a deterministic ECDSA personal_sign over a canonical message naming the volume - only the wallet holder can reproduce it, nothing is stored or verified server-side, and no transaction is made. The contract is byte-exact across the CLI, the push script, and the app:
the derivation contract (v1)
message = "Enclave encrypted volume key v1\nvolume: <keyId>\n\n
Signing derives this volume's encryption key. Only sign in
apps you trust with its contents."
sig = lowercase 65-byte personal_sign hex, 0x-prefixed
password = sha256_hex( sig + "\n" + "enclave-encvol-v1:password" )
salt = sha256_hex( sig + "\n" + "enclave-encvol-v1:salt" )
Wallet + sealed credentials: one signature, nothing typed. Bucket credentials can ride the public config encrypted under the same signature - enclave encvol seal-creds prints the credsEnvelope value. At unlock, one signature derives the crypt key and opens the credentials, in the browser, before anything is sent:
the credentials envelope (v1)
encKey / macKey = sha256( sig + "\n" + "enclave-encvol-v1:creds-enc" / ":creds-mac" )
envelope = "encv1:" + base64( iv[16] || AES-256-CTR(encKey, iv, credsJSON)
|| HMAC-SHA256(macKey, iv || ct)[32] )
Plain volumes: skip the crypt layer
Not everything is secret. "encrypted": false mounts the bucket verbatim - same live /enc/<name> preopen, same unlock/sync/lock lifecycle and caps, no crypt anywhere. Think public datasets, published model weights, assets your app serves. Unlock then takes S3 credentials only - an empty body suffices for a public-read bucket - and refuses a password outright; filenameEncryption / directoryNameEncryption don't apply. Push data with any S3 client, or enclave-encvol.sh push --plain to get the matching config snippet. The bucket host sees every byte of a plain volume: keep anything confidential in an encrypted one.
Restarts, locking, write-back
Nothing survives a restart, by design: an unlocked volume is plaintext in enclave RAM plus credentials in the manager's memory, and a re-claim, update, or crash returns the volume to locked - someone holding the key must unlock again. Wallet mode makes that a ten-second signature rather than a secret to retrieve; Lock is the same wipe on demand. Push-back (sync) re-encrypts local edits to the bucket with the same key - it needs write-capable credentials at unlock and a volume not marked readOnly.
Trust fine print
rclone crypt authenticates file contents, not the tree: a malicious bucket can serve a stale or partial file set, though never forged bytes. The credsEnvelope is exactly as sensitive as the volume - the wallet that signs for the key opens the credentials, so guard that wallet accordingly. Wallet unlock needs a deterministic (RFC 6979) EOA signer - MetaMask, Ledger, and the CLI wallet qualify; smart-contract and randomized MPC wallets don't, so give those users password mode.
14Custom domains
Every deployment answers on its own subdomain, <label>.app.enclave.host. You can also attach a domain you own, and the app serves it under that name - same backend, no redirect, no frame - with a certificate minted inside the enclave. The private key for your domain is generated in the CVM and never leaves it, which is the same guarantee the platform subdomain already gives you.
Open Domains on the deployment in the dashboard, type the hostname, and it shows you the exact records to create. There are three:
your DNS provider
# 1. routing - where the traffic goes
shop.example.com. CNAME cc1f4f3f.app.enclave.host.
# 2. ownership - proves this deployment may claim the name
_enclave-challenge.shop.example.com. TXT "enclave-verify-9f3a…"
# 3. certificates - delegates issuance AND renewal. Leave it in place.
_acme-challenge.shop.example.com. CNAME _acme-challenge.cc1f4f3f.app.enclave.host.
DNS is re-checked every few minutes, so the status moves on its own: waiting for DNS → verified → live, usually within a minute or two of the records publishing. Nothing is served on your name until it is verified and its certificate exists - a name with no certificate gets a refused handshake, never a warning page.
www vs the apex
A subdomain like www.example.com or shop.example.com is the easy case: it can hold a CNAME, so record 1 above works as written.
An apex (example.com, no subdomain) cannot hold a CNAME - RFC 1034 forbids a CNAME alongside the SOA and NS records every zone apex must have. Two ways round it, and we accept both:
CNAME flattening (Cloudflare's CNAME at the root, Route 53 ALIAS, DNSimple ALIAS, Netlify, and most modern providers). You enter a CNAME to <label>.app.enclave.host and the provider publishes our A/AAAA records under your apex instead. Verification sees those addresses, recognizes our edge, and accepts it - flattening and a real CNAME are the same proof that traffic reaches us.
A/AAAA by hand, pointing at our edge addresses. It works, and it is what flattening does for you, but it is a static copy of an address we may one day change: prefer flattening, or a www subdomain, wherever your provider offers it.
The common shape is to attach both: example.com (flattened) and www.example.com (CNAME), then have your app redirect one to the other. Each is a separate attachment with its own certificate. Records 2 and 3 are per-hostname either way - _enclave-challenge.www.example.com is a different record from _enclave-challenge.example.com.
Writing an app that serves more than one name
Your guest environment carries ENCLAVE_HOSTS: a comma-separated list of every hostname this deployment answers on, its own subdomain first. Use it for the checks only your app can make - a Host allowlist, CSRF trusted origins, a websocket Origin check - instead of hardcoding a name you cannot know at publish time. Build absolute URLs and redirects from the request's Host (it reaches you untouched), and leave cookies host-only unless you have a reason not to.
ENCLAVE_HOSTS is set when the app starts, so a domain attached to a running app appears in it after a restart. Routing and TLS for the new name work immediately - only the app's own view of the list waits.
When it doesn't go live
"No TXT record …" - the _enclave-challenge record is missing or holds a different value. Some providers append the zone name for you: creating _enclave-challenge.shop.example.com in the example.com zone often needs the name entered as _enclave-challenge.shop.
"CNAME does not point at …" - the name resolves, but somewhere else. If you use a CDN in front of us, put it in DNS-only mode (Cloudflare: grey cloud, not orange). A proxying CDN terminates TLS itself, so our edge never sees your visitors' handshakes and the certificate we mint for you is never presented.
"CAA records only allow …" - your zone publishes a CAA policy that excludes our CA. Add 0 issue "sectigo.com" alongside whatever is there, or remove the restriction.
A certificate error, with the CA's own words - issuance was attempted and refused. The reason is shown verbatim under the domain; nearly always it is CAA, or record 3 missing so the CA cannot find the challenge.
It was live and went back to "waiting for DNS" - the records stopped resolving for a sustained period, so routing and issuance were withdrawn. Put them back and it recovers by itself, usually within a few minutes.
Detaching
Detach in the dashboard and routing stops within a minute; the enclave drops the certificate (it only ever held it in RAM). Remove the three records at your provider too - a CNAME left pointing at a name you no longer own is worth cleaning up wherever it points.
15Attestation for your users
On a confidential host the platform's core promise extends to your app: a user can verify, cryptographically and before sending a byte of data, that they're talking to an attested enclave running exactly the code that was measured. For apps deployed by CID, the attestation includes your app: its app.cid field names the exact bytes that ran, hash-verified against the CID inside the enclave by the measured runtime, with a coverage note stating precisely what covers it (the boot measurement covers the runtime; the runtime pins your app).
terminal · verify before you send data
# Enclave's own verifier (verifier/ in the repo, bundled into the CLI):
enclave attest dep_3xK9f2Qa # the box your deployment runs on
enclave attest dep_3xK9f2Qa --expect <measurement> # pin the launch measurement yourself
# what it checks: a FRESH report for a nonce it draws (sent through the relay to the box),
# AMD signature up to AMD's root, firmware floor, guest policy, the box's key bound to the
# nonce, and the launch measurement (yours with --expect; otherwise the operator's list,
# reported as "limited", never as a pass)
verify.js · in your users' browser
// same-origin, hash-pinned build of verifier/web
import { verifyMetalBoxWeb } from "https://enclave.host/vendor/enclave-verifier.js";
const r = await verifyMetalBoxWeb({ name: "metal0", expected: { measurements: ["<measurement>"] } });
if (r.status !== "verified") throw new Error("not verified: " + r.reasons.at(-1));
// r.transportKeySha256 is the key the report binds: use it for an end-to-end channel
Point your users at the attestation walkthrough for what each link in the chain proves, or embed the check in your own client as above. For raw-TCP services, add the bridge-cert pin so even the TLS session is bound to the measured enclave.
16Other languages
The platform runs components, not a language: anything that emits a wasi:http component works. Mature routes today:
Rust: cargo-component, as used throughout this guide. The most complete support, including std::net for service apps.
JavaScript / TypeScript: jco componentize (ComponentizeJS) turns a JS module exporting a fetch-style handler into a wasi:http component.
Python: componentize-py builds components from Python code implementing the same world.
Go: TinyGo's wasip2 target with the wasi:http bindings.
The acceptance test is identical regardless of toolchain. If this runs, it deploys:
One caveat for service apps: raw-socket support (wasi:sockets) varies by toolchain: Rust's stdlib maps today; check yours before designing around tcp:/udp: ports.
17Limits & sandbox reference
What your code can touch
capability
web app (default)
service app (declared ports)
inbound HTTP
your handler, fresh instance per request
http:N: you bind and serve it yourself
sockets
none
bind assigned actuals · outbound TCP/UDP · DNS lookups
GPU (wasi-nn)
only with gpuShare > 0: ONNX + GGUF (llama.cpp) + image diffusion (stable-diffusion.cpp) via wasi:nn on a reserved share of the card (VRAM = share × 140.4 GB), through Enclave Shield's masked offload; no share = no import (ch. 12)
filesystem
/data only: private, RAM-backed, ephemeral, capped by storage_mb
environment
empty
ENCLAVE_PORTS only
memory
linear memory capped at the deployment's cpuShare × node RAM, enforced by the runtime
host / exec / SSH
none: structurally absent, not merely denied
Numbers to know
limit
value
notes
wasm artifact
≤ 2 GB
enforced at upload and again at fetch
app specs (vramMb/gpuGflops/memMb/cpuGflops)
exact MB + GFLOPS, per version
set the MINIMUM deploy shares (spec ÷ server spec, larger of memory/compute, ceil to %)
config cpuFallback (memMb/cpuGflops)
exact MB + GFLOPS · only with gpuOptional
the node minimum WITHOUT a card, replacing memMb/cpuGflops there. The four specs above describe your app beside the card it asked for; if its weights move into node RAM when there isn't one, say so here
memory cap
= memMb
exactly what you ask for, runtime-enforced; the CPU share is derived from it
You built a plain module. Build with cargo component for wasip2, or the blessed wasip3 container (chapter 03) - both emit components; the upload gateway and the enclave both check the component layer field.
"app targets WASIp3 … a p3-capable enclave must claim it"
Your version published as wasi: "0.3" but landed on (or was hinted to) a box whose runtime doesn't serve p3 yet. Pick a p3-capable enclave in the deploy target list (per-box p3 on /availability), or wait for the fleet-wide flag.
403 not_approved on deploy
That version is still Pending (or was Rejected); the catalog owner approves each CID before it can run. Check the status badge on your app card in Apps.
"firewall: bound unassigned port(s) … app killed"
The app hardcoded a port. Read ENCLAVE_PORTS and bind the actual ports (ch. 09).
"did not open port in time"
Bind your first http:/tcp: port early in main(); the readiness window is ~20 s.
"storage: /data used … exceeds the cap; app killed"
Write less, or publish the next version with a larger storage_mb.
502 no_http at /x/{id}
The app declares only tcp:/udp: ports, so there's no HTTP to proxy. Use the bridge or relay paths (ch. 10–11).
409 when opening a tcp/udp bridge
The app hasn't actually bound that port (yet); the bridge refuses until the enclave confirms the bind. Check the deployment logs.
state vanishes between requests
Web apps get a fresh instance per request. Persist in /data (ch. 08).
runs locally, memory-traps deployed
Local wasmtime is uncapped unless you pass -W max-memory-size. Test with the parity command (ch. 03) at your published memMb (the cap at the minimum CPU share).
"component imports instance wasi:nn/… not found in the linker"
The app imports wasi:nn but the deployment bought no GPU share. Redeploy with resources.gpuShare > 0 (ch. 12).
wasi-nn fails: "Failed while accessing backend: …"
The suffix carries the actual backend cause (ONNX Runtime, llama.cpp or stable-diffusion.cpp) - read it. Common ones: CUDA can't initialize (target gpu on a CPU-only node or local wasmtime without CUDA libraries - strict on purpose, no silent CPU fallback; use cpu there), an input tensor's name/shape/dtype not matching the model, a model operator the provider doesn't support, a load_by_name() name that doesn't match an attached GGUF or checkpoint volume, or a GGUF prefill batch over the 512-token compute limit (chunk it).
deploy fails: "GPU interface warming up" / "GPU interface unavailable on this node"
GPU deployments launch only after the enclave's boot-time CUDA readiness probe passes (so a broken GPU path fails your deploy loudly instead of hanging your app). "Warming up" resolves in a minute or two - retry; "unavailable" carries the probe's diagnosis and is an operator problem, not yours.
Ship checklist
wasmtime serve (or run, for service apps) runs it locally, including with the enclave-parity flags.
No hardcoded ports anywhere; everything comes from ENCLAVE_PORTS.
All writes go under /data, stay under the cap, and survive being wiped.
Publish on the Apps tab → wait for the version's Approved badge → deploy it from its card.
Verify your own deployment's attestation, then hand the same check to your users.
19Wallet sessions: let an agent act without your key
Every on-chain change above is a wallet transaction. A session lets an agent, a script or a browser tab make those changes without holding your wallet key. It is a P-256 key, a policy and an optional USDC budget, approved once by your wallet. The key signs each action, the platform's relayer submits it to your session vault (one SessionVault contract per wallet, on Base), and the vault checks the policy on-chain before it calls the ledger or the catalog. There is no wallet prompt per action and no ETH: the relayer is reimbursed a small USDC fee per operation out of the session's budget, at a price the key itself signed. The vault has no admin, so you can always end a session or withdraw straight from your wallet.
What a policy allows
field
meaning
actions
On-chain: deploy.create, deploy.fund, deploy.setAppRef and deploy.setConfig (staging only), deploy.setShares, deploy.setMaxRate, deploy.setActive, deploy.refund, app.publish. API scopes the relay enforces from the same grant: api.status, api.logs, api.restart, api.upload, api.appAccess, api.placement, api.account.
environments
staging and/or prod. A deployment your vault holds is one or the other; every deployment your wallet holds counts as prod. A session never changes what a production deployment runs.
apps
Up to 8: a slug of your vault's own apps, 0x<appId> for any app, or * for any free app. Paid apps and publishing always need the app named.
budget & limits
USDC escrowed now; spend and operations per period; the relay fee cap per operation; the publisher-fee ceiling; the ceiling on any price cap it sets.
expiry
After it nothing passes, and the unspent budget returns to your wallet.
Presets: browser (sign-in on enclave.host: every deployment action, any free app, staging and prod, $0 budget, 12 h), staging-publish (agents: publish and run named apps on staging only, $10 escrowed, $5/day, 7 days) and auth-only (status reads, no money).
Open one from a terminal
agent terminal · enclave session
enclave session new --preset staging-publish --app myapp-staging --budget 10 --days 7 --label "Claude Code"
# prints what it allows, a check code (3F9A-0C1E) and https://enclave.host/grant#…
# you open the link, compare the check code, and sign: once, or twice with a budget
# (the grant, then the USDC it escrows). The relayer submits; you need no ETH.
enclave publish … # now acts through the session, no wallet prompt
enclave session status # budget, spend, time left
enclave session terminate # sign out: the unspent budget returns to you
Add --env to print export ENCLAVE_SESSION=… instead of storing a key file, for agents that keep secrets in their environment. Treat that value as a key. The full walkthrough is Give Claude Code a staging session.
The apps your wallet owns
A session can also manage the deployments your wallet holds, where they are. You allow it once, for your vault, with one wallet transaction to the deployments ledger: Sessions → Apps your wallet owns → Grant, or enclave session delegate run with your wallet key. After that, every session whose grant covers prod may, within its own actions:
suspend and resume, resize, and lower the price cap;
add runtime out of its budget, credited to your wallet (a paid app only if the grant names it and its fee is under the grant's ceiling);
cancel, with the refund paid to your wallet.
It can never change the version or config a wallet-held app runs, raise its price cap, move it out of your wallet, or touch its secrets. Revoke on the same card, or enclave session delegate --revoke, cuts every session off your wallet's apps in one transaction. Staging-only sessions, such as the staging-publish preset, never reach them.
A production session reaches all of them
The grant's app list narrows only which paid apps a session may fund. A session holding prod and deploy.setActive can stop any app your wallet owns. Give production to agents sparingly: only the actions they need, a small budget, a short expiry.
Always your wallet's
Topping up, extending or withdrawing from the vault; promoting a version to production; opening, changing or ending other sessions; secrets and custom domains; moving a deployment; anything touching the treasury. Ending a session, by sign-out, expiry or revocation, never stops what is already running: deployments keep their own balance on the ledger.
Contracts on Base: session vault factory 0x1F5c887c0cDF491b16AB6c449abAfDF9B2ec9C9C, deployments ledger 0xB1783B61E1FDee6a6163DC3CdaC27bF32E7d117B. The relay API is in the API reference; the agent tools are on the MCP tab.
The enclave CLI
One executable file, wallet-native - your wallet is your account. Point it at a key and every operation on this page becomes a one-liner; state lives in ~/.config/enclave/ and nothing else touches your machine.
Install
Three routes, same result: the enclave command on your PATH. Each needs node ≥ 20; pick the one that fits.
install · Linux / macOS
curl -fsSL https://get.enclave.host | sh
install · Windows (PowerShell)
irm https://get.enclave.host/install.ps1 | iex
install · from a clone, via npm (any OS)
git clone https://github.com/EnclaveHost/enclave
cd enclave/cli
npm i
npm i -g .
Authenticate
No signup step: the CLI authenticates with whatever key you point it at. Generate a fresh one or import the wallet you already use; commands that spend need USDC on Base under that address.
bring a wallet
# generate fresh, or import the key you already use
enclave key new # prints the address; fund it with USDC on Base
enclave key import # reads a private key from stdin; or ENCLAVE_KEY env
enclave whoami # address, USDC balance, running deployments
Auth is SIWE under the hood: commands that need the API sign a one-time challenge (GET /auth/nonce → POST /auth/login) and cache the bearer token until it expires. On-chain commands (create, fund, publish) sign Base transactions with the same key - the CLI never sends your key anywhere.
No wallet? Sign in with your Enclave account instead: enclave login prints a link you approve from your phone or any browser where your passkey works, and the terminal holds an account session. That session reads the deployments and credit that belong to your account (ls, whoami, account); deploying and adding credit stay on the site, where your passkey can sign.
create + fund + wait until live, one shot; prints the app URL. Shares are fractions of one card / one node; omitted, they default to the app's catalog minimums
EnclaveDeployments.create() · fundWithAuthorization · POST /claim-hint
enclave ls
your deployments, state and funded time remaining
GET /deployments · GET /account
enclave status <id>
one deployment: state, runner, lease, balance, URL
GET /deployments/{id}
enclave logs <id> [-f]
your app's stdout/stderr; -f follows
GET /deployments/{id}/logs
enclave fund <id> --usdc 5 | --eth 0.002
top up runtime by the second
fundWithAuthorization / fundEth
enclave attest <id>
fetch the CPU + GPU attestation reports and verify them locally; prints measurements and the verdict
GET /deployments/{id}/attestation
enclave restart <id>
stop + relaunch the app in place: same version, endpoint and balance (app state is ephemeral) - the fix for a wedged instance, no wallet tx
POST /deployments/{id}/restart
enclave stop <id>
take the work item off the queue, then terminate; unspent balance stays on the record for a later reactivation (or the owner calls refund to recover its own unspent escrow)
switch a deployment to another approved version of its app (default: the latest); paid time carries over - the runner restarts the app in place on the new version
EnclaveDeployments.setAppRef() · POST /claim-hint
enclave secrets set|ls|rm|clear <id>
private env vars for a deployment (S3 keys, API tokens): relay-stored, never on-chain, injected at app start by the lease-holding enclave; one wallet signature per change, checked against the on-chain owner (--restart applies changes now, ls --show reveals values)
POST /secrets/{id} · POST /secrets/{id}/get
enclave publish app.wasm --slug hello
validate the component, pin to IPFS, cut a catalog version; add --fee $/hr to charge deployers an hourly fee, paid straight to your wallet (capped on-chain, immutable per version)
ipfs.enclave.host/add-wasm · EnclaveAppCatalog
enclave apps [query]
browse or search the on-chain catalog
EnclaveAppCatalog reads
enclave encvol message <keyId>
print the canonical message a wallet signs for a volume key (ch. 13)
local only - nothing sent anywhere
enclave encvol derive --key-id K | --sig 0x…
an encrypted volume's password + salt: the CLI wallet signs the canonical message itself (deterministic, RFC 6979), or derive from a signature made in any other wallet
local only - nothing sent anywhere
enclave encvol seal-creds --key-id K
encrypt S3 credentials (AWS_* env, or --access-key/--secret-key) under the wallet key → a credsEnvelope value for the public App Config; the app then unlocks with one signature, nothing typed
local only - nothing sent anywhere
enclave pricing · availability · gpu
rates + live ETH quote · fleet headroom · GPU probe state
GET /pricing · /availability · /gpu
enclave account
address, payment forwarder, totals; signed in with an account: profile, credit balance, account deployments
GET /account · GET /account/me · GET /billing/vault
enclave login · logout
sign in with your Enclave account (passkey): approve a link from your phone or any signed-in browser; the session reads your account's deployments and credit, and never signs transactions
POST /account/device/start · /claim
enclave session new --preset staging-publish --app S [--budget 10] [--days 7] [--env]
make a session key and print what it may do, a check code and a grant link; the owner opens the link and signs once with their wallet; the key is stored 0600 (or printed as ENCLAVE_SESSION with --env) and becomes the active session (below)
/grant#… · POST /sessions/open
enclave session status · list · use <sid>
live or ended, budget and spend, this period's limit, expiry, actions and environments; the stored sessions; pick the active one
SessionVault.sessionOf (a chain read)
enclave session terminate
sign out: the unspent budget returns to the owner's wallet and the key file is deleted; running deployments are not touched
POST /sessions/end
enclave session top-up-link --amount 5
a link for the owner: one wallet signature adds budget
/sessions#topup=… · POST /sessions/owner (topUpWithAuthorization)
run by the owner with the wallet key: let every session of your vault act on the apps your wallet owns, as production (one wallet transaction); --revoke takes it back for all of them at once; --status reads it and needs no key
EnclaveDeployments.setDelegate(vault, bool) · GET /sessions/owner/{address}
Global flags: --json for machine-readable output, -x to print every REST call and transaction before it is sent (the curl-equivalent escape hatch), --base / ENCLAVE_API_BASE to target an enclave directly instead of the gateway, --rpc for your own Base RPC.
The whole lifecycle, in five lines
terminal
enclave publish target/wasm32-wasip2/release/hello.wasm --slug hello
enclave deploy hello:1 --fund 2 # create, fund $2, wait for live; shares = app minimums
enclave attest 0x3f2a9c1b # verify before you send data (any unique id prefix)
curl https://3f2a9c1b.app.enclave.host/ # → hello from Enclave
enclave logs 0x3f2a9c1b -f # watch it serve
Everything the site does, nothing it hides: any command run with -x shows the exact API traffic, ready to replay with curl.
Act through a session (agents, no wallet key)
With a session active, or ENCLAVE_SESSION set, these commands act through it instead of the wallet key: publish (the publisher is your session vault), deploy (--env staging|prod, default staging; --fund comes out of the session budget), fund --usdc, upgrade, resize, config set|clear, stop, resume, rate-cap and refund. They work on deployments your vault holds and, once the owner has run enclave session delegate, on the apps the owner's wallet holds, which a session treats as production: stop, resume, resize, fund, refund (to the wallet) and a lower rate-cap, never upgrade or config. A refusal (budget, policy, environment, expiry, missing delegation) prints its code and the next step, and the command stops: it never falls back to the wallet key. --wallet uses the wallet key for one command.
terminal · a session's day
enclave session new --preset staging-publish --app myapp-staging --budget 10 --days 7 --label "Claude Code"
enclave publish target/wasm32-wasip2/release/app.wasm --slug myapp-staging --version 0.4.1
enclave deploy myapp-staging:0.4.1 --env staging --fund 2
enclave session status
enclave session terminate # unspent budget back to the owner
Secrets, transfers, promotion to production and the vault's own money (top-up, extend, withdraw) stay with the owner's wallet. See chapter 19 for the policy model.
The MCP server
Coding agents connect over the Model Context Protocol at https://mcp.enclave.host (Streamable HTTP). It exposes the full platform surface as typed tools: the catalog, pricing, live capacity, deployments, logs, attestation, sign-in, and uploads. An agent holding a funded key can browse the catalog, deploy, fund, watch logs, publish, and verify attestation end to end.
Connect
One URL, no signup, no API key. Pick your client:
connect · Claude Code
claude mcp add --transport http enclave https://mcp.enclave.host
Once connected, the guide tool (topic getting-started) walks the agent through everything below on its own.
No keys, by design
No tool accepts a private key, signs anything, or submits a wallet transaction. Reads are direct; every state change comes back as an unsigned Base transaction{chainId, to, data, value}, validated against live chain state first (catalog approval, share minimums, the GPU cap, publisher fee snapshots), for the agent to sign with your own wallet. Sign-in and uploads take locally produced signatures. Session tokens for owner-scoped reads ride per-call parameters or the Authorization header; transactions never need a session. An agent with no wallet can instead act through a wallet session: the builders then return calls for the agent's own session key to sign, and session_execute hands the signed calls to the sessions relayer.
attestation documents; real verification stays client-side against the pinned repo
auth_nonce · auth_login
SIWE sign-in: fetch the message, sign it locally, trade the signature for a session token
upload_token
wallet-signed authorization for pinning wasm and images to IPFS
plan_deploy
validate + price a deployment, returns the unsigned create() transaction
build_fund
unsigned funding transactions: USDC approve + fund, or payable fundEth
build_stop · build_resume · build_upgrade
unsigned lifecycle transactions; upgrade validates approval, share fit, and the fee snapshot
build_publish
unsigned publishVersion transaction from a pinned CID
claim_hint
nudge the fleet to claim funded work right now
A typical deploy, agent-side
list_apps → plan_deploy → sign and send the create transaction → read the new id from the receipt's Created event → build_fund → sign and send → claim_hint → poll get_deployment until running. The app serves at its own origin, https://<first-8-hex-of-id>.app.enclave.host.
The CLI and MCP are equivalent surfaces: anything an agent plans over MCP can also be run with enclave one-liners, and both talk to the same API and contracts underneath.
Sessions: act without a wallet
An agent that holds no wallet works through a wallet session. It keeps a P-256 key of its own, its owner approves a policy for that key once, and the agent signs each action with it. The server still holds no keys and signs nothing; the sessions relayer submits, paid out of the session's budget.
tool
does
session_request
{publicKey: {x, y}, label, preset, budgetUsd, spendPerDayUsd, expiresInHours, apps, environments, owner} → {link, checkCode, keyHash, policy, vault, sid}: the grant link the owner opens and signs with their wallet (with owner set, only that wallet may sign, and vault and sid are known in advance). Nothing is on-chain until they sign.
session_status
{owner} → the owner's vault, its sessions (live or not, actions, environments, budget, spend, expiry), held deployments and delegation; or {publicKey} → the sessions opened for that key
with session: {vault, sid}: instead of unsigned wallet transactions, return calls, each {action, actionName, describe, nonce, fee, deadline, args, digest, digestSha256}, for the session key to sign. The builder refuses up front what the session can't do (an action outside the grant, a production record for a staging session, a missing delegation).
session_execute
{vault, sid, publicKey, calls}, each call with its signature added → {executed}: the calls go through the sessions relayer in order, and it stops at the first failure
session_end
sign out, in two calls: {vault, sid} returns a digest; sign it, then call again with publicKey, deadline and signature. The unspent budget goes back to the owner; running apps are untouched
build_delegate
{owner, grant} → the owner's unsigned wallet transaction (setDelegate on the deployments ledger) that lets their sessions act on the apps their wallet owns, as production; grant: false revokes it for every session at once
The flow: generate a P-256 key and keep the private half in the agent's secret store → session_request → the owner opens the link, checks the code and approves → session_status until the session is live → a builder with session → sign each digest → session_execute. The signature is ECDSA P-256 over SHA-256 of the 32 digest bytes, which is exactly what WebCrypto computes:
agent · sign the calls
// call.digest is the EIP-712 digest of SessionCall(sessionId, nonce, action, argsHash, fee, deadline),
// domain {name: "Enclave Sessions", version: "1", chainId: 8453, verifyingContract: vault}
for (const call of calls) {
const digest = Uint8Array.from(call.digest.slice(2).match(/../g), (h) => parseInt(h, 16));
const sig = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, privateKey, digest));
call.signature = "0x" + [...sig].map((b) => b.toString(16).padStart(2, "0")).join(""); // r ‖ s, 64 bytes
}
// then: session_execute { vault, sid, publicKey: {x, y}, calls }
Pick the preset deliberately
session_request defaults to the agent preset: the browser policy's deployment actions (every deployment action, any free app, staging and production; never signing in as the owner or opening their private apps) with a $20 budget for 7 days. Once the owner has delegated, such a session can stop, resize or cancel every app their wallet owns. For an agent that only publishes and tests, pass preset: "staging-publish": it is staging only and never reaches the wallet's apps. A session never changes what a production deployment runs, and never touches secrets.
The Enclave API
Everything the service does, fully scoped. Authenticate with an Ethereum signature, pay USDC or ETH per deployment, and orchestrate confidential compute over plain HTTPS. Prefer a terminal? The enclave CLI wraps every operation below. Wallet sessions have their own signed surface, documented after the reference.
Wallet sessions (guide chapter 19) are served under https://api.enclave.host/v1/sessions. The relay holds no keys and is trusted for liveness only: every write carries a signature the session vault checks on-chain, either the session key's (P-256) or the owner wallet's (EIP-712), and every submission is simulated first, so a call the vault would refuse is never sent. JSON in and out. Bigints come back as "123n" strings; requests accept those, decimal strings or integers. Errors are {error, code}.
route
body → answer
GET /config
→ {chainId, factory, book, usdc, router, relayer, site, rpc, keyAttestations}: the chain and vault factory this relay serves
POST /quote
{vault, sid, action} → {fee, deadline, nonce}: the relay fee for one action (USDC, 6 decimals), a deadline 120 s out, and the session's next nonce
POST /execute
{vault, sid, nonce, action, args, fee, deadline, x, y, r, s} → {txHash, result, block}. args is the action's ABI-encoded arguments; x, y the session public key; r, s its signature over the SessionCall digest (below)
POST /open
{owner, grant, ownerSig, deposit?} → {txHash, vault, sid, block}: opens a session the owner signed. A grant with a budget needs deposit: {validAfter, validBefore, sig}, the owner's USDC ReceiveWithAuthorization whose nonce is the grant digest
POST /owner
{owner, vault, op, args, sig} → {txHash, block}: an owner operation, EIP-712-signed by the owner wallet and relayed gas-free (rate-limited per owner). op: topUp, topUpWithAuthorization, extend, terminate, revokeAll, withdraw, promote, adopt, setEnvironment, release
POST /end
{vault, sid, deadline, x, y, r, s} → {txHash, refund6}: the session key signs itself out (SessionEnd(sessionId, deadline)); its API access ends first, and the unspent budget returns to the owner
GET /by-key/{keyHash}
→ {sessions}: the sessions opened for a key, keyHash = keccak256(abi.encode(x, y))
GET /owner/{address}
→ {owner, vault, deployed, sessions, held, delegation: {ledger, supported, granted}}: the owner's vault, its sessions and held deployments, and whether the owner has let the vault act on the apps their wallet owns
GET /session/{vault}/{sid}
→ {vault, sid, state, index, ops, revoked}: on-chain state (live, balance, spend, limits, expiry, actions) and the relay's action log
POST /attest
binds a session key to an enclave measurement (attested keys); answers 503 attest_disabled while that is off
Actions and their arguments
action is the number; args is abi.encode of the arguments (for deploy.create and app.publish, of one tuple). The SDK in sdk/sessions builds all of them.
#
action
args
0
deploy.create
(string appRef, uint16 gpuMilli, uint16 cpuMilli, uint32 appPort, string ports, bool isPublic, string configCid, uint256 maxRate6, uint8 env, uint256 fund6); env 1 = staging, 2 = prod; the publisher fee is read from the catalog, never passed
1
deploy.fund
(bytes32 id, uint256 amount6)
2
deploy.setAppRef
(bytes32 id, string appRef), staging only
3
deploy.setConfig
(bytes32 id, string config), staging only
4
deploy.setShares
(bytes32 id, uint16 gpuMilli, uint16 cpuMilli)
5
deploy.setMaxRate
(bytes32 id, uint256 maxRate6), USDC per second; production caps only go down
6
deploy.setActive
(bytes32 id, bool active)
7
deploy.refund
(bytes32 id); the refund goes to whoever holds the deployment (your vault or your wallet)
8
app.publish
(string slug, string name, string description, string version, string cid, uint32[4] res, string ports, string config, string configCid); to your vault's apps the grant names, free versions only
the session key's signature
SessionCall(bytes32 sessionId, uint256 nonce, uint8 action, bytes32 argsHash, uint256 fee, uint64 deadline)
domain = {name: "Enclave Sessions", version: "1", chainId: 8453, verifyingContract: vault}
argsHash = keccak256(args)
r, s = ECDSA P-256 over SHA-256(the 32-byte EIP-712 digest) // the vault verifies it with the P-256 precompile
A deployment your wallet holds counts as production to a session: only sessions that cover prod act on it, and deploy.setAppRef and deploy.setConfig never pass. Suspend, resume, resize, cap and refund also need the owner's one-time setDelegate(vault, true) on the deployments ledger (0xB1783B61E1FDee6a6163DC3CdaC27bF32E7d117B); without it the ledger reverts "!owner". Funding a paid one needs its app named in the grant as 0x<appId>.
Session-signed requests to other routes
A few relay routes also accept the session key in place of a wallet signature, each gated by an API scope in the grant: POST /v1/account/session-login (api.account: an account token that ends with the session), POST /v1/apps/upload-token (api.upload) and /v1/placement/{id} (api.placement). No bearer token is issued; each request is signed on its own:
Authorization: EnclaveSession
Authorization: EnclaveSession v1 vault=<addr>,sid=<0x…>,ts=<unix>,n=<nonce>,x=<b64url>,y=<b64url>,sig=<b64url>
sig = ECDSA P-256 over SHA-256 of the UTF-8 string
"enclave-api-v1\n" + METHOD + "\n" + host + path + query + "\n" + hex(sha256(body)) + "\n"
+ ts + "\n" + n + "\n" + lowercase(vault) + "\n" + lowercase(sid)
// ts within 60 s of the relay's clock; n is 8-64 base64url characters, single use;
// sig is the raw r ‖ s (64 bytes), base64url
The relay checks the signature, that the key belongs to the session, that the session is live on-chain (mutations read it fresh, reads may use a cache of at most 5 s), and that the grant holds the route's scope. Secrets and custom domains never accept a session: they stay owner-wallet signatures.
Architecture
How Enclave works, and what each part guarantees.
This is the architecture behind the front page, written as the invariants it holds rather than the parts it is made of. The dated engineering note at the end records what has been demonstrated, what has not, and what is attached right now, so the design and the evidence never blur.
One app bundle, one domain contract
An Enclave app is a WebAssembly component targeting wasi:http. Its bytes live on IPFS addressed by content hash, its listing lives in the EnclaveAppCatalog contract on Base, and the runtime inside the enclave refuses any bytes that do not hash to the CID in the deployment's on-chain record. One build, no per-host binaries.
Underneath the catalog, every backend speaks one versioned agreement, enclave-domain-abi/1. A bundle is a canonical manifest plus the artifact, and its identity is the hash of exactly those bytes. An attestation report binds the domain's transport key and a fresh nonce in its first half and the app's identity in its second, and that identity comes from the layer that loaded the bundle, never from the caller. Every domain moves through the same starting, running, ending and ended lifecycle, is reclaimed exactly once however it ends, and answers one small control protocol. A Go reference implementation and a Rust mirror are checked against one set of conformance vectors. Every backend speaks this contract and nothing else.
What portable means here
The package is the same everywhere. The guarantees come from the host, and the host must prove them. A bundle that runs on a confidential server and on an enclave backend gets the protection the machine proved when it attached, and that proof, not the machine's description of itself, is what the listing, the attestation document and your dashboard show. Portability is about your build, never about flattening the differences between hosts.
The isolation contract
A host is admitted by what it proves, never by what it runs. The contract is independent of the operating system: the same properties, the same verifier rules, on every backend.
Per-app hardware isolation. Each app runs in its own hardware-enforced domain. Another app cannot read its memory, reach its sockets, or obtain a report that names it.
The ordinary host operating system is excluded. A smaller, measured trusted layer that the hardware's virtualization enforces owns the domains; the operating system above it keeps running the machine without being able to see into them.
Measured identity. A domain's identity is the hash of exactly the bundle it runs, and the code that names it is itself inside a measurement a verifier can derive.
Attestation. A report names the domain's identity and is signed by something the host does not control: the processor, or the trusted layer with a root in the machine's measured boot.
Key and traffic binding. The domain's transport key is generated inside it and bound into the report, and TLS terminates inside the domain, so nothing on the host can sit in the middle.
Fail-closed verification. Unverified means refused. A field that is present but unsigned is unauthenticated, and a report that cannot be chained to its root is not evidence.
Lifecycle cleanup. A domain that ends is reclaimed exactly once, however it ends, and leaves nothing behind for the next one.
Portable operation. The same bundle, the same domain contract, the same verifier rule, across every backend.
Two kinds of host meet this contract: a confidential processor, and, on machines without one, Enclave Shield, described next. Both are held to one set of acceptance tests covering every property above, and a machine is listed only when it passes them. A machine that meets part of the contract is not listed at a weaker level; it is not listed. The relay derives eligibility from the evidence it verified when the machine attached, never from the machine's own description of itself and never from the operating system it runs.
Enclave Shield
Enclave Shield is Enclave's proprietary protection technology for machines that lack confidential-computing hardware. It has two components, and on such a machine both together are Shield protection: a host isolation component that keeps the ordinary operating system out of each app's domain, and a GPU offload component that lets a GPU outside a confidential boundary compute on masked inputs and return verified results, so it never receives plaintext protected state. The architecture is OS-independent: the same portable app bundle, the same domain contract and the same security contract span every backend, and the operating system is an implementation backend, never a product tier.
The host isolation component
Hardware-enforced per-app CPU isolation. A smaller, measured trusted layer that the hardware's virtualization enforces owns the app domains, and the machine's ordinary operating system keeps running the machine without being able to see into them; the layer's own identity is rooted in the machine's measured boot, and its report names each domain. The guarantee is the same on every operating system: a backend is part of the product only because it enforces this contract on its platform, and the operating system is an implementation detail beneath it, never the boundary. There is no plain or software-only fallback tier; a machine that cannot meet the contract is not a host. The distinction we keep honest: this component isolates apps from each other and from the host operating system and enforces the measured, signed policy, but it does not give the separate guarantee a confidential processor gives, that the machine's owner cannot inspect the data.
The GPU offload component
Masked, verified GPU offload for any GPU that does not support, or is not operating in, confidential-computing mode. Its guarantee is exact: the untrusted card sees only masked inputs, returns results that are verified before use, and never receives plaintext protected state. The trusted domain keeps the tokenizer, sampling, attention and every nonlinear step; only the large matrix multiplies leave it, each activation hidden under a one-time pad before it goes and checked against a fast probabilistic proof when it comes back. A wrong or tampered result aborts before any output. The weights are public; the card and its host learn shapes and timing, and can refuse to serve, and that is all.
This component is selected by the card, never by the host. It applies wherever the attached GPU is outside the confidential boundary, even when the app or VM runs inside a confidential CPU. A GPU operating in a supported confidential-computing mode sits inside the boundary and does not need it. Shield's GPU component alone never makes a host eligible for tenant apps; on a machine without confidential hardware an app needs both components, the host isolation for the host and the offload for the card.
The rule, and the gate
In Enclave's architecture, Shield's GPU offload is the only supported way to expose a GPU that is not operating in confidential-computing mode, on any host. Admission reads and states the card's protection mode from explicit, verified GPU evidence (inside the boundary, or outside it and reached through Shield), never inferring the rule from the CPU's technology, the host operating system, whether the CPU is confidential, or the absence of a field; a card outside the boundary is neither advertised as capacity nor routed GPU work until its Shield evidence verifies, and admission fails closed. This is Enclave's policy for its own platform, not a claim about every possible system.
The performance research on the offload mechanism, masked decoding of large models on datacenter cards, is a separate line of work from the product rule; its figures live in the dated evidence note, never here. Enclave Shield is not the pVM CPU phone tier, described further down.
Two ways to meet it
Confidential hardware does not replace the contract; it adds to it: on a confidential processor the same domain gains memory encryption and protection from the machine's operator, and the report is signed by the processor itself. The app, the bundle, and the way you verify it do not change. The GPU is a separate axis on both: inside the boundary when the card runs in confidential-computing mode, otherwise through Shield's GPU offload. The table states what each level guarantees; what is attached right now is the fleet panel below.
property
Enclave Shield (no confidential-computing hardware)
on confidential hardware
the host's operating system
excluded from the domain by the measured trusted layer
excluded by the processor: guest memory is encrypted and integrity-protected against it
the machine's owner and operator
kept out of the domain through the operating system; not excluded the way a confidential processor excludes them, and still runs the machine
cannot read the domain at all; can still stop or starve it
physical access to memory
not defended, and the level says so
not defended by current processors, and the level says so
root of the report
the trusted layer's key, rooted in the machine's measured boot
the processor's fused key, chained to the silicon vendor
keys and traffic
generated and terminated inside the domain
generated and terminated inside the confidential VM
a GPU on the host
no card here runs in confidential-computing mode, so every card is reached only through Shield's GPU offload: masked inputs, verified results, never plaintext; not advertised or routed until its Shield evidence verifies
a card in a supported confidential-computing mode sits inside the boundary; any other card, even beside a confidential processor, is reached only through Shield's GPU offload under the same rule
app against app
one hardware domain per app
one hardware domain per app, inside the processor's boundary
The limit we state on purpose
A confidential processor defends memory against software: the host operating system, its administrator, and everything running beside the VM. It does not defend against someone with physical access and lab equipment on the memory bus, and current processors say so themselves. On the hosted fleet the operator is remote from the machines; a self-hosted seller owns theirs. That difference is real, and it is why a host's listing names who runs it and how it attests rather than printing one green word for every box.
There is no level below the contract. Ordinary virtualization with the host operating system in charge is a baseline the isolation work runs for comparison, and it is never built for, listed, routed tenant work, or sold. Which machines are attached right now is shown live at the bottom of this page.
What is measured, and by whom
"Attested" on this site means one thing: a signature chain from a root the host does not control down to the bytes that are running, checked by your client. A field that is merely present in a document is unauthenticated until something signs it, and the copy says which is which.
link
confidential hardware
Enclave Shield
root of trust
the CPU vendor's fused key; the GPU vendor's, where a card is inside the boundary
the machine's measured boot, vouching for the trusted layer's per-boot key
the measurement covers
firmware, kernel and the runtime image; on a confidential server the guest's kernel, initramfs and boot arguments, built on the host from a tagged release; no signed release measurement exists yet, so the operator's published allowlist is the reference unless you pin your own
the trusted layer and the code that names domains, so that a derived measurement covers the naming authority
your app
not in the boot measurement on purpose: the measured runtime refuses bytes that do not hash to the CID in your deployment record, and the attestation says exactly that
the bundle's hash, named in the report by the measured layer that loaded it
transport key
generated inside the VM and bound into the report; the operator cannot sit in the middle
generated inside the domain and bound into the report
who checks
your browser (the verifier ships from this origin), the CLI, an agent's tool call, or a fully independent third-party tool; the API's self-check is a labeled diagnostic, never the verdict
The pVM CPU tier is distinct from the server isolation contract above: CPU-only inference inside a Pixel's protected VM. The model runs entirely on the CPU cores the protected VM owns, the VM's attestation chain is verified by the relay before the phone is admitted, and admission is by proven capability, never by device name. It wears an amber pvm cpu badge wherever it appears, so it is never mistaken for the jade badge of a confidential server. It is the only phone tier: there is no accelerator tier. Which phones hold it, and what has been measured on which device, is in the dated evidence note, with the device, the model and the conditions attached.
Independent infrastructure
The platform is designed not to stop working if this company, its servers, or its domain name go away. Hosts still supply hardware, connectivity, power and upkeep; the point is that no indispensable party sits between a deployer and a host. What is already on public contracts and content addressing, and what runs on our machines, is listed under its own heading.
Already independent
The catalog, the deployment ledger, the host registry and payment routing are contracts on a public chain. Funding a deployment is a transaction; a publisher's fee forwards to their wallet in the same one; nobody takes custody.
App bundles are content-addressed and hash-checked inside the enclave, so any mirror that serves the right bytes is as good as ours.
The source, the measurements and the release provenance are public, and the verifier runs on your side.
Confidential servers can be self-launched on hardware you own, from the same published images, with no vendor control plane in the path. metal0 is one, serving now.
Your own apps on your own box run free: a zero-rate deployment needs no balance at all.
The CLI and the MCP server hold no keys. State changes come back as unsigned transactions for your wallet, or an agent's, to sign.
Still runs on our servers
Discovery and placement at api.enclave.host, which also hubs the tunnels that make home machines behind carrier NAT reachable.
Traffic relays that splice app connections to enclaves by name and carry TCP, UDP and outbound IPv6. They never hold a TLS key and see ciphertext, but they are ours.
The site and the IPFS gateway. Availability only: the bytes are hash-checked in the enclave regardless of who served them.
The certificate service, which sees only a signing request; the private key stays in the enclave. It also answers the DNS zone that delegated ACME challenges point to, so whoever runs it could obtain a certificate for a delegated name without the enclave; certificate transparency logs are where that would show.
Card and stablecoin checkout, which creates and funds the on-chain record on your behalf.
The measurement allowlist that admits new hosts and new releases. It lives in the relay's configuration and is published with every attestation answer.
The rest of the design
The architecture removes the remaining indispensable parties one by one: publisher-owned signatures over app bundles, a trust policy you keep locally instead of a list we publish, runtime measurements reproducible by anyone and verified on your side, several mirrors and peer transports for bundles, coordinators that are interchangeable or bypassed by talking to a host directly, and secrets and state that recover without any party we run. The contracts and payment paths in use are not replaced by this page; the evidence note says which of these pieces run and which do not.
Engineering evidence, 2026-10-14
The dated record of what has been demonstrated against the architecture above, what has not, and what is attached right now. Everything on this page above this line describes the product; everything below it is measured fact. The scripts, the negative tests and the corrections live in the repository's isolation, windows and shielded directories.
Attached right now. The live panel below is authoritative. On 2026-10-14 three hosts serve, each running one VM per app: a self-launched confidential server (AMD SEV-SNP), a Windows machine running Enclave Shield, and a Pixel 10 phone in the pVM CPU tier. The hosted confidential fleet, where apps shared one confidential VM as a Wasm sandbox and a process each, was retired on 2026-10-14 together with its container provider.
Confidential server, in production. The node's own confidential VM runs only the control plane; every app runs in its own SEV-SNP guest on the same machine, its TLS ends inside that guest, and the node serves only deployments that require this isolation. The site and the CLI check the server's report (AMD chain, a fresh nonce) against the operator's published allowlist, which gives a "limited" verdict: genuine and fresh, not independently reproduced. Pinning a measurement you derived yourself (enclave attest --expect) is what reaches "verified". The guest image is built on the host from a tagged release, so no signed release measurement exists yet. Each app guest's own report binds its TLS key. Since 2026-10-17 a public deployment's Verify button checks that report in your browser: fetched from the app's own address over a fresh nonce, from the same processor as the server, against the relay's published prediction of that guest, so it is limited too. Run live against production on 2026-10-17, all 18 checks passed on a public app's guest. The CLI checks only the server.
The domain contract. A Go reference and a Rust mirror pass one set of conformance vectors, and the Windows partition backend is checked against the same vectors.
Per-app hardware domains on confidential hardware. One confidential guest per app is in production (above). Demonstrated in the lab on one AMD EPYC host: one app per confidential guest with the app inside the launch measurement, TLS inside and the key bound into the report; several apps under a monitor inside one guest, separated by the guest kernel and labeled weaker; a hardware privilege split with a measured firmware layer above the monitor, where the derived digest equals the live report but covers that firmware layer and not the monitor image; and one confidential guest per app driven by the domain contract, where an adversary with root in its own guest minted a fully signed, correctly bound report naming the other app and was rejected on the measurement alone, independently rechecked from the saved evidence (14 checks, none failing). Not demonstrated: one hardware plane per app inside one guest with the naming authority in the measured firmware layer; hardware caps that at two or three apps per guest. The run of the same image on ordinary virtualization is a control, not a level.
Enclave Shield, host isolation component. In production on one Windows machine: every app runs in its own VBS-isolated Hyper-V partition, booted from our measured paravisor image with the kernel, initramfs and command line inside the measured pages, and holds its own TLS key. The relay checks the machine's boot through its TPM (Secure Boot on, test signing off) and verifies each partition's hardware-signed report, which binds the app, the runtime, the TLS key and a fresh nonce. Under VBS isolation the hypervisor keeps Windows out of a partition's memory; our own test of that from the host side has not been run yet, so the machine's records still say the host is not excluded. The hypervisor and the person with physical access remain trusted. Since 2026-10-16 your browser can check it too, from the deployment's Verify button: the machine's TPM-quoted boot, with a credential the page mints itself, then that app's partition report under that boot's key, over a fresh nonce. The verdict stays limited, and the CLI does not do this check yet. The Linux backend exists as a design; nothing upstream provides it on x86.
Enclave Shield, GPU offload component. In production on two hosts: the confidential server's two V100-class cards, which have no confidential-computing mode, serve a 27B model to its app guests over vsock, and the Windows machine's integrated GPU serves a small model through Vulkan. Neither card is reachable except through the offload. Pre-dealt masks, which would trade the GPU's work for a dealer the platform runs, are not in production. The design's reviews so far are internal; no external review has been done. Figures from the large-model performance research are withheld until they are stable; when published they carry the model, the hardware, the mode and the conditions.
The pVM CPU tier. A Pixel 10 serves CPU-only apps, one protected VM per app. The relay verifies each protected VM's attestation chain to Google's pinned root at attach and judges a capability report signed inside the VM against a served-model policy. Since 2026-10-17 a public deployment's Verify button checks the app's own protected VM in your browser as well. The evidence comes from the app's own address over a fresh nonce. It must chain to Google's root, bind the key the app's TLS certificate carries, and come from the phone's own VM or a sibling holding the phone's registered proof key. The verdict is limited, because the expected app, the allow-lists and the proof key are the relay's statements. Run live against production on 2026-10-18, all 14 checks passed on a public deployment on the Pixel 10. The Pixel 10 baseline is recorded in the repository (shielded/anchor/avf/PVM-CPU.md, with its raw data); no speed figure is published here. No Pixel 11 has been runtime-validated. The earlier accelerator experiments are closed and are not a tier.
Independence. On public contracts and content addressing: the catalog, the deployment ledger, the host registry, payment routing, hash-checked bundles, public source, self-launched confidential servers, free self-hosting, keyless CLI and MCP. Running on company servers: discovery and placement, the traffic relays, the site and IPFS gateway, the certificate service and its DNS zone, checkout, deployment secrets (the relay operator can read them), and the measurement allowlist that admits new hosts and releases. Not implemented: publisher-owned signatures, local trust policy, mirrors and peer transports, interchangeable coordinators, independent recovery of secrets and state. The source is published for audit under a license that does not permit others to run the service.
What is serving right now
The live fleet, with the badges the tables above explain. A jade badge is hardware evidence the relay or the box's own attestation presented; the absence of one is the signal, and a machine without it takes no tenant work. This panel is the answer to "what can I deploy on right now".
Your hardware belongs on this list.
Run an enclave on a box you own, claim paid work from the open queue, and the
ledger pays your wallet for every lease you serve.
Host an enclave →