deepdebug-local-agent 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,623 @@
1
+ # CLAUDE CODE — AGENT INFINITE LOOP
2
+ # Terminal 1: cd ~/Documents/Projects/deepdebug-local-agent
3
+ # claude --dangerously-skip-permissions "$(cat CLAUDE_CODE_AGENT_INFINITE.md)"
4
+
5
+ ## LEI FUNDAMENTAL
6
+ NUNCA terminar com erro. NUNCA passar ao próximo item sem o actual estar verde.
7
+ O único critério de paragem: TODOS OS ITEMS VERDES.
8
+
9
+ ## SETUP
10
+
11
+ ```bash
12
+ AGENT="https://deepdebug-local-agent-qa-575662086506.us-central1.run.app"
13
+ GATEWAY="https://deepdebug-gateway-qa-575662086506.us-central1.run.app"
14
+ GCP_CLIENT="gen-lang-client-0961289539"
15
+ SA_EMAIL="deepdebug-agent@${GCP_CLIENT}.iam.gserviceaccount.com"
16
+ PG="PGPASSWORD='DeepDebugQA@2026' psql -h 35.224.155.29 -U deepdebug -d deepdebug"
17
+ WS_ID="b040f1ae-5f3c-4612-9ce1-0b18a5aea303"
18
+
19
+ log() { echo "[$(date '+%H:%M:%S')] $1"; }
20
+ ok() { echo "[OK] $1"; }
21
+ fail() { echo "[FAIL] $1"; }
22
+
23
+ deploy_agent() {
24
+ cd ~/Documents/Projects/deepdebug-local-agent
25
+ node --check src/server.js 2>&1 | head -5
26
+ if [ $? -ne 0 ]; then fail "Syntax error"; return 1; fi
27
+ git add -A && git commit -m "auto-heal: $1" --allow-empty -q
28
+ gcloud builds submit --config cloudbuild-agent-qa.yaml --project insptechai 2>&1 | tail -3
29
+ log "Waiting 5 min..."; sleep 300
30
+ ok "Agent deployed"
31
+ }
32
+
33
+ read_logs() {
34
+ gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=deepdebug-local-agent-qa" \
35
+ --project=insptechai --freshness=5m --format="value(textPayload)" 2>/dev/null \
36
+ | grep -v "^$" | head -20
37
+ }
38
+ ```
39
+
40
+ ---
41
+
42
+ ## ITEM A1 — /workspace/setup ENDPOINT
43
+
44
+ ### Critério: endpoint existe, clone funciona, detect stack funciona, SSE retorna event:complete
45
+
46
+ ```bash
47
+ check_setup() {
48
+ local R=$(curl -s -X POST "$AGENT/workspace/setup" \
49
+ -H "Content-Type: application/json" \
50
+ -d '{"workspaceId":"test-check-001","repoUrl":"https://github.com/williambella/simple-api.git","gitToken":"","provider":"deepdebug_cloud"}' \
51
+ --max-time 30 2>/dev/null)
52
+ echo "$R" | grep -q "progress\|complete\|Starting setup"
53
+ }
54
+
55
+ fix_setup() {
56
+ cd ~/Documents/Projects/deepdebug-local-agent
57
+ # Verificar se endpoint existe
58
+ grep -n "workspace/setup\|cloneRepository\|detectProjectStack\|generateDockerfile" src/server.js | head -10
59
+
60
+ # Se não existir, o endpoint foi adicionado em sessões anteriores mas pode ter sido sobrescrito
61
+ # Ver o fim do ficheiro
62
+ tail -100 src/server.js
63
+
64
+ # Verificar se as funções auxiliares existem
65
+ grep -n "async function cloneRepository\|async function detectProjectStack\|async function generateDockerfile" src/server.js
66
+
67
+ # Se não existirem, adicionar o endpoint completo com todas as funções
68
+ # baseado no que foi implementado nas sessões anteriores
69
+ }
70
+ ```
71
+
72
+ ### Implementação completa se não existir:
73
+
74
+ ```bash
75
+ add_setup_endpoint() {
76
+ cd ~/Documents/Projects/deepdebug-local-agent
77
+
78
+ # Verificar se já existe
79
+ if grep -q "workspace/setup" src/server.js; then
80
+ log "/workspace/setup already exists"
81
+ return 0
82
+ fi
83
+
84
+ log "Adding /workspace/setup endpoint..."
85
+
86
+ # Adicionar antes do app.listen (última linha do ficheiro)
87
+ LISTEN_LINE=$(grep -n "app.listen\|server.listen" src/server.js | tail -1 | cut -d: -f1)
88
+ log "app.listen at line: $LISTEN_LINE"
89
+
90
+ # Criar ficheiro com o código a adicionar
91
+ cat > /tmp/setup_endpoint.js << 'JSEOF'
92
+
93
+ // ============================================================
94
+ // WORKSPACE BASE
95
+ // ============================================================
96
+ const WORKSPACE_BASE = process.env.WORKSPACE_BASE || process.env.WORKSPACES_MOUNT || '/mnt/workspaces';
97
+
98
+ // ============================================================
99
+ // POST /workspace/setup — AI Cloud Engineering Agent
100
+ // ============================================================
101
+ app.post('/workspace/setup', async (req, res) => {
102
+ const { workspaceId, repoUrl, gitToken, provider = 'deepdebug_cloud',
103
+ cloudProjectId, cloudToken, serviceAccountJson } = req.body || {};
104
+
105
+ if (!workspaceId || !repoUrl) {
106
+ return res.status(400).json({ error: 'workspaceId and repoUrl required' });
107
+ }
108
+
109
+ res.setHeader('Content-Type', 'text/event-stream');
110
+ res.setHeader('Cache-Control', 'no-cache');
111
+ res.setHeader('Connection', 'keep-alive');
112
+ res.setHeader('X-Accel-Buffering', 'no');
113
+
114
+ const emit = (event, msg) => {
115
+ const data = typeof msg === 'object' ? JSON.stringify(msg) : JSON.stringify({ message: msg });
116
+ res.write(`event:${event}\ndata:${data}\n\n`);
117
+ };
118
+
119
+ try {
120
+ const wsPath = path.join(WORKSPACE_BASE, workspaceId);
121
+ emit('progress', `Starting setup for workspace: ${workspaceId}`);
122
+ emit('progress', `Workspace path: ${wsPath}`);
123
+
124
+ // STEP 1: Clone
125
+ emit('progress', 'Cloning repository...');
126
+ const cloneResult = await cloneRepo(wsPath, repoUrl, gitToken);
127
+ if (!cloneResult.ok) {
128
+ emit('error', { error: `Clone failed: ${cloneResult.error}` });
129
+ return res.end();
130
+ }
131
+ emit('progress', `Repository cloned on branch: ${cloneResult.branch}`);
132
+
133
+ // STEP 2: Detect stack
134
+ emit('progress', 'AI analysing project structure...');
135
+ const stack = await detectStack(wsPath);
136
+ emit('progress', `Stack: ${stack.language} / ${stack.buildTool} / ${stack.framework}`);
137
+
138
+ // STEP 3: Generate Dockerfile
139
+ const dfResult = await ensureDockerfile(wsPath, stack);
140
+ emit('progress', dfResult.existed ? 'Dockerfile already exists' : `Dockerfile generated for ${stack.language}`);
141
+
142
+ // STEP 4: Register workspace
143
+ try {
144
+ await wsManager.open(workspaceId, wsPath);
145
+ } catch (e) { /* ignore */ }
146
+
147
+ emit('progress', `Provider: ${provider}. Workspace ready.`);
148
+ emit('progress', 'AI Cloud Engineering setup complete!');
149
+ emit('complete', {
150
+ success: true, workspaceId, wsPath, provider,
151
+ branch: cloneResult.branch,
152
+ stack: { language: stack.language, buildTool: stack.buildTool }
153
+ });
154
+ res.end();
155
+ } catch (err) {
156
+ console.error('[Setup] Error:', err.message);
157
+ emit('error', { error: err.message });
158
+ res.end();
159
+ }
160
+ });
161
+
162
+ // ============================================================
163
+ // POST /workspace/build-push — Build + push via Cloud Build
164
+ // ============================================================
165
+ app.post('/workspace/build-push', async (req, res) => {
166
+ const { workspaceId, imageName, gcpProject, saKeyJson } = req.body || {};
167
+ if (!workspaceId || !imageName || !gcpProject || !saKeyJson) {
168
+ return res.status(400).json({ error: 'workspaceId, imageName, gcpProject, saKeyJson required' });
169
+ }
170
+
171
+ res.setHeader('Content-Type', 'text/event-stream');
172
+ res.setHeader('Cache-Control', 'no-cache');
173
+ res.setHeader('Connection', 'keep-alive');
174
+ res.setHeader('X-Accel-Buffering', 'no');
175
+
176
+ const emit = (event, msg) => {
177
+ const data = typeof msg === 'object' ? JSON.stringify(msg) : JSON.stringify({ message: msg });
178
+ res.write(`event:${event}\ndata:${data}\n\n`);
179
+ };
180
+
181
+ try {
182
+ const wsPath = path.join(WORKSPACE_BASE, workspaceId);
183
+ if (!fs.existsSync(wsPath)) {
184
+ emit('error', { error: `Workspace not found: ${wsPath}` }); return res.end();
185
+ }
186
+ if (!fs.existsSync(path.join(wsPath, 'Dockerfile'))) {
187
+ emit('error', { error: 'No Dockerfile in workspace' }); return res.end();
188
+ }
189
+
190
+ // Step 1: Get SA access token
191
+ emit('progress', 'Authenticating with GCP...');
192
+ let accessToken;
193
+ try {
194
+ const { createSign } = await import('crypto');
195
+ const sa = JSON.parse(saKeyJson);
196
+ const now = Math.floor(Date.now() / 1000);
197
+ const h = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
198
+ const c = Buffer.from(JSON.stringify({
199
+ iss: sa.client_email,
200
+ scope: 'https://www.googleapis.com/auth/cloud-platform',
201
+ aud: 'https://oauth2.googleapis.com/token',
202
+ iat: now, exp: now + 3600
203
+ })).toString('base64url');
204
+ const sign = createSign('SHA256');
205
+ sign.update(`${h}.${c}`);
206
+ const jwt = `${h}.${c}.${sign.sign(sa.private_key, 'base64url')}`;
207
+ const tokenResp = await fetch('https://oauth2.googleapis.com/token', {
208
+ method: 'POST',
209
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
210
+ body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`
211
+ });
212
+ const tokenData = await tokenResp.json();
213
+ accessToken = tokenData.access_token;
214
+ if (!accessToken) throw new Error('No access_token: ' + JSON.stringify(tokenData));
215
+ emit('progress', 'GCP authentication successful');
216
+ } catch (e) {
217
+ emit('error', { error: 'GCP auth failed: ' + e.message }); return res.end();
218
+ }
219
+
220
+ // Step 2: Compress workspace
221
+ emit('progress', 'Compressing source code...');
222
+ const { execSync } = await import('child_process');
223
+ const tarPath = `/tmp/source-${workspaceId}-${Date.now()}.tar.gz`;
224
+ try {
225
+ execSync(`tar -czf "${tarPath}" --exclude=".git" --exclude="node_modules" --exclude="target" --exclude="*.class" --exclude=".gradle" -C "${wsPath}" . 2>&1`, { stdio: 'pipe', timeout: 120000 });
226
+ const sizeKB = Math.round(fs.statSync(tarPath).size / 1024);
227
+ emit('progress', `Source compressed: ${sizeKB}KB`);
228
+ } catch (e) {
229
+ emit('error', { error: 'Compression failed: ' + e.message.substring(0, 200) }); return res.end();
230
+ }
231
+
232
+ // Step 3: Upload to GCS
233
+ const bucketName = `${gcpProject}_cloudbuild`;
234
+ const objectName = `source/${workspaceId}-${Date.now()}.tar.gz`;
235
+ emit('progress', `Uploading source to GCS bucket: ${bucketName}...`);
236
+ try {
237
+ await fetch(`https://storage.googleapis.com/storage/v1/b?project=${gcpProject}`, {
238
+ method: 'POST',
239
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({ name: bucketName, location: 'US' })
241
+ });
242
+ const fileBuffer = fs.readFileSync(tarPath);
243
+ const uploadResp = await fetch(
244
+ `https://storage.googleapis.com/upload/storage/v1/b/${encodeURIComponent(bucketName)}/o?uploadType=media&name=${encodeURIComponent(objectName)}`,
245
+ { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/gzip' }, body: fileBuffer }
246
+ );
247
+ if (!uploadResp.ok) {
248
+ const errText = await uploadResp.text();
249
+ throw new Error(`GCS upload ${uploadResp.status}: ${errText.substring(0, 200)}`);
250
+ }
251
+ emit('progress', 'Source uploaded to GCS');
252
+ } catch (e) {
253
+ emit('error', { error: 'GCS upload failed: ' + e.message }); return res.end();
254
+ } finally {
255
+ try { fs.unlinkSync(tarPath); } catch {}
256
+ }
257
+
258
+ // Step 4: Submit Cloud Build
259
+ emit('progress', 'Submitting Cloud Build...');
260
+ let buildId = 'unknown';
261
+ try {
262
+ const buildReq = {
263
+ source: { storageSource: { bucket: bucketName, object: objectName } },
264
+ steps: [
265
+ { name: 'gcr.io/cloud-builders/docker', args: ['build', '-t', imageName, '.'], id: 'build' },
266
+ { name: 'gcr.io/cloud-builders/docker', args: ['push', imageName], id: 'push', waitFor: ['build'] }
267
+ ],
268
+ images: [imageName], timeout: '1200s'
269
+ };
270
+ const buildResp = await fetch(`https://cloudbuild.googleapis.com/v1/projects/${gcpProject}/builds`, {
271
+ method: 'POST',
272
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
273
+ body: JSON.stringify(buildReq)
274
+ });
275
+ if (!buildResp.ok) {
276
+ const errText = await buildResp.text();
277
+ throw new Error(`Cloud Build ${buildResp.status}: ${errText.substring(0, 300)}`);
278
+ }
279
+ const buildData = await buildResp.json();
280
+ buildId = buildData.metadata?.build?.id || buildData.name?.split('/').pop() || 'unknown';
281
+ emit('progress', `Cloud Build started: ${buildId}`);
282
+ } catch (e) {
283
+ emit('error', { error: 'Cloud Build submit failed: ' + e.message }); return res.end();
284
+ }
285
+
286
+ // Step 5: Poll status
287
+ emit('progress', 'Building Docker image (3-5 minutes)...');
288
+ let status = 'WORKING';
289
+ let attempts = 0;
290
+ while (!['SUCCESS', 'FAILURE', 'CANCELLED', 'TIMEOUT'].includes(status) && attempts < 40) {
291
+ await new Promise(r => setTimeout(r, 15000));
292
+ attempts++;
293
+ try {
294
+ const sResp = await fetch(`https://cloudbuild.googleapis.com/v1/projects/${gcpProject}/builds/${buildId}`, { headers: { Authorization: `Bearer ${accessToken}` } });
295
+ const sData = await sResp.json();
296
+ status = sData.status || 'WORKING';
297
+ emit('progress', `Build status: ${status} (${attempts}/40)`);
298
+ } catch (e) { emit('progress', `Status check: ${e.message}`); }
299
+ }
300
+
301
+ if (status === 'SUCCESS') {
302
+ emit('progress', 'Docker image built and pushed successfully!');
303
+ emit('complete', { success: true, imageName, buildId });
304
+ } else {
305
+ emit('error', { error: `Cloud Build ${status}. Check GCP Console.` });
306
+ }
307
+ res.end();
308
+ } catch (err) {
309
+ console.error('[build-push] Fatal:', err.message);
310
+ emit('error', { error: err.message });
311
+ res.end();
312
+ }
313
+ });
314
+
315
+ // ============================================================
316
+ // POST /workspace/git/prepare — Clone/update repo
317
+ // ============================================================
318
+ app.post('/workspace/git/prepare', async (req, res) => {
319
+ const { workspaceId, repoUrl, gitToken, branch } = req.body || {};
320
+ if (!workspaceId || !repoUrl) return res.status(400).json({ error: 'workspaceId and repoUrl required' });
321
+ const wsPath = path.join(WORKSPACE_BASE, workspaceId);
322
+ try {
323
+ const result = await cloneRepo(wsPath, repoUrl, gitToken, branch);
324
+ res.json({ ok: true, path: wsPath, ...result });
325
+ } catch (e) {
326
+ res.status(500).json({ ok: false, error: e.message });
327
+ }
328
+ });
329
+
330
+ // ============================================================
331
+ // POST /workspace/write-file — Write file to workspace
332
+ // ============================================================
333
+ app.post('/workspace/write-file', async (req, res) => {
334
+ const { workspaceId, path: filePath, content, overwrite = true } = req.body || {};
335
+ if (!filePath || content === undefined) return res.status(400).json({ error: 'path and content required' });
336
+ try {
337
+ const base = workspaceId ? path.join(WORKSPACE_BASE, workspaceId) : WORKSPACE_ROOT;
338
+ const abs = path.join(base, filePath);
339
+ if (!overwrite && fs.existsSync(abs)) return res.json({ ok: true, path: abs, skipped: true });
340
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
341
+ fs.writeFileSync(abs, content, 'utf8');
342
+ res.json({ ok: true, path: abs });
343
+ } catch (e) {
344
+ res.status(500).json({ ok: false, error: e.message });
345
+ }
346
+ });
347
+
348
+ // ============================================================
349
+ // HELPER FUNCTIONS
350
+ // ============================================================
351
+
352
+ async function cloneRepo(wsPath, repoUrl, gitToken, branch = null) {
353
+ const { execSync } = await import('child_process');
354
+ let authUrl = repoUrl;
355
+ if (gitToken && gitToken.length > 5) {
356
+ try {
357
+ const urlObj = new URL(repoUrl);
358
+ urlObj.username = 'x-access-token';
359
+ urlObj.password = gitToken;
360
+ authUrl = urlObj.toString();
361
+ console.log(`[Clone] Using authenticated clone for ${urlObj.hostname}`);
362
+ } catch {}
363
+ }
364
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo', HOME: '/tmp' };
365
+ const opts = { stdio: 'pipe', timeout: 300000, env, shell: true };
366
+
367
+ if (fs.existsSync(path.join(wsPath, '.git'))) {
368
+ console.log(`[Clone] Workspace exists, updating...`);
369
+ try {
370
+ execSync('git fetch origin 2>&1', { ...opts, cwd: wsPath });
371
+ const branch = execSync('git branch --show-current 2>&1', { ...opts, cwd: wsPath }).toString().trim();
372
+ execSync(`git reset --hard origin/${branch} 2>&1`, { ...opts, cwd: wsPath });
373
+ return { ok: true, branch, updated: true };
374
+ } catch (e) {
375
+ console.warn('[Clone] Update failed, re-cloning');
376
+ fs.rmSync(wsPath, { recursive: true, force: true });
377
+ }
378
+ }
379
+
380
+ fs.mkdirSync(wsPath, { recursive: true });
381
+ const branches = branch ? [branch] : ['main', 'master', 'develop'];
382
+ for (const b of branches) {
383
+ try {
384
+ console.log(`[Clone] Trying branch: ${b}...`);
385
+ execSync(`git clone --depth=1 --branch "${b}" "${authUrl}" "${wsPath}" 2>&1`, opts);
386
+ console.log(`[Clone] Cloned on branch: ${b}`);
387
+ return { ok: true, branch: b, cloned: true };
388
+ } catch (e) {
389
+ const err = (e.stdout || e.stderr || e.message || '').toString();
390
+ if (err.includes('404') || err.includes('not found') || err.includes('Repository not found')) break;
391
+ }
392
+ }
393
+ try {
394
+ execSync(`git clone --depth=1 "${authUrl}" "${wsPath}" 2>&1`, opts);
395
+ const b = execSync('git branch --show-current 2>&1', { ...opts, cwd: wsPath }).toString().trim();
396
+ return { ok: true, branch: b, cloned: true };
397
+ } catch (e) {
398
+ return { ok: false, error: e.message.substring(0, 200) };
399
+ }
400
+ }
401
+
402
+ async function detectStack(wsPath) {
403
+ const check = f => fs.existsSync(path.join(wsPath, f));
404
+ const read = f => { try { return fs.readFileSync(path.join(wsPath, f), 'utf8'); } catch { return ''; } };
405
+ let language = 'unknown', buildTool = 'unknown', framework = 'unknown', port = 8080;
406
+ if (check('pom.xml')) {
407
+ language = 'java'; buildTool = 'maven';
408
+ const pom = read('pom.xml');
409
+ if (pom.includes('spring-boot')) framework = 'spring-boot';
410
+ else if (pom.includes('quarkus')) framework = 'quarkus';
411
+ } else if (check('build.gradle') || check('build.gradle.kts')) {
412
+ language = 'java'; buildTool = 'gradle';
413
+ } else if (check('package.json')) {
414
+ language = 'javascript'; buildTool = 'npm';
415
+ const pkg = JSON.parse(read('package.json') || '{}');
416
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
417
+ if (deps.next) framework = 'nextjs';
418
+ else if (deps.express) framework = 'express';
419
+ else if (deps.fastify) framework = 'fastify';
420
+ } else if (check('requirements.txt') || check('pyproject.toml')) {
421
+ language = 'python'; buildTool = 'pip';
422
+ const reqs = read('requirements.txt');
423
+ if (reqs.includes('fastapi')) framework = 'fastapi';
424
+ else if (reqs.includes('django')) framework = 'django';
425
+ else if (reqs.includes('flask')) framework = 'flask';
426
+ } else if (check('go.mod')) { language = 'go'; buildTool = 'go'; }
427
+ else if (check('Cargo.toml')) { language = 'rust'; buildTool = 'cargo'; }
428
+ else if (check('composer.json')) { language = 'php'; buildTool = 'composer'; }
429
+ return { language, buildTool, framework, port, hasDockerfile: check('Dockerfile') };
430
+ }
431
+
432
+ async function ensureDockerfile(wsPath, stack) {
433
+ const dfPath = path.join(wsPath, 'Dockerfile');
434
+ if (fs.existsSync(dfPath)) return { ok: true, existed: true };
435
+ const { language, buildTool, port = 8080 } = stack;
436
+ let content = '';
437
+ if (language === 'java' && buildTool === 'maven') {
438
+ content = `FROM maven:3.9-eclipse-temurin-17 AS build\nWORKDIR /app\nCOPY pom.xml .\nRUN mvn dependency:go-offline -B\nCOPY src ./src\nRUN mvn clean package -DskipTests -B\n\nFROM eclipse-temurin:17-jre-jammy\nWORKDIR /app\nCOPY --from=build /app/target/*.jar app.jar\nEXPOSE ${port}\nENTRYPOINT ["java", "-jar", "app.jar"]\n`;
439
+ } else if (language === 'java' && buildTool === 'gradle') {
440
+ content = `FROM gradle:8-jdk17 AS build\nWORKDIR /app\nCOPY . .\nRUN gradle build -x test --no-daemon\n\nFROM eclipse-temurin:17-jre-jammy\nWORKDIR /app\nCOPY --from=build /app/build/libs/*.jar app.jar\nEXPOSE ${port}\nENTRYPOINT ["java", "-jar", "app.jar"]\n`;
441
+ } else if (language === 'javascript' || language === 'typescript') {
442
+ content = `FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --only=production\nCOPY . .\nEXPOSE ${port}\nENV PORT=${port}\nENV NODE_ENV=production\nCMD ["node", "src/index.js"]\n`;
443
+ } else if (language === 'python') {
444
+ content = `FROM python:3.11-slim\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\nEXPOSE ${port}\nCMD ["python", "app.py"]\n`;
445
+ } else if (language === 'go') {
446
+ content = `FROM golang:1.21-alpine AS build\nWORKDIR /app\nCOPY . .\nRUN go build -o main .\n\nFROM alpine:3.18\nWORKDIR /app\nCOPY --from=build /app/main .\nEXPOSE ${port}\nCMD ["./main"]\n`;
447
+ } else {
448
+ content = `FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .\nEXPOSE ${port}\nCMD ["./start.sh"]\n`;
449
+ }
450
+ fs.writeFileSync(dfPath, content, 'utf8');
451
+ return { ok: true, existed: false };
452
+ }
453
+ JSEOF
454
+
455
+ # Inserir antes do app.listen
456
+ head -n $((LISTEN_LINE - 1)) src/server.js > /tmp/server_new.js
457
+ cat /tmp/setup_endpoint.js >> /tmp/server_new.js
458
+ tail -n +$LISTEN_LINE src/server.js >> /tmp/server_new.js
459
+ mv /tmp/server_new.js src/server.js
460
+
461
+ log "Endpoints added to server.js"
462
+ }
463
+ ```
464
+
465
+ ---
466
+
467
+ ## LOOP PRINCIPAL DO AGENT
468
+
469
+ ```bash
470
+ log "======================================"
471
+ log "AGENT INFINITE LOOP — START"
472
+ log "======================================"
473
+
474
+ cd ~/Documents/Projects/deepdebug-local-agent
475
+
476
+ # Verificar e adicionar endpoints se necessário
477
+ if ! grep -q "workspace/setup" src/server.js; then
478
+ log "Endpoints missing — adding..."
479
+ add_setup_endpoint
480
+ deploy_agent "add setup+build-push+clone endpoints"
481
+ fi
482
+
483
+ AGENT_DONE=false
484
+ while [ "$AGENT_DONE" = "false" ]; do
485
+ ALL_GREEN=true
486
+
487
+ # ─── CHECK A1: /workspace/setup ───
488
+ log "CHECK A1: /workspace/setup"
489
+ SETUP_R=$(curl -s -X POST "$AGENT/workspace/setup" \
490
+ -H "Content-Type: application/json" \
491
+ -d '{"workspaceId":"agent-check-001","repoUrl":"https://github.com/williambella/simple-api.git","gitToken":"","provider":"deepdebug_cloud"}' \
492
+ --max-time 30 2>/dev/null)
493
+ if echo "$SETUP_R" | grep -q "Starting setup\|progress\|complete"; then
494
+ ok "A1: /workspace/setup OK"
495
+ else
496
+ fail "A1: /workspace/setup FAIL — $SETUP_R"
497
+ ALL_GREEN=false
498
+ # Fix: re-adicionar endpoints
499
+ add_setup_endpoint
500
+ deploy_agent "fix setup endpoint"
501
+ fi
502
+
503
+ # ─── CHECK A2: /workspace/build-push ───
504
+ log "CHECK A2: /workspace/build-push"
505
+ BP_R=$(curl -s -X POST "$AGENT/workspace/build-push" \
506
+ -H "Content-Type: application/json" \
507
+ -d '{"workspaceId":"x","imageName":"x","gcpProject":"x","saKeyJson":"{}"}' \
508
+ --max-time 10 2>/dev/null)
509
+ if echo "$BP_R" | grep -q "Authenticating\|progress\|error\|auth"; then
510
+ ok "A2: /workspace/build-push exists"
511
+ else
512
+ fail "A2: /workspace/build-push FAIL"
513
+ ALL_GREEN=false
514
+ add_setup_endpoint
515
+ deploy_agent "fix build-push endpoint"
516
+ fi
517
+
518
+ # ─── CHECK A3: Clone com token real ───
519
+ log "CHECK A3: Clone com token real"
520
+ GIT_TOKEN=$(PGPASSWORD='DeepDebugQA@2026' psql -h 35.224.155.29 -U deepdebug -d deepdebug -t -c \
521
+ "SELECT t.access_token FROM oauth_tokens t JOIN tenants tn ON tn.id::text=t.tenant_id WHERE t.provider='GITHUB' LIMIT 1;" \
522
+ | tr -d ' \n')
523
+
524
+ if [ -n "$GIT_TOKEN" ]; then
525
+ CLONE_R=$(curl -s -N -X POST "$AGENT/workspace/setup" \
526
+ -H "Content-Type: application/json" \
527
+ -d "{\"workspaceId\":\"agent-check-clone-$(date +%s)\",\"repoUrl\":\"https://github.com/williambella/simple-api.git\",\"gitToken\":\"$GIT_TOKEN\",\"provider\":\"deepdebug_cloud\"}" \
528
+ --max-time 60 2>/dev/null)
529
+ if echo "$CLONE_R" | grep -q "Stack:\|Cloned on branch\|complete"; then
530
+ ok "A3: Clone with token OK"
531
+ else
532
+ fail "A3: Clone FAIL — checking logs"
533
+ read_logs
534
+ ALL_GREEN=false
535
+ fi
536
+ fi
537
+
538
+ # ─── CHECK A4: /workspace/build-push com SA real ───
539
+ log "CHECK A4: build-push com SA key real"
540
+ SA_KEY_ENC=$(PGPASSWORD='DeepDebugQA@2026' psql -h 35.224.155.29 -U deepdebug -d deepdebug -t -c \
541
+ "SELECT service_account_key FROM oauth_tokens WHERE tenant_id='63922779-7321-4e79-b0f0-63890cd0d98e' AND provider='GCP';" \
542
+ | tr -d ' \n')
543
+ ENC_KEY=$(gcloud secrets versions access latest --secret=encryption-key-qa --project=insptechai 2>/dev/null)
544
+
545
+ # Testar JWT geração localmente primeiro
546
+ JWT_TEST=$(node -e "
547
+ const crypto = require('crypto');
548
+ try {
549
+ const enc = '$SA_KEY_ENC';
550
+ const key = '$ENC_KEY';
551
+ const kb = Buffer.alloc(32); Buffer.from(key).copy(kb);
552
+ const dec = crypto.createDecipheriv('aes-256-ecb', kb, null);
553
+ dec.setAutoPadding(true);
554
+ const sa = JSON.parse(Buffer.concat([dec.update(Buffer.from(enc,'base64')),dec.final()]).toString());
555
+ console.log('SA_OK:'+sa.client_email+':'+sa.project_id);
556
+ } catch(e) { console.log('SA_FAIL:'+e.message); }
557
+ " 2>/dev/null)
558
+
559
+ if echo "$JWT_TEST" | grep -q "SA_OK"; then
560
+ log "SA key decrypt OK: $JWT_TEST"
561
+ ok "A4: SA key decrypts correctly"
562
+ else
563
+ fail "A4: SA key decrypt FAIL: $JWT_TEST"
564
+ ALL_GREEN=false
565
+ log "Encryption key issue — checking..."
566
+ fi
567
+
568
+ # ─── CHECK A5: Cloud Build no projecto cliente ───
569
+ log "CHECK A5: Cloud Build acessível"
570
+ SA_TOKEN=$(node -e "
571
+ const crypto = require('crypto');
572
+ const enc = '$SA_KEY_ENC';
573
+ const key = '$ENC_KEY';
574
+ const kb = Buffer.alloc(32); Buffer.from(key).copy(kb);
575
+ const dec = crypto.createDecipheriv('aes-256-ecb', kb, null);
576
+ dec.setAutoPadding(true);
577
+ const sa = JSON.parse(Buffer.concat([dec.update(Buffer.from(enc,'base64')),dec.final()]).toString());
578
+ const now = Math.floor(Date.now()/1000);
579
+ const h = Buffer.from(JSON.stringify({alg:'RS256',typ:'JWT'})).toString('base64url');
580
+ const c = Buffer.from(JSON.stringify({iss:sa.client_email,scope:'https://www.googleapis.com/auth/cloud-platform',aud:'https://oauth2.googleapis.com/token',iat:now,exp:now+3600})).toString('base64url');
581
+ const s = crypto.createSign('SHA256'); s.update(h+'.'+c);
582
+ const jwt = h+'.'+c+'.'+s.sign(sa.private_key,'base64url');
583
+ fetch('https://oauth2.googleapis.com/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion='+jwt})
584
+ .then(r=>r.json()).then(d=>{
585
+ if(d.access_token) { process.stdout.write(d.access_token); }
586
+ else { console.error('FAIL:'+JSON.stringify(d)); process.exit(1); }
587
+ }).catch(e=>{console.error(e.message);process.exit(1);});
588
+ " 2>/dev/null)
589
+
590
+ if [ -n "$SA_TOKEN" ] && [ ${#SA_TOKEN} -gt 20 ]; then
591
+ ok "A5: SA token obtained (${#SA_TOKEN} chars)"
592
+ # Testar Cloud Run API
593
+ CR_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
594
+ -H "Authorization: Bearer $SA_TOKEN" \
595
+ "https://run.googleapis.com/v2/projects/$GCP_CLIENT/locations/us-central1/services" 2>/dev/null)
596
+ log "Cloud Run API status: $CR_STATUS"
597
+ if [ "$CR_STATUS" = "200" ]; then
598
+ ok "A5: Cloud Run API accessible with SA token"
599
+ else
600
+ fail "A5: Cloud Run API returned $CR_STATUS — fixing roles"
601
+ ALL_GREEN=false
602
+ gcloud projects add-iam-policy-binding $GCP_CLIENT \
603
+ --member="serviceAccount:$SA_EMAIL" --role="roles/run.admin" --quiet 2>/dev/null
604
+ gcloud projects add-iam-policy-binding $GCP_CLIENT \
605
+ --member="serviceAccount:$SA_EMAIL" --role="roles/iam.serviceAccountUser" --quiet 2>/dev/null
606
+ sleep 10
607
+ fi
608
+ else
609
+ fail "A5: Could not obtain SA token"
610
+ ALL_GREEN=false
611
+ fi
612
+
613
+ if [ "$ALL_GREEN" = "true" ]; then
614
+ log "======================================"
615
+ log "AGENT — TODOS OS ITEMS VERDES!"
616
+ log "======================================"
617
+ AGENT_DONE=true
618
+ else
619
+ log "Items failing — fixing and retrying..."
620
+ sleep 5
621
+ fi
622
+ done
623
+ ```
package/Dockerfile CHANGED
@@ -42,6 +42,8 @@ RUN apk update && \
42
42
  curl \
43
43
  nfs-utils \
44
44
  openjdk17-jdk \
45
+ maven \
46
+ python3 \
45
47
  && rm -rf /var/cache/apk/*
46
48
 
47
49
  # Set JAVA_HOME so ./mvnw can find Java