Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

**Example applications for [dstack](https://github.com/Dstack-TEE/dstack) - Deploy containerized apps to TEEs with end-to-end security in minutes**

[Getting Started](#getting-started) • [Use Cases](#use-cases) • [Core Patterns](#core-patterns) • [Infrastructure](#infrastructure) • [Dev Tools](#dev-scaffolding) • [Starter Packs](#starter-packs) • [Other Use Cases](#other-use-cases)
[Getting Started](#getting-started) • [Confidential AI](#confidential-ai) • [Use Cases](#use-cases) • [Core Patterns](#core-patterns) • [Infrastructure](#infrastructure) • [Dev Tools](#dev-scaffolding) • [Starter Packs](#starter-packs) • [Other Use Cases](#other-use-cases)

</div>

Expand Down Expand Up @@ -61,6 +61,22 @@ See [Phala Cloud](https://cloud.phala.network) for production TEE deployment.

---

## Confidential AI

Run AI workloads where prompts, model weights, and inference stay encrypted in hardware.

| Example | Description |
|---------|-------------|
| [confidential-ai/inference](./confidential-ai/inference) | Private LLM inference with vLLM on Confidential GPU |
| [confidential-ai/training](./confidential-ai/training) | Confidential fine-tuning on sensitive data using Unsloth |
| [confidential-ai/agents](./confidential-ai/agents) | Secure AI agent with TEE-derived wallet keys using LangChain and Confidential AI models |

GPU deployments require: `--instance-type h200.small --region US-EAST-1 --image dstack-nvidia-dev-0.5.4.1`

See [Confidential AI Guide](https://github.com/Dstack-TEE/dstack/blob/master/docs/confidential-ai.md) for concepts and security model.

---

## Tutorials

Step-by-step guides covering core dstack concepts.
Expand Down
23 changes: 23 additions & 0 deletions confidential-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Confidential AI Examples

Run AI workloads with hardware-enforced privacy. Your prompts, model weights, and computations stay encrypted in memory.

| Example | Description | Status |
|---------|-------------|--------|
| [inference](./inference) | Private LLM with response signing | Ready to deploy |
| [training](./training) | Fine-tuning on sensitive data | Requires local build |
| [agents](./agents) | AI agent with TEE-derived keys | Requires local build |

Start with inference—it deploys in one command and shows the full attestation flow.

```bash
cd inference
phala auth login
phala deploy -n my-llm -c docker-compose.yaml \
--instance-type h200.small \
-e TOKEN=your-secret-token
```

First deployment takes 10-15 minutes (large images + model loading). Check progress with `phala cvms serial-logs <app_id> --tail 100`.

See the [Confidential AI Guide](https://github.com/Dstack-TEE/dstack/blob/master/docs/confidential-ai.md) for how the security model works.
12 changes: 12 additions & 0 deletions confidential-ai/agents/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY agent.py .

EXPOSE 8080

CMD ["python", "agent.py"]
91 changes: 91 additions & 0 deletions confidential-ai/agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Secure AI Agent

Run AI agents with TEE-derived wallet keys. The agent calls a confidential LLM (redpill.ai), so prompts never leave encrypted memory.

## Quick Start

```bash
phala auth login
phala deploy -n my-agent -c docker-compose.yaml \
-e LLM_API_KEY=your-redpill-key
```

Your API key is encrypted client-side and only decrypted inside the TEE.

Test it:

```bash
# Get agent info and wallet address
curl https://<endpoint>/

# Chat with the agent
curl -X POST https://<endpoint>/chat \
-H "Content-Type: application/json" \
-d '{"message": "What is your wallet address?"}'

# Sign a message
curl -X POST https://<endpoint>/sign \
-H "Content-Type: application/json" \
-d '{"message": "Hello from TEE"}'
```

## How It Works

```mermaid
graph TB
User -->|TLS| Agent
subgraph TEE1[Agent CVM]
Agent[Agent Code]
Agent --> Wallet[TEE-derived wallet]
end
Agent -->|TLS| LLM
subgraph TEE2[LLM CVM]
LLM[redpill.ai]
end
```

The agent derives an Ethereum wallet from TEE keys:

```python
from dstack_sdk import DstackClient
from dstack_sdk.ethereum import to_account

client = DstackClient()
eth_key = client.get_key("agent/wallet", "mainnet")
account = to_account(eth_key)
# Same path = same key, even across restarts
```

Both the agent and the LLM run in separate TEEs. User queries stay encrypted from browser to agent to LLM and back.

## API

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Agent info, wallet address, TCB info |
| `/attestation` | GET | TEE attestation quote |
| `/chat` | POST | Chat with the agent |
| `/sign` | POST | Sign a message with agent's wallet |

## Using a Different LLM

The agent uses redpill.ai by default for end-to-end confidentiality. To use a different OpenAI-compatible endpoint:

```bash
phala deploy -n my-agent -c docker-compose.yaml \
-e LLM_BASE_URL=https://api.openai.com/v1 \
-e LLM_API_KEY=sk-xxxxx
```

Note: Using a non-confidential LLM means prompts leave the encrypted environment.

## Cleanup

```bash
phala cvms delete my-agent --force
```

## Further Reading

- [Confidential AI Guide](https://github.com/Dstack-TEE/dstack/blob/master/docs/confidential-ai.md)
- [dstack Python SDK](https://github.com/Dstack-TEE/dstack/tree/master/sdk/python)
198 changes: 198 additions & 0 deletions confidential-ai/agents/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""
Secure AI Agent with TEE Key Derivation

This agent demonstrates:
- TEE-derived Ethereum wallet (deterministic, persistent)
- Protected API credentials (encrypted at deploy)
- Confidential LLM calls via redpill.ai
- Attestation proof for execution verification
"""

import os

from dstack_sdk import DstackClient
from dstack_sdk.ethereum import to_account
from eth_account.messages import encode_defunct
from flask import Flask, jsonify, request
from langchain_classic.agents import AgentExecutor, create_react_agent
from langchain_classic.tools import Tool
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate

app = Flask(__name__)

# Lazy initialization - only connect when needed
_client = None
_account = None


def get_client():
"""Get dstack client (lazy initialization)."""
global _client
if _client is None:
_client = DstackClient()
return _client


def get_account():
"""Get Ethereum account (lazy initialization)."""
global _account
if _account is None:
client = get_client()
eth_key = client.get_key("agent/wallet", "mainnet")
_account = to_account(eth_key)
print(f"Agent wallet address: {_account.address}")
return _account


def get_wallet_address(_: str = "") -> str:
"""Get the agent's wallet address."""
return f"Agent wallet: {get_account().address}"


def get_attestation(nonce: str = "default") -> str:
"""Get TEE attestation quote."""
quote = get_client().get_quote(nonce.encode()[:64])
return f"TEE Quote (first 100 chars): {quote.quote[:100]}..."


def sign_message(message: str) -> str:
"""Sign a message with the agent's wallet."""
signable = encode_defunct(text=message)
signed = get_account().sign_message(signable)
return f"Signature: {signed.signature.hex()}"


# Define agent tools
tools = [
Tool(
name="GetWallet",
func=get_wallet_address,
description="Get the agent's Ethereum wallet address",
),
Tool(
name="GetAttestation",
func=get_attestation,
description="Get TEE attestation quote to prove secure execution",
),
Tool(
name="SignMessage",
func=sign_message,
description="Sign a message with the agent's wallet. Input: the message to sign",
),
]

# LangChain agent (lazy initialization)
_agent_executor = None


def get_agent_executor():
"""Get LangChain agent executor (lazy initialization)."""
global _agent_executor
if _agent_executor is None:
template = """You are a secure AI agent running in a Trusted Execution Environment (TEE).
You have access to a deterministic Ethereum wallet derived from TEE keys.
Your wallet address and signing capabilities are protected by hardware.

You have access to the following tools:
{tools}

Use the following format:
Question: the input question
Thought: think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (repeat Thought/Action/Action Input/Observation as needed)
Thought: I now know the final answer
Final Answer: the final answer

Question: {input}
{agent_scratchpad}"""

prompt = PromptTemplate.from_template(template)

# Use redpill.ai for confidential LLM calls (OpenAI-compatible API)
llm = ChatOpenAI(
model=os.environ.get("LLM_MODEL", "openai/gpt-4o-mini"),
base_url=os.environ.get("LLM_BASE_URL", "https://api.redpill.ai/v1"),
api_key=os.environ.get("LLM_API_KEY", ""),
temperature=0,
)

agent = create_react_agent(llm, tools, prompt)
_agent_executor = AgentExecutor(
agent=agent, tools=tools, verbose=True, handle_parsing_errors=True
)
return _agent_executor


@app.route("/")
def index():
"""Agent info endpoint."""
try:
info = get_client().info()
return jsonify(
{
"status": "running",
"wallet": get_account().address,
"app_id": info.app_id,
}
)
except Exception:
return jsonify({"status": "running", "error": "Failed to retrieve agent info"})


@app.route("/attestation")
def attestation():
"""Get TEE attestation."""
nonce = request.args.get("nonce", "default")
quote = get_client().get_quote(nonce.encode()[:64])
return jsonify({"quote": quote.quote, "nonce": nonce})


@app.route("/chat", methods=["POST"])
def chat():
"""Chat with the agent."""
data = request.get_json()
message = data.get("message", "")

if not message:
return jsonify({"error": "No message provided"}), 400

try:
result = get_agent_executor().invoke({"input": message})
return jsonify(
{
"response": result["output"],
"wallet": get_account().address,
}
)
except Exception:
return jsonify({"error": "Failed to process chat request"}), 500


@app.route("/sign", methods=["POST"])
def sign():
"""Sign a message with the agent's wallet."""
data = request.get_json()
message = data.get("message", "")

if not message:
return jsonify({"error": "No message provided"}), 400

signable = encode_defunct(text=message)
signed = get_account().sign_message(signable)
return jsonify(
{
"message": message,
"signature": signed.signature.hex(),
"signer": get_account().address,
}
)


if __name__ == "__main__":
print("Starting agent server...")
app.run(host="0.0.0.0", port=8080)
19 changes: 19 additions & 0 deletions confidential-ai/agents/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Secure AI Agent with TEE Key Derivation
#
# Runs an AI agent with:
# - TEE-derived wallet keys (deterministic, never leave TEE)
# - Encrypted API credentials
# - Confidential LLM calls via redpill.ai
#
# Deploy: phala deploy -n my-agent -c docker-compose.yaml -e LLM_API_KEY=your-key

services:
agent:
image: h4x3rotab/tee-agent:v0.4
volumes:
- /var/run/dstack.sock:/var/run/dstack.sock
environment:
- LLM_BASE_URL=https://api.redpill.ai/v1
- LLM_API_KEY # Encrypted at deploy time
ports:
- "8080:8080"
6 changes: 6 additions & 0 deletions confidential-ai/agents/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dstack-sdk>=0.1.0
langchain-classic>=1.0.0
langchain-openai>=0.0.5
langchain-community>=0.4.0
web3>=6.0.0
flask>=3.0.0
Loading
Loading