feat: Gitea webhook with HMAC-SHA256 auth, agent label routing, auto-close issues
Made-with: Cursor
This commit is contained in:
6
dist/agents.js
vendored
6
dist/agents.js
vendored
@@ -70,8 +70,12 @@ Your job is to complete the coding task given to you. Follow these rules:
|
|||||||
- Never modify .env files or credentials.
|
- Never modify .env files or credentials.
|
||||||
- Never commit secrets or API keys.
|
- Never commit secrets or API keys.
|
||||||
|
|
||||||
|
**If you were triggered by a Gitea issue:**
|
||||||
|
- After committing, close the issue using gitea_close_issue.
|
||||||
|
- The repo name is in the format "owner/name".
|
||||||
|
|
||||||
Be methodical. Read before you write. Test before you commit.`,
|
Be methodical. Read before you write. Test before you commit.`,
|
||||||
tools: pick([...FILE_TOOLS, ...SHELL_TOOLS, ...GIT_TOOLS])
|
tools: pick([...FILE_TOOLS, ...SHELL_TOOLS, ...GIT_TOOLS, ...GITEA_TOOLS])
|
||||||
},
|
},
|
||||||
PM: {
|
PM: {
|
||||||
name: 'PM',
|
name: 'PM',
|
||||||
|
|||||||
27
dist/server.js
vendored
27
dist/server.js
vendored
@@ -40,6 +40,7 @@ const express_1 = __importDefault(require("express"));
|
|||||||
const cors_1 = __importDefault(require("cors"));
|
const cors_1 = __importDefault(require("cors"));
|
||||||
const fs = __importStar(require("fs"));
|
const fs = __importStar(require("fs"));
|
||||||
const path = __importStar(require("path"));
|
const path = __importStar(require("path"));
|
||||||
|
const crypto = __importStar(require("crypto"));
|
||||||
const child_process_1 = require("child_process");
|
const child_process_1 = require("child_process");
|
||||||
const job_store_1 = require("./job-store");
|
const job_store_1 = require("./job-store");
|
||||||
const agent_runner_1 = require("./agent-runner");
|
const agent_runner_1 = require("./agent-runner");
|
||||||
@@ -157,19 +158,25 @@ app.get('/api/jobs', (req, res) => {
|
|||||||
const limit = parseInt(req.query.limit || '20', 10);
|
const limit = parseInt(req.query.limit || '20', 10);
|
||||||
res.json((0, job_store_1.listJobs)(limit));
|
res.json((0, job_store_1.listJobs)(limit));
|
||||||
});
|
});
|
||||||
// Gitea webhook endpoint — triggers agent from a push/issue event
|
// Gitea webhook endpoint — triggers agent from an issue event
|
||||||
app.post('/webhook/gitea', (req, res) => {
|
// Must use raw body for HMAC verification — register before express.json()
|
||||||
|
app.post('/webhook/gitea', express_1.default.raw({ type: 'application/json' }), (req, res) => {
|
||||||
const event = req.headers['x-gitea-event'];
|
const event = req.headers['x-gitea-event'];
|
||||||
const body = req.body;
|
const rawBody = req.body;
|
||||||
// Verify secret if configured
|
// Verify HMAC-SHA256 signature
|
||||||
const webhookSecret = process.env.WEBHOOK_SECRET;
|
const webhookSecret = process.env.WEBHOOK_SECRET;
|
||||||
if (webhookSecret) {
|
if (webhookSecret) {
|
||||||
const sig = req.headers['x-gitea-signature'];
|
const sig = req.headers['x-gitea-signature'];
|
||||||
if (!sig || sig !== webhookSecret) {
|
const expected = crypto
|
||||||
|
.createHmac('sha256', webhookSecret)
|
||||||
|
.update(rawBody)
|
||||||
|
.digest('hex');
|
||||||
|
if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
||||||
res.status(401).json({ error: 'Invalid webhook signature' });
|
res.status(401).json({ error: 'Invalid webhook signature' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const body = JSON.parse(rawBody.toString('utf8'));
|
||||||
let task = null;
|
let task = null;
|
||||||
let agentName = 'Coder';
|
let agentName = 'Coder';
|
||||||
let repo;
|
let repo;
|
||||||
@@ -183,13 +190,17 @@ app.post('/webhook/gitea', (req, res) => {
|
|||||||
else if (labels.includes('agent:marketing')) {
|
else if (labels.includes('agent:marketing')) {
|
||||||
agentName = 'Marketing';
|
agentName = 'Marketing';
|
||||||
}
|
}
|
||||||
else {
|
else if (labels.includes('agent:coder')) {
|
||||||
agentName = 'Coder';
|
agentName = 'Coder';
|
||||||
}
|
}
|
||||||
task = `Resolve this GitHub issue:\n\nTitle: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}`;
|
else {
|
||||||
|
// No agent label — ignore
|
||||||
|
res.json({ ignored: true, reason: 'no agent label on issue' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
task = `You have been assigned to resolve a Gitea issue in the repo ${repo}.\n\nIssue #${issue.number}: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}\n\nWhen done, close the issue by calling gitea_close_issue.`;
|
||||||
}
|
}
|
||||||
else if (event === 'push') {
|
else if (event === 'push') {
|
||||||
// Optionally trigger on push — useful for CI-style automation
|
|
||||||
res.json({ ignored: true, reason: 'push events not auto-processed' });
|
res.json({ ignored: true, reason: 'push events not auto-processed' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,8 +84,12 @@ Your job is to complete the coding task given to you. Follow these rules:
|
|||||||
- Never modify .env files or credentials.
|
- Never modify .env files or credentials.
|
||||||
- Never commit secrets or API keys.
|
- Never commit secrets or API keys.
|
||||||
|
|
||||||
|
**If you were triggered by a Gitea issue:**
|
||||||
|
- After committing, close the issue using gitea_close_issue.
|
||||||
|
- The repo name is in the format "owner/name".
|
||||||
|
|
||||||
Be methodical. Read before you write. Test before you commit.`,
|
Be methodical. Read before you write. Test before you commit.`,
|
||||||
tools: pick([...FILE_TOOLS, ...SHELL_TOOLS, ...GIT_TOOLS])
|
tools: pick([...FILE_TOOLS, ...SHELL_TOOLS, ...GIT_TOOLS, ...GITEA_TOOLS])
|
||||||
},
|
},
|
||||||
|
|
||||||
PM: {
|
PM: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { createJob, getJob, listJobs, updateJob } from './job-store';
|
import { createJob, getJob, listJobs, updateJob } from './job-store';
|
||||||
import { runAgent } from './agent-runner';
|
import { runAgent } from './agent-runner';
|
||||||
@@ -136,21 +137,28 @@ app.get('/api/jobs', (req: Request, res: Response) => {
|
|||||||
res.json(listJobs(limit));
|
res.json(listJobs(limit));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gitea webhook endpoint — triggers agent from a push/issue event
|
// Gitea webhook endpoint — triggers agent from an issue event
|
||||||
app.post('/webhook/gitea', (req: Request, res: Response) => {
|
// Must use raw body for HMAC verification — register before express.json()
|
||||||
|
app.post('/webhook/gitea', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
|
||||||
const event = req.headers['x-gitea-event'] as string;
|
const event = req.headers['x-gitea-event'] as string;
|
||||||
const body = req.body as any;
|
const rawBody = req.body as Buffer;
|
||||||
|
|
||||||
// Verify secret if configured
|
// Verify HMAC-SHA256 signature
|
||||||
const webhookSecret = process.env.WEBHOOK_SECRET;
|
const webhookSecret = process.env.WEBHOOK_SECRET;
|
||||||
if (webhookSecret) {
|
if (webhookSecret) {
|
||||||
const sig = req.headers['x-gitea-signature'] as string;
|
const sig = req.headers['x-gitea-signature'] as string;
|
||||||
if (!sig || sig !== webhookSecret) {
|
const expected = crypto
|
||||||
|
.createHmac('sha256', webhookSecret)
|
||||||
|
.update(rawBody)
|
||||||
|
.digest('hex');
|
||||||
|
if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
||||||
res.status(401).json({ error: 'Invalid webhook signature' });
|
res.status(401).json({ error: 'Invalid webhook signature' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const body = JSON.parse(rawBody.toString('utf8'));
|
||||||
|
|
||||||
let task: string | null = null;
|
let task: string | null = null;
|
||||||
let agentName = 'Coder';
|
let agentName = 'Coder';
|
||||||
let repo: string | undefined;
|
let repo: string | undefined;
|
||||||
@@ -164,13 +172,16 @@ app.post('/webhook/gitea', (req: Request, res: Response) => {
|
|||||||
agentName = 'PM';
|
agentName = 'PM';
|
||||||
} else if (labels.includes('agent:marketing')) {
|
} else if (labels.includes('agent:marketing')) {
|
||||||
agentName = 'Marketing';
|
agentName = 'Marketing';
|
||||||
} else {
|
} else if (labels.includes('agent:coder')) {
|
||||||
agentName = 'Coder';
|
agentName = 'Coder';
|
||||||
|
} else {
|
||||||
|
// No agent label — ignore
|
||||||
|
res.json({ ignored: true, reason: 'no agent label on issue' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
task = `Resolve this GitHub issue:\n\nTitle: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}`;
|
task = `You have been assigned to resolve a Gitea issue in the repo ${repo}.\n\nIssue #${issue.number}: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}\n\nWhen done, close the issue by calling gitea_close_issue.`;
|
||||||
} else if (event === 'push') {
|
} else if (event === 'push') {
|
||||||
// Optionally trigger on push — useful for CI-style automation
|
|
||||||
res.json({ ignored: true, reason: 'push events not auto-processed' });
|
res.json({ ignored: true, reason: 'push events not auto-processed' });
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user