name: build-nova-app-claw-skill description: "Build and deploy Nova Platform apps (TEE apps on Sparsity Nova / sparsity.cloud). Use when a user wants to create a Nova app, write enclave application code, build it into a Docker image, and deploy it to the Nova Platform to get a live running URL. Handles the full lifecycle: scaffold, code, build, push, deploy, verify running, and register on-chain. Triggers on requests like 'build me a Nova app', 'deploy to Nova Platform', 'create a TEE app on sparsity.cloud', 'I want to run an enclave app on Nova'."
Nova App Builder
End-to-end workflow: scaffold → code → push to Git → create app → build → deploy → register on-chain (create app on-chain → enroll version → generate ZK proof → register instance).
Architecture Overview
Nova apps run inside AWS Nitro Enclaves, managed by Enclaver (Sparsity edition) and supervised by Odyn (PID 1 inside the enclave). Key concepts:
- Enclaver: packages your Docker image into an EIF (Enclave Image File) and manages the enclave lifecycle.
- Odyn: supervisor inside the enclave; provides Internal API for signing, attestation, encryption, KMS, S3, and manages networking.
- Nova Platform: cloud platform at sparsity.cloud — builds EIFs from Git, runs enclaves, exposes app URLs.
- Nova KMS: distributed key management; enclave apps derive keys via
/v1/kms/derive.
Enclave Identity — Two Keys
- Encumbered Key / Enclave Address (
/v1/eth/address,/v1/eth/sign) — An ephemeral key generated by Odyn inside the enclave at boot. Unique per enclave instance. The endpoint returns both the enclave address (20-byte Ethereum address) and the public key (65-byte uncompressed secp256k1 key, starting with0x04). The address is simply derived from the public key (last 20 bytes of its keccak256 hash). Use it to prove a response originated from inside this specific TEE instance. This is an encumbered key as defined in ERC-733 — a private key that exists only within the enclave and can never be extracted. - App Wallet (
/v1/app-wallet/address,/v1/app-wallet/sign) — A persistent wallet derived via Nova KMS. Shared across all instances of the same app — the same app always gets the same wallet address regardless of which enclave is running. Use for stable on-chain identity (holding funds, signing transactions). Requiresenable_app_wallet: trueandenable_decentralized_kms: trueat app creation.
Runtime On-Chain Self-Discovery
The on-chain app ID is known before build (create app → create-onchain → get app ID), so it should be hardcoded into the enclave code. It's stable and never changes.
However, instance-level data (instance ID, version enrollment status) only exists after deployment and changes per deployment. With Helios always enabled, the enclave can query this live:
- Use the hardcoded app ID to query the Nova App Registry contract via Helios RPC (
http://127.0.0.1:18545, Base Sepolia) - The contract returns the app's registered instances, version data, etc.
For data not stored on-chain (e.g. registration TX hashes, proof URIs), the app can call the Nova Platform API (GET /api/apps/{sqid}/status) at runtime.
Prerequisites (collect from user before starting)
Ensure skill is up to date before starting:
git -C /path/to/build-nova-app-claw-skill pull origin mainOlder versions are missing
Dockerfile.txtin the template, causing scaffold to fail.
App idea: What does the app do?
Nova account + API key: Sign up at sparsity.cloud → Account → API Keys.
GitHub repo + GitHub PAT: Used only to push your app code to GitHub. Nova Platform then builds from the repo URL. The PAT is not passed to Nova Platform.
GitHub PAT setup:
- GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Required permissions: Contents (Read & Write), Metadata (Read)
- Push with token:
git remote set-url origin https://oauth2:${GH_TOKEN}@github.com/<user>/<repo>.git git push origin main
⚠️ Do NOT ask for Docker registry credentials or AWS S3 credentials. Nova Platform handles the Docker build and image registry internally (Git-based build pipeline). S3 storage credentials are managed by the Enclaver/Odyn layer — the app only calls Internal API endpoints (
/v1/s3/*). Users never touch AWS credentials.
Full Workflow
Step 1 — Scaffold the project
python3 scripts/scaffold.py \
--name <app-name> \
--desc "<one-line description>" \
--port <port> \
--out <output-dir>
Port choice: Any port works. Set it via
advanced.app_listening_portwhen creating the app. Must also matchEXPOSEin your Dockerfile. No requirement to use 8080.
This generates <output-dir>/<app-name>/ with the following structure:
<app-name>/
├── Dockerfile
└── enclave/
├── main.py
├── odyn.py
└── requirements.txt
Note: The template ships as
Dockerfile.txt(GitHub cannot distribute extensionless files in skill templates).scaffold.pyautomatically renames it toDockerfileand substitutes the port. No manual action needed.
Alternatively, fork nova-app-template — a production-ready starter with KMS, S3, App Wallet, E2E encryption, and a built-in React frontend.
Step 2 — Write the app logic
Edit enclave/main.py. Key patterns:
Minimal FastAPI app:
import os, httpx
from fastapi import FastAPI
app = FastAPI()
IN_ENCLAVE = os.getenv("IN_ENCLAVE", "false").lower() == "true"
ODYN_BASE = "http://127.0.0.1:18000" if IN_ENCLAVE else "http://odyn.sparsity.cloud:18000"
@app.get("/api/hello")
def hello():
r = httpx.get(f"{ODYN_BASE}/v1/eth/address", timeout=10)
return {"message": "Hello from TEE!", "enclave": r.json()["address"]}
With App Wallet + KMS (from nova-app-template):
from kms_client import NovaKmsClient
kms = NovaKmsClient(endpoint=ODYN_BASE)
@app.get("/api/wallet")
def wallet():
return kms.app_wallet_address() # {"address": "0x...", "app_id": 0}
@app.post("/api/sign")
def sign(body: dict):
return kms.app_wallet_sign(body["message"]) # {"signature": "0x..."}
Dual-chain topology (as in the template):
- Auth/Registry chain: Base Sepolia (84532) — KMS authorization & app registry
- Business chain: Ethereum Mainnet (1) — your business logic
Helios light-client RPC runs locally at http://127.0.0.1:18545 (Base Sepolia) and http://127.0.0.1:18546 (Mainnet).
⚠️ Helios RPC ports must be decided before creating the app — they are set in
advanced.helios_chains[].local_rpc_portand locked at creation time. Choose values carefully up front.
Rules for enclave code:
- All outbound HTTP must use
httpx(proxy-aware). Never userequestsorurllib— they may bypass the egress proxy. - Persistent state → use
/v1/s3/*endpoints; the enclave filesystem is ephemeral. - Secrets → derive via KMS (
/v1/kms/derive); never from env vars or hardcoded. - Test locally:
IN_ENCLAVE=false uvicorn main:app --port <port>(Odyn calls hit the public mock).
Step 3 — Commit & push to Git
Your repo only needs Dockerfile + app code. No local Docker build needed — Nova Platform builds from your Git repo directly.
KMS integration is fully handled by the platform — just set enable_decentralized_kms: true (and optionally enable_app_wallet: true) in advanced when creating the app. No contract addresses, app IDs, or manual KMS config needed in your code.
git add .
git commit -m "Initial Nova app"
git push origin main
Step 4 — Deploy to Nova Platform
The deployment is a 3-step process: Create App → Trigger Build → Create Deployment.
Recommended flow: After creating the app, immediately run create-onchain (Step 6a) to get the on-chain app ID before writing your app code. This lets you hardcode the app ID into the enclave so it's aware of its own on-chain identity from the start. The enclave can then query the Nova App Registry contract at runtime via Helios light-client RPC to pull live on-chain state (version, instance, etc.).
Via Portal (recommended for first-time)
- Go to sparsity.cloud → Apps → Create App
- Fill in Name, Description, Git Repo URL, configure Advanced settings → Submit → copy the App sqid
- In the App page → Builds → Trigger Build:
- Git Ref:
main(or tag / commit SHA) - Version: e.g.
v1.0.0
- Git Ref:
- Wait for build status →
success(Nova builds Docker image → EIF → generates PCRs) - Deployments → Create Deployment → select the successful build → Deploy
- Poll until state →
running→ copy the App URL (hostname from app detail)
Via API (scripted)
The script handles the full pipeline including on-chain registration automatically.
# Preview config without deploying
python3 scripts/nova_deploy.py \
--repo https://github.com/you/my-app \
--name "my-app" \
--port 8080 \
--api-key <your-nova-api-key> \
--dry-run
# Deploy (includes on-chain registration automatically)
python3 scripts/nova_deploy.py \
--repo https://github.com/you/my-app \
--name "my-app" \
--port 8080 \
--api-key <your-nova-api-key>
Or via raw API:
BASE="https://sparsity.cloud/api"
TOKEN="<your-api-key>"
REPO="https://github.com/you/my-app"
# 1. Create app — 'advanced' is the only config field needed
SQID=$(curl -sX POST "$BASE/apps" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name":"my-app",
"repo_url":"'"$REPO"'",
"advanced":{
"directory":"/",
"app_listening_port":8080,
"egress_allow":["**"],
"enable_decentralized_kms":false,
"enable_persistent_storage":false,
"enable_s3_storage":false,
"enable_s3_kms_encryption":false,
"enable_ipfs_storage":false,
"enable_walrus_storage":false,
"enable_app_wallet":false,
"enable_helios_rpc":true,
"helios_chains":[
{"chain_id":"84532","kind":"opstack","network":"base-sepolia","execution_rpc":"https://sepolia.base.org","local_rpc_port":18545}
]
}
}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['sqid'])")
echo "App sqid: $SQID"
# 2. Trigger build
BUILD_ID=$(curl -sX POST "$BASE/apps/$SQID/builds" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"git_ref":"main","version":"1.0.0"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Build ID: $BUILD_ID"
# 3. Poll build
while true; do
STATUS=$(curl -s "$BASE/builds/$BUILD_ID/status" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))")
echo "Build: $STATUS"
[ "$STATUS" = "success" ] && break
[ "$STATUS" = "failed" ] && echo "Build failed!" && exit 1
sleep 15
done
# 4. Create deployment
DEPLOY_ID=$(curl -sX POST "$BASE/apps/$SQID/deployments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"build_id\":$BUILD_ID}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Deployment ID: $DEPLOY_ID"
# 5. Poll deployment
while true; do
RESP=$(curl -s "$BASE/deployments/$DEPLOY_ID/status" -H "Authorization: Bearer $TOKEN")
STATE=$(echo $RESP | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('deployment_state',''))")
echo "Deployment state: $STATE"
[ "$STATE" = "running" ] && break
[ "$STATE" = "failed" ] && echo "Deploy failed!" && exit 1
sleep 15
done
# 6. Get app URL (hostname from detail, or use unified status)
curl -s "$BASE/apps/$SQID/detail" -H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['app'].get('hostname',''))"
# Tip: GET /api/apps/{sqid}/status gives the full lifecycle in one call:
# build_status, deployment_state, proof_status, onchain_instance_id, etc.
Step 5 — Verify the live app
# Health check
curl https://<hostname>/
# Hello endpoint (returns enclave address + signature)
curl https://<hostname>/api/hello
# Attestation (proves it's a real Nitro Enclave) — POST, returns binary CBOR
curl -sX POST https://<hostname>/.well-known/attestation --output attestation.bin
Note:
/api/app-walletis only available if you forked nova-app-template. The scaffold template includes/api/helloinstead.
Step 6 — On-Chain Registration
After the app is running, register it on-chain to establish verifiable trust. This step is required — it's what makes Nova apps provably secure.
Using the script (recommended):
python3 scripts/nova_deploy.py ...
The script automatically executes steps 6a–6d after deployment.
Manually via API — 4-step sequence:
# 6a. Create app on-chain
curl -sX POST "$BASE/apps/$SQID/create-onchain" \
-H "Authorization: Bearer $TOKEN"
# Poll: GET /api/apps/{sqid}/status → onchain_app_id is set when done
# 6b. Enroll build version on-chain (PCR measurements → trusted code fingerprint)
curl -sX POST "$BASE/apps/$SQID/builds/$BUILD_ID/enroll" \
-H "Authorization: Bearer $TOKEN"
# Poll enrollment via build status endpoint
while true; do
ENROLLED=$(curl -s "$BASE/builds/$BUILD_ID/status" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('is_enrolled',''))")
echo "Enrolled: $ENROLLED"
[ "$ENROLLED" = "True" ] && break
sleep 10
done
# 6c. Generate ZK proof
curl -sX POST "$BASE/zkproof/generate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"deployment_id\": $DEPLOY_ID}"
# Poll proof via deployment status endpoint (or /zkproof/status/{deployment_id})
while true; do
PROOF=$(curl -s "$BASE/deployments/$DEPLOY_ID/status" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('proof_status',''))")
echo "Proof status: $PROOF"
[ "$PROOF" = "proved" ] && break
[ "$PROOF" = "failed" ] && echo "Proof failed!" && exit 1
sleep 30
done
# 6d. Register instance on-chain
curl -sX POST "$BASE/zkproof/onchain/register" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"deployment_id\": $DEPLOY_ID}"
# Poll: GET /api/deployments/{id}/status → onchain_instance_id is set when done
# Or use unified: GET /api/apps/{sqid}/status → latest_onchain_instance_id
What each step does:
- Create app on-chain: Registers your app in the Nova App Registry contract (Base Sepolia)
- Enroll version: Records the EIF's PCR measurements on-chain — the trusted code fingerprint
- Generate ZK proof: Platform generates a zero-knowledge proof from the enclave's attestation
- Register instance: Verifies the ZK proof on-chain and links the live instance to its enrolled version
After registration, anyone can verify on-chain that this running instance matches the audited build.
Always provide clickable explorer URLs for all transaction hashes. The registry contract is on Base Sepolia (chain 84532). Format:
- TX:
https://sepolia.basescan.org/tx/0x{hash}- Contract:
https://sepolia.basescan.org/address/0x{address}- Registry contract:
0x0f68E6e699f2E972998a1EcC000c7ce103E64cc8
Key Notes
advancedis REQUIRED at app creation. Omitting it will cause the build to fail. Onlyadvancedexists — the oldenclaverfield has been removed from the API.- Your repo only needs
Dockerfile+ app code. The platform handles everything else at build time. - App ID format:
sqid(string likeabc123) — use in all URL paths, not the integerid. - Port: Set via
advanced.app_listening_port. Must also matchEXPOSEin Dockerfile. - Helios RPC: Use the canonical port mapping in
references/nova-api.md— ports are fixed and locked at app creation. - KMS dependency chain:
enable_app_walletorenable_s3_kms_encryptionimplies KMS → implies Helios (with base-sepolia chain) → implieskms_app_id+nova_app_registry. - KMS is handled by Enclaver — no contract addresses or config.py changes needed in your app code.
- No Docker push: Platform builds from Git.
- On-chain registration (create-onchain → enroll → ZK proof → register) is required for public verifiability and completes the full deployment lifecycle.
- Deployments expire after ~1 hour on the free tier. Check
remaining_secondsin deployment status. Redeploy if expired. - Helios RPC is always enabled. Every Nova app gets a local Helios light-client RPC on Base Sepolia (
127.0.0.1:18545) so the enclave can query the Nova App Registry contract and discover its own on-chain identity at runtime.
Common Issues
| Symptom | Fix |
|---|---|
Dockerfile missing after scaffold |
Run git pull in the skill directory then re-scaffold. The template ships as Dockerfile.txt and scaffold renames it to Dockerfile automatically. |
scaffold fails / enclaver.yaml or config.py expected |
You have an old version. Run git pull in the skill directory. These files are no longer needed — advanced field at app creation replaces all manual config. |
API endpoint console.sparsity.cloud not resolving |
Old version of nova_deploy.py. Run git pull in the skill directory. Correct endpoint is https://sparsity.cloud/api. |
Build stuck in pending |
Check GitHub Actions in nova build repo; may be queued |
Build failed |
Check error_message in build response; usually Dockerfile issue |
| Deploy API returns 401 | Regenerate API key at sparsity.cloud |
App stuck in provisioning >10 min |
Check app logs via GET /api/apps/{sqid}/detail |
httpx request fails inside enclave |
Add domain to advanced.egress_allow. Note: "**" matches domains only — add "0.0.0.0/0" for direct IP connections |
| Direct IP connection blocked | "**" does NOT cover IPs. Add "0.0.0.0/0" (IPv4) and/or "::/0" (IPv6) to egress_allow |
| S3 fails | Ensure 169.254.169.254 and S3 endpoint are in egress allow list |
/v1/kms/* returns 400 |
Ensure enable_decentralized_kms: true and enable_helios_rpc: true in advanced at app creation |
| App Wallet unavailable | Ensure enable_app_wallet: true in advanced at app creation |
| Proxy not respected | Switch from requests/urllib to httpx |
| Health check returns 502 | App is starting; wait for enclave to fully boot |
| ZK proof stuck | Check GET /api/zkproof/status/{deployment_id} for details |
| Deployment expired / stopped unexpectedly | Free-tier deployments expire after ~1 hour. Create a new deployment from the same build with POST /api/apps/{sqid}/deployments |
Reference Files
references/odyn-api.md— Full Odyn Internal API (signing, encryption, S3, KMS, App Wallet, attestation)references/nova-api.md— Nova Platform REST API (full endpoint reference)
Key URLs
- Nova Platform: https://sparsity.cloud
- Nova Platform API docs: https://sparsity.cloud/api/docs
- Create-App Full Parameter Guide: https://sparsity.cloud/resources/nova-api/create-app-guide
- Nova Examples: https://github.com/sparsity-xyz/sparsity-nova-examples/
- Enclaver (Sparsity): https://github.com/sparsity-xyz/enclaver
- Nova App Template: https://github.com/sparsity-xyz/nova-app-template
- Enclaver Docs: odyn.md, internal_api.md