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 | import { Hono } from 'hono'; |
| #2 | import type { AppEnv } from '../types'; |
| #3 | import { createAccessMiddleware } from '../auth'; |
| #4 | import { ensureMoltbotGateway, findExistingMoltbotProcess, mountR2Storage, syncToR2, waitForProcess } from '../gateway'; |
| #5 | import { R2_MOUNT_PATH } from '../config'; |
| #6 | |
| #7 | // CLI commands can take 10-15 seconds to complete due to WebSocket connection overhead |
| #8 | const CLI_TIMEOUT_MS = 20000; |
| #9 | |
| #10 | /** |
| #11 | * API routes |
| #12 | * - /api/admin/* - Protected admin API routes (Cloudflare Access required) |
| #13 | * |
| #14 | * Note: /api/status is now handled by publicRoutes (no auth required) |
| #15 | */ |
| #16 | const api = new Hono<AppEnv>(); |
| #17 | |
| #18 | /** |
| #19 | * Admin API routes - all protected by Cloudflare Access |
| #20 | */ |
| #21 | const adminApi = new Hono<AppEnv>(); |
| #22 | |
| #23 | // Middleware: Verify Cloudflare Access JWT for all admin routes |
| #24 | adminApi.use('*', createAccessMiddleware({ type: 'json' })); |
| #25 | |
| #26 | // GET /api/admin/devices - List pending and paired devices |
| #27 | adminApi.get('/devices', async (c) => { |
| #28 | const sandbox = c.get('sandbox'); |
| #29 | |
| #30 | try { |
| #31 | // Ensure moltbot is running first |
| #32 | await ensureMoltbotGateway(sandbox, c.env); |
| #33 | |
| #34 | // Run moltbot CLI to list devices (CLI is still named clawdbot until upstream renames) |
| #35 | // Must specify --url to connect to the gateway running in the same container |
| #36 | const proc = await sandbox.startProcess('clawdbot devices list --json --url ws://localhost:18789'); |
| #37 | await waitForProcess(proc, CLI_TIMEOUT_MS); |
| #38 | |
| #39 | const logs = await proc.getLogs(); |
| #40 | const stdout = logs.stdout || ''; |
| #41 | const stderr = logs.stderr || ''; |
| #42 | |
| #43 | // Try to parse JSON output |
| #44 | try { |
| #45 | // Find JSON in output (may have other log lines) |
| #46 | const jsonMatch = stdout.match(/\{[\s\S]*\}/); |
| #47 | if (jsonMatch) { |
| #48 | const data = JSON.parse(jsonMatch[0]); |
| #49 | return c.json(data); |
| #50 | } |
| #51 | |
| #52 | // If no JSON found, return raw output for debugging |
| #53 | return c.json({ |
| #54 | pending: [], |
| #55 | paired: [], |
| #56 | raw: stdout, |
| #57 | stderr, |
| #58 | }); |
| #59 | } catch { |
| #60 | return c.json({ |
| #61 | pending: [], |
| #62 | paired: [], |
| #63 | raw: stdout, |
| #64 | stderr, |
| #65 | parseError: 'Failed to parse CLI output', |
| #66 | }); |
| #67 | } |
| #68 | } catch (error) { |
| #69 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| #70 | return c.json({ error: errorMessage }, 500); |
| #71 | } |
| #72 | }); |
| #73 | |
| #74 | // POST /api/admin/devices/:requestId/approve - Approve a pending device |
| #75 | adminApi.post('/devices/:requestId/approve', async (c) => { |
| #76 | const sandbox = c.get('sandbox'); |
| #77 | const requestId = c.req.param('requestId'); |
| #78 | |
| #79 | if (!requestId) { |
| #80 | return c.json({ error: 'requestId is required' }, 400); |
| #81 | } |
| #82 | |
| #83 | try { |
| #84 | // Ensure moltbot is running first |
| #85 | await ensureMoltbotGateway(sandbox, c.env); |
| #86 | |
| #87 | // Run moltbot CLI to approve the device (CLI is still named clawdbot) |
| #88 | const proc = await sandbox.startProcess(`clawdbot devices approve ${requestId} --url ws://localhost:18789`); |
| #89 | await waitForProcess(proc, CLI_TIMEOUT_MS); |
| #90 | |
| #91 | const logs = await proc.getLogs(); |
| #92 | const stdout = logs.stdout || ''; |
| #93 | const stderr = logs.stderr || ''; |
| #94 | |
| #95 | // Check for success indicators (case-insensitive, CLI outputs "Approved ...") |
| #96 | const success = stdout.toLowerCase().includes('approved') || proc.exitCode === 0; |
| #97 | |
| #98 | return c.json({ |
| #99 | success, |
| #100 | requestId, |
| #101 | message: success ? 'Device approved' : 'Approval may have failed', |
| #102 | stdout, |
| #103 | stderr, |
| #104 | }); |
| #105 | } catch (error) { |
| #106 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| #107 | return c.json({ error: errorMessage }, 500); |
| #108 | } |
| #109 | }); |
| #110 | |
| #111 | // POST /api/admin/devices/approve-all - Approve all pending devices |
| #112 | adminApi.post('/devices/approve-all', async (c) => { |
| #113 | const sandbox = c.get('sandbox'); |
| #114 | |
| #115 | try { |
| #116 | // Ensure moltbot is running first |
| #117 | await ensureMoltbotGateway(sandbox, c.env); |
| #118 | |
| #119 | // First, get the list of pending devices (CLI is still named clawdbot) |
| #120 | const listProc = await sandbox.startProcess('clawdbot devices list --json --url ws://localhost:18789'); |
| #121 | await waitForProcess(listProc, CLI_TIMEOUT_MS); |
| #122 | |
| #123 | const listLogs = await listProc.getLogs(); |
| #124 | const stdout = listLogs.stdout || ''; |
| #125 | |
| #126 | // Parse pending devices |
| #127 | let pending: Array<{ requestId: string }> = []; |
| #128 | try { |
| #129 | const jsonMatch = stdout.match(/\{[\s\S]*\}/); |
| #130 | if (jsonMatch) { |
| #131 | const data = JSON.parse(jsonMatch[0]); |
| #132 | pending = data.pending || []; |
| #133 | } |
| #134 | } catch { |
| #135 | return c.json({ error: 'Failed to parse device list', raw: stdout }, 500); |
| #136 | } |
| #137 | |
| #138 | if (pending.length === 0) { |
| #139 | return c.json({ approved: [], message: 'No pending devices to approve' }); |
| #140 | } |
| #141 | |
| #142 | // Approve each pending device |
| #143 | const results: Array<{ requestId: string; success: boolean; error?: string }> = []; |
| #144 | |
| #145 | for (const device of pending) { |
| #146 | try { |
| #147 | const approveProc = await sandbox.startProcess(`clawdbot devices approve ${device.requestId} --url ws://localhost:18789`); |
| #148 | await waitForProcess(approveProc, CLI_TIMEOUT_MS); |
| #149 | |
| #150 | const approveLogs = await approveProc.getLogs(); |
| #151 | const success = approveLogs.stdout?.toLowerCase().includes('approved') || approveProc.exitCode === 0; |
| #152 | |
| #153 | results.push({ requestId: device.requestId, success }); |
| #154 | } catch (err) { |
| #155 | results.push({ |
| #156 | requestId: device.requestId, |
| #157 | success: false, |
| #158 | error: err instanceof Error ? err.message : 'Unknown error', |
| #159 | }); |
| #160 | } |
| #161 | } |
| #162 | |
| #163 | const approvedCount = results.filter(r => r.success).length; |
| #164 | return c.json({ |
| #165 | approved: results.filter(r => r.success).map(r => r.requestId), |
| #166 | failed: results.filter(r => !r.success), |
| #167 | message: `Approved ${approvedCount} of ${pending.length} device(s)`, |
| #168 | }); |
| #169 | } catch (error) { |
| #170 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| #171 | return c.json({ error: errorMessage }, 500); |
| #172 | } |
| #173 | }); |
| #174 | |
| #175 | // GET /api/admin/storage - Get R2 storage status and last sync time |
| #176 | adminApi.get('/storage', async (c) => { |
| #177 | const sandbox = c.get('sandbox'); |
| #178 | const hasCredentials = !!( |
| #179 | c.env.R2_ACCESS_KEY_ID && |
| #180 | c.env.R2_SECRET_ACCESS_KEY && |
| #181 | c.env.CF_ACCOUNT_ID |
| #182 | ); |
| #183 | |
| #184 | // Check which credentials are missing |
| #185 | const missing: string[] = []; |
| #186 | if (!c.env.R2_ACCESS_KEY_ID) missing.push('R2_ACCESS_KEY_ID'); |
| #187 | if (!c.env.R2_SECRET_ACCESS_KEY) missing.push('R2_SECRET_ACCESS_KEY'); |
| #188 | if (!c.env.CF_ACCOUNT_ID) missing.push('CF_ACCOUNT_ID'); |
| #189 | |
| #190 | let lastSync: string | null = null; |
| #191 | |
| #192 | // If R2 is configured, check for last sync timestamp |
| #193 | if (hasCredentials) { |
| #194 | try { |
| #195 | // Mount R2 if not already mounted |
| #196 | await mountR2Storage(sandbox, c.env); |
| #197 | |
| #198 | // Check for sync marker file |
| #199 | const proc = await sandbox.startProcess(`cat ${R2_MOUNT_PATH}/.last-sync 2>/dev/null || echo ""`); |
| #200 | await waitForProcess(proc, 5000); |
| #201 | const logs = await proc.getLogs(); |
| #202 | const timestamp = logs.stdout?.trim(); |
| #203 | if (timestamp && timestamp !== '') { |
| #204 | lastSync = timestamp; |
| #205 | } |
| #206 | } catch { |
| #207 | // Ignore errors checking sync status |
| #208 | } |
| #209 | } |
| #210 | |
| #211 | return c.json({ |
| #212 | configured: hasCredentials, |
| #213 | missing: missing.length > 0 ? missing : undefined, |
| #214 | lastSync, |
| #215 | message: hasCredentials |
| #216 | ? 'R2 storage is configured. Your data will persist across container restarts.' |
| #217 | : 'R2 storage is not configured. Paired devices and conversations will be lost when the container restarts.', |
| #218 | }); |
| #219 | }); |
| #220 | |
| #221 | // POST /api/admin/storage/sync - Trigger a manual sync to R2 |
| #222 | adminApi.post('/storage/sync', async (c) => { |
| #223 | const sandbox = c.get('sandbox'); |
| #224 | |
| #225 | const result = await syncToR2(sandbox, c.env); |
| #226 | |
| #227 | if (result.success) { |
| #228 | return c.json({ |
| #229 | success: true, |
| #230 | message: 'Sync completed successfully', |
| #231 | lastSync: result.lastSync, |
| #232 | }); |
| #233 | } else { |
| #234 | const status = result.error?.includes('not configured') ? 400 : 500; |
| #235 | return c.json({ |
| #236 | success: false, |
| #237 | error: result.error, |
| #238 | details: result.details, |
| #239 | }, status); |
| #240 | } |
| #241 | }); |
| #242 | |
| #243 | // POST /api/admin/gateway/restart - Kill the current gateway and start a new one |
| #244 | adminApi.post('/gateway/restart', async (c) => { |
| #245 | const sandbox = c.get('sandbox'); |
| #246 | |
| #247 | try { |
| #248 | // Find and kill the existing gateway process |
| #249 | const existingProcess = await findExistingMoltbotProcess(sandbox); |
| #250 | |
| #251 | if (existingProcess) { |
| #252 | console.log('Killing existing gateway process:', existingProcess.id); |
| #253 | try { |
| #254 | await existingProcess.kill(); |
| #255 | } catch (killErr) { |
| #256 | console.error('Error killing process:', killErr); |
| #257 | } |
| #258 | // Wait a moment for the process to die |
| #259 | await new Promise(r => setTimeout(r, 2000)); |
| #260 | } |
| #261 | |
| #262 | // Start a new gateway in the background |
| #263 | const bootPromise = ensureMoltbotGateway(sandbox, c.env).catch((err) => { |
| #264 | console.error('Gateway restart failed:', err); |
| #265 | }); |
| #266 | c.executionCtx.waitUntil(bootPromise); |
| #267 | |
| #268 | return c.json({ |
| #269 | success: true, |
| #270 | message: existingProcess |
| #271 | ? 'Gateway process killed, new instance starting...' |
| #272 | : 'No existing process found, starting new instance...', |
| #273 | previousProcessId: existingProcess?.id, |
| #274 | }); |
| #275 | } catch (error) { |
| #276 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| #277 | return c.json({ error: errorMessage }, 500); |
| #278 | } |
| #279 | }); |
| #280 | |
| #281 | // Mount admin API routes under /admin |
| #282 | api.route('/admin', adminApi); |
| #283 | |
| #284 | export { api }; |
| #285 |