/** * Zip Import Script — Imports all agent zips from system-agents/agent_zips via REST API. * * Flow: * 1. Read .zip files from system-agents/agent_zips * 2. Upload each zip to Firebase Storage using Admin SDK * 3. Call POST /api/v1/agents/import with source: 'upload' and the GCS destination * 4. Wait for package and deploy * * Usage: * npx tsx test/integration/11-zip-import.ts */ import * as fs from 'fs'; import * as path from 'path'; import * as admin from 'firebase-admin'; import { initScriptAuth, getAuthTokens, getBaseUrl, OWNER_UID } from '../../scripts/lib/auth-init'; import { isTransientNetworkError } from './lib/network-utils'; initScriptAuth(); const CONCURRENCY = 2; // Zip packaging can be heavy /** Parse --agents=name1,name2 filter from CLI args */ function parseAgentsFilter(): string[] | null { const arg = process.argv.find(a => a.startsWith('--agents=')); if (arg) { return arg.split('=')[1].split(',').map(s => s.trim()).filter(Boolean); } return null; } const AGENTS_FILTER = parseAgentsFilter(); // ─── Token Management ──────────────────────────────────────────────────────── let _cachedTokens: { idToken: string; appCheckToken: string } | null = null; let _tokenObtainedAt = 0; const TOKEN_REFRESH_INTERVAL_MS = 45 * 60 * 1000; async function getFreshTokens(): Promise<{ idToken: string; appCheckToken: string }> { const now = Date.now(); if (_cachedTokens && (now - _tokenObtainedAt) < TOKEN_REFRESH_INTERVAL_MS) { return _cachedTokens; } console.log(' 🔑 Refreshing auth tokens...'); _cachedTokens = await getAuthTokens(); _tokenObtainedAt = Date.now(); console.log(' ✅ Tokens refreshed'); return _cachedTokens; } // ─── HTTP API Import ───────────────────────────────────────────────────────── async function callImportApi( payload: Record, tokens: { idToken: string; appCheckToken: string }, baseUrl: string ): Promise<{ success: boolean; message: string; agentId?: string; completedSteps?: string[] }> { const response = await fetch(`${baseUrl}/api/v1/agents/import`, { method: 'POST', headers: { 'Authorization': `Bearer ${tokens.idToken}`, 'X-Firebase-AppCheck': tokens.appCheckToken, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok && response.status !== 207) { throw new Error(`API ${response.status}: ${data.message || data.error || JSON.stringify(data).substring(0, 200)}`); } return data; } async function getAgentStatus(agentId: string, tokens: any, baseUrl: string) { try { const response = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, { headers: { 'Authorization': `Bearer ${tokens.idToken}`, 'X-Firebase-AppCheck': tokens.appCheckToken } }); const data = await response.json(); if (!response.ok) throw new Error(`API error: ${response.status} - ${data.message || JSON.stringify(data)}`); return data.agent; } catch (error: any) { if (isTransientNetworkError(error)) { console.warn(`\n ⚠️ [${agentId}] Network dropped locally natively (${error.code || error.message}) - Auto-retrying on next polling loop...`); return { agent: { deploymentStatus: 'deploying' } }; } throw error; } } async function triggerAgentDeployment(agentId: string, tokens: any, baseUrl: string) { const response = await fetch(`${baseUrl}/api/v1/agents/${agentId}/deploy`, { method: 'POST', headers: { 'Authorization': `Bearer ${tokens.idToken}`, 'X-Firebase-AppCheck': tokens.appCheckToken, 'Content-Type': 'application/json', }, }); const data = await response.json(); if (!response.ok) throw new Error(`Deploy ${agentId} failed: ${response.status}`); return data; } async function pollUntilStatus( agentId: string, tokens: any, baseUrl: string, targetStatuses: string[], timeoutMs = 30 * 60 * 1000, intervalMs = 30_000 ): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { const freshTokens = await getFreshTokens(); const agent = await getAgentStatus(agentId, freshTokens, baseUrl); if (targetStatuses.includes(agent.deploymentStatus)) return agent.deploymentStatus; process.stdout.write('.'); await new Promise(r => setTimeout(r, intervalMs)); } return 'timeout'; } // ─── Process Single Zip ────────────────────────────────────────────────────── async function processZip( zipPath: string, tokens: { idToken: string; appCheckToken: string }, baseUrl: string ): Promise { const filename = path.basename(zipPath); const agentName = filename.replace(/\.zip$/, ''); const prefix = `[${agentName}]`; console.log(`\n${'═'.repeat(70)}`); console.log(`📦 ${filename}`); console.log(`${'─'.repeat(70)}`); try { const bucketName = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET; if (!bucketName) throw new Error("NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET is not set"); const bucket = admin.storage().bucket(bucketName); // Upload zip to GCS console.log(`${prefix} ⏳ Uploading to GCS...`); const gcsDest = `agent_uploads/${OWNER_UID}/batch_upload_${Date.now()}_${filename}`; await bucket.upload(zipPath, { destination: gcsDest, contentType: 'application/zip', metadata: { metadata: { source: 'batch_import' } } }); console.log(`${prefix} ✅ Uploaded to ${gcsDest}`); // Call Import API console.log(`${prefix} ⏳ Calling Import API...`); const payload = { source: 'upload', name: agentName, description: `Imported from ${filename}`, sourceUrl: gcsDest, // Expected by import API for upload mode visibility: 'private', }; const freshTokens = await getFreshTokens(); const result = await callImportApi(payload, freshTokens, baseUrl); console.log(`${prefix} ${result.success ? '✅' : '❌'} API result: ${result.message}`); return { name: agentName, success: result.success, agentId: result.agentId, completedSteps: result.completedSteps || [], error: result.success ? undefined : result.message, }; } catch (err: any) { console.error(`${prefix} ❌ Error: ${err.message}`); return { name: agentName, success: false, error: err.message }; } } // ─── Main ──────────────────────────────────────────────────────────────────── async function main() { const baseUrl = getBaseUrl(); console.log(`\n🔬 Zip Import via REST API`); console.log(` Server: ${baseUrl}`); console.log(` Owner: ${OWNER_UID}\n`); // Verify auth and server let tokens; try { tokens = await getAuthTokens(); console.log(`✅ Authenticated as ${OWNER_UID}`); const healthCheck = await fetch(baseUrl); if (!healthCheck.ok) throw new Error(`Server returned ${healthCheck.status}`); console.log(`✅ Server reachable at ${baseUrl}\n`); } catch (err: any) { console.error(`❌ Setup failed: ${err.message}`); process.exit(1); } // Find zips const zipsDir = path.join(process.cwd(), 'system-agents', 'agent_zips'); if (!fs.existsSync(zipsDir)) { console.log(`⚠️ Directory not found: ${zipsDir}`); process.exit(0); } let files = fs.readdirSync(zipsDir).filter(f => f.endsWith('.zip')); if (AGENTS_FILTER) { files = files.filter(f => AGENTS_FILTER.some(agent => f.includes(agent))); console.log(`\n🔍 Applied --agents filter: ${AGENTS_FILTER.join(', ')}`); } if (files.length === 0) { console.log("No zip files found to process."); process.exit(0); } const zipPaths = files.map(f => path.join(zipsDir, f)); // Phase 1: Import const results: any[] = []; for (const zip of zipPaths) { results.push(await processZip(zip, tokens, baseUrl)); } // Phase 2: Wait for packaging const importedAgents = results.filter(r => r.success && r.agentId); if (importedAgents.length === 0) process.exit(1); console.log(`\n🔄 Waiting for packaging...`); const packagingResults: any[] = []; for (const agent of importedAgents) { process.stdout.write(` ⏳ [${agent.name}] Packaging`); const status = await pollUntilStatus(agent.agentId, tokens, baseUrl, ['packaged', 'schema_review', 'error', 'failed']); console.log(` → ${status}`); packagingResults.push({ ...agent, status }); } // Phase 3: Deploy const deployable = packagingResults.filter(r => ['packaged', 'schema_review'].includes(r.status)); if (deployable.length === 0) process.exit(1); console.log(`\n🚀 Deploying...`); for (const agent of deployable) { try { console.log(` ⏳ [${agent.name}] Deploying...`); await triggerAgentDeployment(agent.agentId, tokens, baseUrl); const status = await pollUntilStatus(agent.agentId, tokens, baseUrl, ['deployed', 'error', 'failed'], 10 * 60000, 15000); console.log(` → ${status}`); } catch (err: any) { console.error(` ❌ [${agent.name}] Deploy error: ${err.message}`); } } } main().catch(err => { console.error('Fatal:', err); process.exit(1); });