Monorepo for Aesthetic.Computer aesthetic.computer
at main 222 lines 6.7 kB view raw
1// patch.mjs - Admin-only endpoint for spawning GitHub Copilot coding agents 2// 2026.02.01 3// 4// POST /api/patch - Spawn a GitHub Copilot coding agent with the given prompt 5// Requires authentication and admin privileges (@jeffrey only) 6// 7// Request body: 8// { 9// prompt: string, // The task/instruction for the PR agent 10// branch?: string, // Optional: target branch (defaults to main) 11// title?: string // Optional: PR title prefix 12// } 13// 14// Response: 15// { 16// success: boolean, 17// message: string, 18// data?: { 19// jobId: string, // ID for tracking the agent job 20// status: string, // "queued" | "running" | "completed" | "failed" 21// } 22// } 23 24import { authorize, handleFor, hasAdmin } from "../../backend/authorization.mjs"; 25import { respond } from "../../backend/http.mjs"; 26import { shell } from "../../backend/shell.mjs"; 27 28const dev = process.env.CONTEXT === "dev"; 29 30// GitHub Copilot Coding Agent configuration 31const GITHUB_TOKEN = process.env.GITHUB_COPILOT_TOKEN; 32const REPO_OWNER = "whistlegraph"; 33const REPO_NAME = "aesthetic-computer"; 34 35export async function handler(event, context) { 36 // Handle CORS preflight 37 if (event.httpMethod === "OPTIONS") { 38 return respond(200, {}, { 39 "Access-Control-Allow-Origin": "*", 40 "Access-Control-Allow-Methods": "POST, OPTIONS", 41 "Access-Control-Allow-Headers": "Content-Type, Authorization", 42 }); 43 } 44 45 if (event.httpMethod !== "POST") { 46 return respond(405, { success: false, message: "Method not allowed" }); 47 } 48 49 try { 50 // 1. Authenticate the user 51 const user = await authorize(event.headers); 52 53 if (!user) { 54 shell.log("🔐 Patch: No user found"); 55 return respond(401, { success: false, message: "Authentication required" }); 56 } 57 58 if (!user.email_verified) { 59 shell.log("🔐 Patch: Email not verified"); 60 return respond(401, { success: false, message: "Email verification required" }); 61 } 62 63 // 2. Check admin privileges (only @jeffrey can use this) 64 const isAdmin = await hasAdmin(user); 65 const handle = await handleFor(user.sub); 66 67 shell.log(`🔐 Patch: User @${handle}, admin: ${isAdmin}`); 68 69 if (!isAdmin) { 70 shell.log(`🚫 Patch: Unauthorized access attempt by @${handle}`); 71 return respond(403, { 72 success: false, 73 message: "Admin privileges required. Only @jeffrey can use this command." 74 }); 75 } 76 77 // 3. Parse the request body 78 let body; 79 try { 80 body = JSON.parse(event.body); 81 } catch (e) { 82 return respond(400, { success: false, message: "Invalid JSON body" }); 83 } 84 85 const { prompt, branch = "main", title } = body; 86 87 if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) { 88 return respond(400, { 89 success: false, 90 message: "A prompt/instruction is required for the PR agent" 91 }); 92 } 93 94 // 4. Check for GitHub token 95 if (!GITHUB_TOKEN) { 96 shell.error("🔴 Patch: GITHUB_COPILOT_TOKEN not configured"); 97 return respond(503, { 98 success: false, 99 message: "GitHub Copilot integration not configured. Please set GITHUB_COPILOT_TOKEN." 100 }); 101 } 102 103 // 5. Spawn the GitHub Copilot PR agent 104 shell.log(`🤖 Patch: Spawning PR agent for @${handle}`); 105 shell.log(`📝 Prompt: ${prompt.substring(0, 100)}...`); 106 107 const agentResult = await spawnCopilotAgent({ 108 prompt: prompt.trim(), 109 branch, 110 title: title || `[patch] ${prompt.substring(0, 50)}...`, 111 handle, 112 }); 113 114 if (agentResult.success) { 115 shell.log(`✅ Patch: Agent spawned successfully, job: ${agentResult.jobId}`); 116 return respond(200, { 117 success: true, 118 message: "PR agent spawned successfully", 119 data: { 120 jobId: agentResult.jobId, 121 status: agentResult.status, 122 url: agentResult.url, 123 }, 124 }); 125 } else { 126 shell.error(`🔴 Patch: Agent spawn failed: ${agentResult.error}`); 127 return respond(500, { 128 success: false, 129 message: agentResult.error || "Failed to spawn PR agent", 130 }); 131 } 132 133 } catch (error) { 134 shell.error(`🔴 Patch error: ${error.message}`); 135 return respond(500, { 136 success: false, 137 message: dev ? error.message : "Internal server error" 138 }); 139 } 140} 141 142/** 143 * Spawn a GitHub Copilot coding agent to create a PR 144 * Uses the GitHub API to trigger Copilot workspace/coding agent 145 */ 146async function spawnCopilotAgent({ prompt, branch, title, handle }) { 147 try { 148 const { Octokit } = await import("@octokit/rest"); 149 150 const octokit = new Octokit({ 151 auth: GITHUB_TOKEN, 152 }); 153 154 // Create an issue that Copilot can work on 155 // GitHub Copilot Coding Agent can be triggered by creating issues 156 // with specific labels or via the GitHub API 157 158 const issueBody = `## Task Description 159 160${prompt} 161 162--- 163*Triggered by @${handle} via aesthetic.computer/patch command* 164*Target branch: ${branch}* 165 166## Instructions for Copilot Agent 167 168Please implement this change and create a pull request. Follow existing code patterns and conventions in the repository. 169 170--- 171🤖 **This issue was auto-generated by the AC patch system** 172`; 173 174 // First, try to create the issue 175 const issueResponse = await octokit.issues.create({ 176 owner: REPO_OWNER, 177 repo: REPO_NAME, 178 title: title, 179 body: issueBody, 180 labels: ["copilot", "auto-patch"], 181 }); 182 183 const issueNumber = issueResponse.data.number; 184 const issueUrl = issueResponse.data.html_url; 185 186 shell.log(`📋 Created issue #${issueNumber}: ${issueUrl}`); 187 188 // Trigger Copilot coding agent on this issue 189 // This uses the repository dispatch event which Copilot can respond to 190 try { 191 await octokit.repos.createDispatchEvent({ 192 owner: REPO_OWNER, 193 repo: REPO_NAME, 194 event_type: "copilot-patch", 195 client_payload: { 196 issue_number: issueNumber, 197 prompt: prompt, 198 branch: branch, 199 triggered_by: handle, 200 }, 201 }); 202 shell.log(`🚀 Dispatched copilot-patch event for issue #${issueNumber}`); 203 } catch (dispatchError) { 204 // Dispatch might fail if not set up, but issue creation is still useful 205 shell.log(`⚠️ Could not dispatch event (may need workflow setup): ${dispatchError.message}`); 206 } 207 208 return { 209 success: true, 210 jobId: `issue-${issueNumber}`, 211 status: "queued", 212 url: issueUrl, 213 }; 214 215 } catch (error) { 216 shell.error(`🔴 spawnCopilotAgent error: ${error.message}`); 217 return { 218 success: false, 219 error: error.message, 220 }; 221 } 222}