DEV Community

Cover image for Building a Confidential AI Agent on Terminal 3: Weather Agent Inside a TDX Enclave
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on

Building a Confidential AI Agent on Terminal 3: Weather Agent Inside a TDX Enclave

How to run Rust code inside a hardware TEE that calls OpenWeatherMap — without the API key ever leaving the enclave.


The Problem

AI agents are useless without access to real APIs, real credentials, and real user data. But every external call introduces a trust question: do we trust the agent's operator not to log our API key? Do we trust the node runner not to peek at the response?

Traditional solutions stack SLAs and compliance paperwork. Terminal 3 (T3N) takes a different approach: hardware-enforced confidentiality. Your code runs inside an Intel TDX Trusted Execution Environment — a hardware black box that even the node operator cannot inspect.

This post walks through building a weather agent that proves the pattern end-to-end.


Architecture Overview

┌──────────────┐     encrypted session      ┌──────────────────────┐
│  User CLI    │ ─────────────────────────→ │  T3N Node            │
│  (tsx + SDK) │                             │  ┌────────────────┐  │
└──────────────┘                             │  │  TDX Enclave   │  │
                                             │  │  ┌──────────┐  │  │
                                             │  │  │ Weather  │  │  │
                                             │  │  │ Contract │──┼──┼──→ OpenWeatherMap
                                             │  │  │ (WASM)   │  │  │
                                             │  │  └────┬─────┘  │  │
                                             │  │       │        │  │
                                             │  │  kv_store::get() │  │
                                             │  │       │        │  │
                                             │  │  ┌────▼─────┐  │  │
                                             │  │  │  Sealed  │  │  │
                                             │  │  │  KV Map  │  │  │
                                             │  │  └──────────┘  │  │
                                             │  └────────────────┘  │
                                             └──────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Three layers of protection:

  1. Transport: All communication is encrypted with ML-KEM (post-quantum) through the T3N SDK's session layer
  2. Compute: The WASM contract executes inside an Intel TDX enclave — memory encrypted by the CPU, attestable remotely
  3. Secrets: API keys are written via the control plane (map-entry-set), bypassing even the map's own ACL, and can only be read by kv_store::get() from inside the enclave

The WIT Interface

WebAssembly Interface Types (WIT) declares the contract's boundary — what it imports from the host and what it exports to callers:

package z:tenant-weather@0.1.0;

world tenant-weather {
    // Host capabilities the contract needs
    import host:tenant/tenant-context@1.0.0;  // → tenant-did, contract-id
    import host:interfaces/logging@2.1.0;     // → info, debug, error
    import host:interfaces/kv-store@2.1.0;    // → get, put, delete, scan
    import host:interfaces/http@2.1.0;        // → call (GET/POST/etc.)

    // What callers can invoke
    export contracts;
}

interface contracts {
    record generic-input {
        input: option<list<u8>>,
        user-profile: option<list<u8>>,
        context: option<list<u8>>,
    }

    get-weather: func(req: generic-input) -> result<list<u8>, string>;
}
Enter fullscreen mode Exit fullscreen mode

Every capability the contract has is declared explicitly here. No implicit access to filesystem, network, or secrets. The host enforces this at the Wasm boundary.


Contract Implementation

Reading Secrets Inside the TEE

The contract resolves the tenant DID at runtime, constructs the map name, and reads the API key:

fn get_api_key() -> Result<String, String> {
    let tid = tenant_context::tenant_did();
    let map_name = format!("z:{}:secrets", hex::encode(&tid));
    let bytes = kv_store::get(&map_name, b"weather_api_key")
        .map_err(|e| format!("kv read: {e}"))?
        .ok_or("weather_api_key not found")?;
    String::from_utf8(bytes).map_err(|e| e.to_string())
}
Enter fullscreen mode Exit fullscreen mode

The key was seeded via the SDK's control plane (tenant.executeControl("map-entry-set", ...)) — it writes directly to the underlying storage, bypassing the map's writers ACL. This is a one-time operation done during deployment. After that, no external observer can read it back.

Making HTTP from Inside the Enclave

let resp = http_iface::call(&http_iface::Request {
    method: http_iface::Verb::Get,
    url: format!("{OWM_BASE}/data/2.5/weather?q={}&appid={}&units=metric",
                 city, api_key),
    headers: None,
    payload: None,
})?;
Enter fullscreen mode Exit fullscreen mode

The HTTP call goes through the host's egress proxy. Before the request leaves the enclave, the host checks: does the calling user's authorization grant allow this contract to dial api.openweathermap.org? If not — the request is denied with egress_denied.

Parsing and Returning

let weather = WeatherResp {
    city: req.city,
    temperature: data["main"]["temp"].as_f64().unwrap_or(0.0),
    feels_like: data["main"]["feels_like"].as_f64().unwrap_or(0.0),
    humidity: data["main"]["humidity"].as_u64().unwrap_or(0) as u32,
    description: data["weather"][0]["description"]
        .as_str().unwrap_or("unknown").to_string(),
    wind_speed: data["wind"]["speed"].as_f64().unwrap_or(0.0),
};

serde_json::to_vec(&weather)
Enter fullscreen mode Exit fullscreen mode

The JSON-serialized response is encrypted through the session and returned to the caller — who never saw the API key, never touched OpenWeatherMap directly, and whose only interaction with the TEE was a signed invocation.


Deployment Script

The TypeScript SDK orchestrates the full lifecycle:

// 1. Authenticate
setEnvironment("testnet");
const t3n = new T3nClient({
  wasmComponent: await loadWasmComponent(),
  handlers: { EthSign: metamask_sign(address, undefined, key) },
});
await t3n.handshake();
const did = await t3n.authenticate(createEthAuthInput(address));

// 2. Register the WASM component
const tenant = new TenantClient({ t3n, baseUrl: getNodeUrl(), tenantDid });
const { contract_id } = await tenant.contracts.register({
  tail: "weather-agent", version: "0.1.0", wasm: wasmBytes,
});

// 3. Create sealed KV map (only this contract can access)
await tenant.maps.create({
  tail: "secrets", visibility: "private",
  writers: { only: [contract_id] },
  readers: { only: [contract_id] },
});

// 4. Seed the API key (control-plane bypasses ACL)
await tenant.executeControl("map-entry-set", {
  map_name: tenant.canonicalName("secrets"),
  key: "weather_api_key", value: OWM_API_KEY,
});

// 5. Grant the contract permission to call OpenWeatherMap
await t3n.execute({
  script_name: "tee:user/contracts",
  function_name: "agent-auth-update",
  input: {
    agents: [{
      agentDid: tenantDid,
      scripts: [{
        scriptName, functions: ["get-weather"],
        allowedHosts: ["api.openweathermap.org"],
      }],
    }],
  },
});

// 6. Invoke
const result = await t3n.executeAndDecode({
  script_name: scriptName, function_name: "get-weather",
  input: { city: "Tokyo" },
});
Enter fullscreen mode Exit fullscreen mode

The script is designed to be idempotent — if the contract version already exists, registration is skipped, the map ACL is updated in place, and only the invocation costs tokens on re-runs.


Live Output

$ npm run deploy

authenticated as did:t3n:2c9d71730c17e69e394afbffb973d1a6444f4423
registered z:...:weather-agent as contract id 260
seeded weather_api_key into z:<tid>:secrets
authorized outbound HTTP to api.openweathermap.org

weather result: {
  "city": "Tokyo",
  "temperature": 23.18,
  "feels_like": 23.85,
  "humidity": 88,
  "description": "moderate rain",
  "wind_speed": 4.78
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters

The weather agent is deliberately minimal, but the pattern generalizes to any confidential agent workflow:

Scenario Secret External API Pattern
Weather OpenWeatherMap key api.openweathermap.org This demo
Payroll Bank API credentials + PII bank.disbursement.com http-with-placeholders for PII substitution
Travel booking Duffel/Amadeus API key api.duffel.com Same pattern + PII placeholders
KYC/Identity Veriff/Onfido key + user docs api.veriff.com PII substitution; user profile never in WASM memory
Healthcare EHR API token + patient data fhir.ehr.com PII substitution; HIPAA-relevant audit trail

The key insight: the contract holds credentials, not the agent. The agent calls the contract; the contract uses the credentials inside the TEE. If the agent is compromised, the attacker gets a function call — not the API key.


The Full Stack

Layer Technology
Contract language Rust
Target wasm32-wasip2 (WASI Preview 2 component)
Code generation wit-bindgen 0.49
Host interfaces kv-store, http, logging, tenant-context
Client SDK @terminal3/t3n-sdk 3.9.0
TEE technology Intel TDX (trusted execution environment)
Network T3N testnet (public sandbox with test tokens)
External data OpenWeatherMap API
Serialization serde + serde_json (no-std, alloc-only)

Try It Yourself

  1. Get a T3N API key at terminal3.io/claim-page (20,000 free test tokens)
  2. Get an OpenWeatherMap API key
  3. Clone the repo, run npm install && cargo build --release && npm run deploy

Full source code and documentation: https://github.com/harishkotra/weather-agent


Built with Terminal 3 — the agent developer kit for confidential, auditable AI agents.

Top comments (0)