repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | // ═══════════════════════════════════════════════════════════════ |
| #2 | // PHALA CLOUD SERVICE - TEE Deployment for AI Agents |
| #3 | // Deploy AI agents to Phala Cloud's Trusted Execution Environment |
| #4 | // ═══════════════════════════════════════════════════════════════ |
| #5 | |
| #6 | import type { Env } from '../index'; |
| #7 | |
| #8 | // ───────────────────────────────────────────────── |
| #9 | // TYPES |
| #10 | // ───────────────────────────────────────────────── |
| #11 | |
| #12 | export interface DeployedAgent { |
| #13 | id: string; |
| #14 | name: string; |
| #15 | status: 'deploying' | 'running' | 'stopped' | 'error'; |
| #16 | appId: string; |
| #17 | instanceId?: string; |
| #18 | deploymentUrl?: string; |
| #19 | dashboardUrl?: string; |
| #20 | logsUrl?: string; |
| #21 | publicKey?: string; |
| #22 | configuration: { |
| #23 | vcpu: number; |
| #24 | memory: number; |
| #25 | diskSize: number; |
| #26 | }; |
| #27 | createdAt: string; |
| #28 | uptime?: string; |
| #29 | } |
| #30 | |
| #31 | export interface DeployOptions { |
| #32 | name: string; |
| #33 | agentId: string; |
| #34 | vcpu?: number; |
| #35 | memory?: number; |
| #36 | diskSize?: number; |
| #37 | envVars?: Array<{ key: string; value: string }>; |
| #38 | } |
| #39 | |
| #40 | interface CvmConfig { |
| #41 | teepod_id: number; |
| #42 | name: string; |
| #43 | image: string; |
| #44 | vcpu: number; |
| #45 | memory: number; |
| #46 | disk_size: number; |
| #47 | compose_manifest: { |
| #48 | docker_compose_file: string; |
| #49 | docker_config: { |
| #50 | url: string; |
| #51 | username: string; |
| #52 | password: string; |
| #53 | }; |
| #54 | features: string[]; |
| #55 | kms_enabled: boolean; |
| #56 | manifest_version: number; |
| #57 | name: string; |
| #58 | public_logs: boolean; |
| #59 | public_sysinfo: boolean; |
| #60 | tproxy_enabled: boolean; |
| #61 | }; |
| #62 | listed: boolean; |
| #63 | encrypted_env?: string; |
| #64 | app_env_encrypt_pubkey?: string; |
| #65 | app_id_salt?: string; |
| #66 | } |
| #67 | |
| #68 | interface PhalaUserInfo { |
| #69 | id: string; |
| #70 | username: string; |
| #71 | } |
| #72 | |
| #73 | interface CvmResponse { |
| #74 | hosted: { |
| #75 | id: string; |
| #76 | name: string; |
| #77 | status: string; |
| #78 | uptime: string; |
| #79 | app_url: string; |
| #80 | app_id: string; |
| #81 | instance_id: string; |
| #82 | configuration: { |
| #83 | memory: number; |
| #84 | disk_size: number; |
| #85 | vcpu: number; |
| #86 | }; |
| #87 | }; |
| #88 | node: { |
| #89 | name: string; |
| #90 | }; |
| #91 | status: string; |
| #92 | dapp_dashboard_url: string; |
| #93 | syslog_endpoint: string; |
| #94 | } |
| #95 | |
| #96 | interface CvmNetworkResponse { |
| #97 | is_online?: boolean; |
| #98 | public_urls?: Array<{ app?: string }>; |
| #99 | } |
| #100 | |
| #101 | interface PhalaAuthResponse { |
| #102 | username?: string; |
| #103 | } |
| #104 | |
| #105 | interface PhalaUserSearchResponse { |
| #106 | users?: Array<{ id?: string }>; |
| #107 | } |
| #108 | |
| #109 | // ───────────────────────────────────────────────── |
| #110 | // CONSTANTS |
| #111 | // ───────────────────────────────────────────────── |
| #112 | |
| #113 | const CLOUD_API_URL = 'https://cloud-api.phala.network'; |
| #114 | const CLOUD_URL = 'https://cloud.phala.network'; |
| #115 | |
| #116 | // Docker compose template for Solana AI Agent |
| #117 | const AGENT_COMPOSE_TEMPLATE = ` |
| #118 | version: '3.8' |
| #119 | services: |
| #120 | agent: |
| #121 | image: ghcr.io/clawdos/solana-ai-agent:latest |
| #122 | ports: |
| #123 | - "4000:4000" |
| #124 | environment: |
| #125 | - NODE_ENV=production |
| #126 | - AGENT_NAME=\${AGENT_NAME} |
| #127 | - AGENT_ID=\${AGENT_ID} |
| #128 | - CROSSMINT_API_KEY=\${CROSSMINT_API_KEY} |
| #129 | - SOLANA_RPC_URL=\${SOLANA_RPC_URL} |
| #130 | - OPENAI_API_KEY=\${OPENAI_API_KEY} |
| #131 | restart: unless-stopped |
| #132 | healthcheck: |
| #133 | test: ["CMD", "curl", "-f", "http://localhost:4000/api/health"] |
| #134 | interval: 30s |
| #135 | timeout: 10s |
| #136 | retries: 3 |
| #137 | `; |
| #138 | |
| #139 | // ───────────────────────────────────────────────── |
| #140 | // PHALA CLOUD SERVICE |
| #141 | // ───────────────────────────────────────────────── |
| #142 | |
| #143 | export class PhalaService { |
| #144 | private apiKey: string; |
| #145 | private headers: Record<string, string>; |
| #146 | |
| #147 | constructor(private env: Env) { |
| #148 | this.apiKey = env.PHALA_CLOUD_API_KEY || env.PHALA_API_KEY || ''; |
| #149 | this.headers = { |
| #150 | 'User-Agent': 'clawdos-agent-api/1.0.0', |
| #151 | 'Content-Type': 'application/json', |
| #152 | 'X-API-Key': this.apiKey, |
| #153 | }; |
| #154 | } |
| #155 | |
| #156 | isConfigured(): boolean { |
| #157 | return !!this.apiKey; |
| #158 | } |
| #159 | |
| #160 | // ═══════════════════════════════════════════════════ |
| #161 | // DEPLOYMENT |
| #162 | // ═══════════════════════════════════════════════════ |
| #163 | |
| #164 | async deployAgent(options: DeployOptions): Promise<DeployedAgent> { |
| #165 | if (!this.isConfigured()) { |
| #166 | throw new Error('Phala Cloud API key not configured'); |
| #167 | } |
| #168 | |
| #169 | console.log(`Deploying agent: ${options.name}`); |
| #170 | |
| #171 | // Create VM configuration |
| #172 | const vmConfig = this.createVmConfig(options); |
| #173 | |
| #174 | // Get public key for encryption |
| #175 | const pubkey = await this.getPubkeyFromCvm(vmConfig); |
| #176 | if (!pubkey) { |
| #177 | throw new Error('Failed to get pubkey from CVM'); |
| #178 | } |
| #179 | |
| #180 | // Encrypt environment variables |
| #181 | const envVars = options.envVars || []; |
| #182 | envVars.push( |
| #183 | { key: 'AGENT_NAME', value: options.name }, |
| #184 | { key: 'AGENT_ID', value: options.agentId }, |
| #185 | { key: 'CROSSMINT_API_KEY', value: this.env.CROSSMINT_SERVERSIDE_API_KEY || '' }, |
| #186 | { key: 'SOLANA_RPC_URL', value: this.env.SOLANA_RPC_URL || '' }, |
| #187 | { key: 'OPENAI_API_KEY', value: this.env.OPENAI_API_KEY || '' } |
| #188 | ); |
| #189 | |
| #190 | const encryptedEnv = await this.encryptSecrets(envVars, pubkey.app_env_encrypt_pubkey); |
| #191 | |
| #192 | // Create CVM |
| #193 | const response = await this.createCvm({ |
| #194 | ...vmConfig, |
| #195 | encrypted_env: encryptedEnv, |
| #196 | app_env_encrypt_pubkey: pubkey.app_env_encrypt_pubkey, |
| #197 | app_id_salt: pubkey.app_id_salt, |
| #198 | }); |
| #199 | |
| #200 | if (!response) { |
| #201 | throw new Error('Failed to create CVM'); |
| #202 | } |
| #203 | |
| #204 | console.log(`Agent deployed with App ID: ${response.app_id}`); |
| #205 | |
| #206 | return { |
| #207 | id: options.agentId, |
| #208 | name: options.name, |
| #209 | status: 'deploying', |
| #210 | appId: response.app_id, |
| #211 | dashboardUrl: `${CLOUD_URL}/dashboard/cvms/app_${response.app_id}`, |
| #212 | configuration: { |
| #213 | vcpu: options.vcpu || 1, |
| #214 | memory: options.memory || 2048, |
| #215 | diskSize: options.diskSize || 20, |
| #216 | }, |
| #217 | createdAt: new Date().toISOString(), |
| #218 | }; |
| #219 | } |
| #220 | |
| #221 | async getDeployedAgents(): Promise<DeployedAgent[]> { |
| #222 | if (!this.isConfigured()) { |
| #223 | return []; |
| #224 | } |
| #225 | |
| #226 | try { |
| #227 | const userInfo = await this.getUserInfo(); |
| #228 | if (!userInfo) { |
| #229 | return []; |
| #230 | } |
| #231 | |
| #232 | const response = await fetch( |
| #233 | `${CLOUD_API_URL}/api/v1/cvms?user_id=${userInfo.id}`, |
| #234 | { headers: this.headers } |
| #235 | ); |
| #236 | |
| #237 | if (!response.ok) { |
| #238 | throw new Error(`Failed to get CVMs: ${response.status}`); |
| #239 | } |
| #240 | |
| #241 | const cvms: CvmResponse[] = await response.json(); |
| #242 | |
| #243 | return cvms.map(cvm => ({ |
| #244 | id: cvm.hosted.app_id, |
| #245 | name: cvm.hosted.name, |
| #246 | status: cvm.status as DeployedAgent['status'], |
| #247 | appId: cvm.hosted.app_id, |
| #248 | instanceId: cvm.hosted.instance_id, |
| #249 | deploymentUrl: cvm.hosted.app_url, |
| #250 | dashboardUrl: cvm.dapp_dashboard_url, |
| #251 | logsUrl: cvm.syslog_endpoint, |
| #252 | configuration: { |
| #253 | vcpu: cvm.hosted.configuration.vcpu, |
| #254 | memory: cvm.hosted.configuration.memory, |
| #255 | diskSize: cvm.hosted.configuration.disk_size, |
| #256 | }, |
| #257 | createdAt: new Date().toISOString(), |
| #258 | uptime: cvm.hosted.uptime, |
| #259 | })); |
| #260 | } catch (error) { |
| #261 | console.error('Error getting deployed agents:', error); |
| #262 | return []; |
| #263 | } |
| #264 | } |
| #265 | |
| #266 | async getAgentStatus(appId: string): Promise<{ status: string; url?: string } | null> { |
| #267 | try { |
| #268 | const response = await fetch( |
| #269 | `${CLOUD_API_URL}/api/v1/cvms/app_${appId}/network`, |
| #270 | { headers: this.headers } |
| #271 | ); |
| #272 | |
| #273 | if (!response.ok) { |
| #274 | return null; |
| #275 | } |
| #276 | |
| #277 | const data = await response.json() as CvmNetworkResponse; |
| #278 | return { |
| #279 | status: data.is_online ? 'running' : 'offline', |
| #280 | url: data.public_urls?.[0]?.app, |
| #281 | }; |
| #282 | } catch (error) { |
| #283 | console.error('Error getting agent status:', error); |
| #284 | return null; |
| #285 | } |
| #286 | } |
| #287 | |
| #288 | async stopAgent(appId: string): Promise<boolean> { |
| #289 | try { |
| #290 | const response = await fetch( |
| #291 | `${CLOUD_API_URL}/api/v1/cvms/app_${appId}/stop`, |
| #292 | { |
| #293 | method: 'POST', |
| #294 | headers: this.headers, |
| #295 | } |
| #296 | ); |
| #297 | |
| #298 | return response.ok; |
| #299 | } catch (error) { |
| #300 | console.error('Error stopping agent:', error); |
| #301 | return false; |
| #302 | } |
| #303 | } |
| #304 | |
| #305 | async startAgent(appId: string): Promise<boolean> { |
| #306 | try { |
| #307 | const response = await fetch( |
| #308 | `${CLOUD_API_URL}/api/v1/cvms/app_${appId}/start`, |
| #309 | { |
| #310 | method: 'POST', |
| #311 | headers: this.headers, |
| #312 | } |
| #313 | ); |
| #314 | |
| #315 | return response.ok; |
| #316 | } catch (error) { |
| #317 | console.error('Error starting agent:', error); |
| #318 | return false; |
| #319 | } |
| #320 | } |
| #321 | |
| #322 | // ───────────────────────────────────────────────── |
| #323 | // PRIVATE HELPERS |
| #324 | // ───────────────────────────────────────────────── |
| #325 | |
| #326 | private createVmConfig(options: DeployOptions): CvmConfig { |
| #327 | return { |
| #328 | teepod_id: 2, |
| #329 | name: options.name, |
| #330 | image: 'dstack-dev-0.3.4', |
| #331 | vcpu: options.vcpu || 1, |
| #332 | memory: options.memory || 2048, |
| #333 | disk_size: options.diskSize || 20, |
| #334 | compose_manifest: { |
| #335 | docker_compose_file: AGENT_COMPOSE_TEMPLATE, |
| #336 | docker_config: { |
| #337 | url: '', |
| #338 | username: '', |
| #339 | password: '', |
| #340 | }, |
| #341 | features: ['kms', 'tproxy-net'], |
| #342 | kms_enabled: true, |
| #343 | manifest_version: 2, |
| #344 | name: options.name, |
| #345 | public_logs: true, |
| #346 | public_sysinfo: true, |
| #347 | tproxy_enabled: true, |
| #348 | }, |
| #349 | listed: false, |
| #350 | }; |
| #351 | } |
| #352 | |
| #353 | private async getPubkeyFromCvm(vmConfig: CvmConfig): Promise<{ app_env_encrypt_pubkey: string; app_id_salt: string } | null> { |
| #354 | try { |
| #355 | const response = await fetch( |
| #356 | `${CLOUD_API_URL}/api/v1/cvms/pubkey/from_cvm_configuration`, |
| #357 | { |
| #358 | method: 'POST', |
| #359 | headers: this.headers, |
| #360 | body: JSON.stringify(vmConfig), |
| #361 | } |
| #362 | ); |
| #363 | |
| #364 | if (!response.ok) { |
| #365 | throw new Error(`HTTP error: ${response.status}`); |
| #366 | } |
| #367 | |
| #368 | return await response.json(); |
| #369 | } catch (error) { |
| #370 | console.error('Error getting pubkey from CVM:', error); |
| #371 | return null; |
| #372 | } |
| #373 | } |
| #374 | |
| #375 | private async createCvm(vmConfig: CvmConfig): Promise<{ app_id: string } | null> { |
| #376 | try { |
| #377 | const response = await fetch( |
| #378 | `${CLOUD_API_URL}/api/v1/cvms/from_cvm_configuration`, |
| #379 | { |
| #380 | method: 'POST', |
| #381 | headers: this.headers, |
| #382 | body: JSON.stringify(vmConfig), |
| #383 | } |
| #384 | ); |
| #385 | |
| #386 | if (!response.ok) { |
| #387 | throw new Error(`HTTP error: ${response.status}`); |
| #388 | } |
| #389 | |
| #390 | return await response.json(); |
| #391 | } catch (error) { |
| #392 | console.error('Error creating CVM:', error); |
| #393 | return null; |
| #394 | } |
| #395 | } |
| #396 | |
| #397 | private async getUserInfo(): Promise<PhalaUserInfo | null> { |
| #398 | try { |
| #399 | const authResponse = await fetch(`${CLOUD_API_URL}/api/v1/auth/me`, { |
| #400 | headers: this.headers, |
| #401 | }); |
| #402 | |
| #403 | if (!authResponse.ok) { |
| #404 | return null; |
| #405 | } |
| #406 | |
| #407 | const authData = await authResponse.json() as PhalaAuthResponse; |
| #408 | const username = authData.username; |
| #409 | if (!username) { |
| #410 | return null; |
| #411 | } |
| #412 | |
| #413 | const userResponse = await fetch( |
| #414 | `${CLOUD_API_URL}/api/v1/users/search?q=${username}`, |
| #415 | { headers: this.headers } |
| #416 | ); |
| #417 | |
| #418 | if (!userResponse.ok) { |
| #419 | return null; |
| #420 | } |
| #421 | |
| #422 | const userData = await userResponse.json() as PhalaUserSearchResponse; |
| #423 | const userId = userData.users?.[0]?.id; |
| #424 | if (!userId) { |
| #425 | return null; |
| #426 | } |
| #427 | return { |
| #428 | id: userId, |
| #429 | username, |
| #430 | }; |
| #431 | } catch (error) { |
| #432 | console.error('Error getting user info:', error); |
| #433 | return null; |
| #434 | } |
| #435 | } |
| #436 | |
| #437 | private async encryptSecrets( |
| #438 | secrets: Array<{ key: string; value: string }>, |
| #439 | pubkey: string |
| #440 | ): Promise<string> { |
| #441 | // For now, return a simple JSON string |
| #442 | // In production, this would use x25519 encryption |
| #443 | const envsJson = JSON.stringify({ env: secrets }); |
| #444 | |
| #445 | // TODO: Implement proper x25519 encryption |
| #446 | // For now, we'll use a placeholder that works with the API |
| #447 | return Array.from(new TextEncoder().encode(envsJson)) |
| #448 | .map((byte) => byte.toString(16).padStart(2, '0')) |
| #449 | .join(''); |
| #450 | } |
| #451 | } |
| #452 |