feat(runner): implement Surgical Healing Protocol in Ralph Loop
This commit is contained in:
82
vibn-agent-runner/dist/agent-session-runner.js
vendored
82
vibn-agent-runner/dist/agent-session-runner.js
vendored
@@ -13,6 +13,41 @@ const vibn_chat_model_1 = require("./llm/vibn-chat-model");
|
||||
const tools_1 = require("./tools");
|
||||
const loader_1 = require("./prompts/loader");
|
||||
const MAX_TURNS = 45;
|
||||
function runBuildVerification(repoRoot, appPath) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
const absoluteAppPath = path.join(repoRoot, appPath);
|
||||
const pkgJsonPath = path.join(absoluteAppPath, "package.json");
|
||||
if (!fs.existsSync(pkgJsonPath)) {
|
||||
return { success: true }; // No package.json, skip build check
|
||||
}
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
||||
// Only verify if there is an explicit build script
|
||||
if (!pkg.scripts || !pkg.scripts.build) {
|
||||
return { success: true };
|
||||
}
|
||||
console.log(`[Ralph Loop] Running automatic build verification: npm run build inside ${absoluteAppPath}...`);
|
||||
// Run npm run build with a 45s timeout to prevent hanging
|
||||
execSync("npm run build", {
|
||||
cwd: absoluteAppPath,
|
||||
stdio: "pipe",
|
||||
timeout: 45000,
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
catch (err) {
|
||||
const stderr = err.stderr
|
||||
? err.stderr.toString()
|
||||
: err.message || String(err);
|
||||
console.warn(`[Ralph Loop] Build verification failed:`, stderr);
|
||||
return {
|
||||
success: false,
|
||||
error: stderr.slice(-3000), // Cap the log length to avoid flooding the prompt context
|
||||
};
|
||||
}
|
||||
}
|
||||
// ── VIBN DB bridge ────────────────────────────────────────────────────────────
|
||||
async function patchSession(opts, payload) {
|
||||
const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`;
|
||||
@@ -265,6 +300,53 @@ Do NOT run git commit or git push — the platform handles committing after you
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// ── Cloud Build Verification (Ralph Loop integration) ──
|
||||
if (opts.repoRoot && ralphIteration < 3) {
|
||||
await emit({
|
||||
ts: now(),
|
||||
type: "info",
|
||||
text: "🔍 [Ralph Loop] Initiating automatic build verification...",
|
||||
});
|
||||
const verification = runBuildVerification(opts.repoRoot, opts.appPath);
|
||||
if (!verification.success) {
|
||||
ralphIteration++;
|
||||
await emit({
|
||||
ts: now(),
|
||||
type: "error",
|
||||
text: `❌ [Ralph Loop] Build verification failed (iteration ${ralphIteration}/3). Feeding compilation errors back to the model...`,
|
||||
});
|
||||
history.push({
|
||||
role: "user",
|
||||
content: `Your previous edits completed, but the project's build check failed with compilation errors.
|
||||
|
||||
=========================================
|
||||
🚨 SURGICAL HEALING PROTOCOL ACTIVE 🚨
|
||||
=========================================
|
||||
The project's compilation/build has failed. You are currently in an autonomous, auto-correcting healing loop and must fix this compilation error immediately.
|
||||
|
||||
To prevent cognitive loop spirals and command limits, you MUST follow this strict, non-negotiable troubleshooting protocol:
|
||||
|
||||
1. 🚫 STRICTLY BLOCK EXPLORATION: DO NOT execute general directory exploration or orientation commands such as 'ls', 'find', 'pwd', 'grep', 'git status', 'git diff', or other search commands. You do not need to look around.
|
||||
2. 🎯 SURGICAL TARGETING: Scan the compiler error logs below to locate the EXACT filename, line number, and column where the compilation failed.
|
||||
3. 🛠️ IMMEDIATE CORRECTION: Read that file immediately using your specific file-reading tool (using precise start/end lines if it is large) and apply a targeted, surgical edit to correct the exact syntax or type error. Do not write a placeholder or partial fix.
|
||||
|
||||
Here are the precise compilation errors from the compiler:
|
||||
\`\`\`text
|
||||
${verification.error}
|
||||
\`\`\`
|
||||
|
||||
Implement the exact fix directly in the code now.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
await emit({
|
||||
ts: now(),
|
||||
type: "info",
|
||||
text: "🟢 [Ralph Loop] Build verification passed successfully! 0 errors.",
|
||||
});
|
||||
}
|
||||
}
|
||||
// If fully complete, trigger auto-commit and finish
|
||||
if (opts.autoApprove) {
|
||||
await autoCommitAndDeploy(opts, task, emit);
|
||||
|
||||
263
vibn-agent-runner/dist/server.js
vendored
263
vibn-agent-runner/dist/server.js
vendored
@@ -47,160 +47,257 @@ const app = (0, express_1.default)();
|
||||
app.use((0, cors_1.default)());
|
||||
const startTime = new Date();
|
||||
// Raw body capture for webhook HMAC — must come before express.json()
|
||||
app.use('/webhook/gitea', express_1.default.raw({ type: '*/*' }));
|
||||
app.use("/webhook/gitea", express_1.default.raw({ type: "*/*" }));
|
||||
app.use(express_1.default.json());
|
||||
const PORT = process.env.PORT || 3333;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build ToolContext from environment variables
|
||||
// ---------------------------------------------------------------------------
|
||||
function ensureWorkspace(repo) {
|
||||
const base = process.env.WORKSPACE_BASE || '/workspaces';
|
||||
function ensureWorkspace(repo, sessionId) {
|
||||
const base = process.env.WORKSPACE_BASE || "/workspaces";
|
||||
if (!repo) {
|
||||
const dir = path.join(base, 'default');
|
||||
const dir = path.join(base, "default");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
const dir = path.join(base, repo.replace('/', '_'));
|
||||
const mainRepoDir = path.join(base, repo.replace("/", "_"));
|
||||
const gitea = {
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
username: process.env.GITEA_USERNAME || ''
|
||||
apiUrl: process.env.GITEA_API_URL || "",
|
||||
apiToken: process.env.GITEA_API_TOKEN || "",
|
||||
username: process.env.GITEA_USERNAME || "",
|
||||
};
|
||||
if (!fs.existsSync(path.join(dir, '.git'))) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const authedUrl = `${gitea.apiUrl}/${repo}.git`
|
||||
.replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
|
||||
// 1. Ensure main repo clone exists
|
||||
if (!fs.existsSync(path.join(mainRepoDir, ".git"))) {
|
||||
fs.mkdirSync(mainRepoDir, { recursive: true });
|
||||
const authedUrl = `${gitea.apiUrl}/${repo}.git`.replace("https://", `https://${gitea.username}:${gitea.apiToken}@`);
|
||||
try {
|
||||
(0, child_process_1.execSync)(`git clone "${authedUrl}" "${dir}"`, { stdio: 'pipe' });
|
||||
(0, child_process_1.execSync)(`git clone "${authedUrl}" "${mainRepoDir}"`, { stdio: "pipe" });
|
||||
}
|
||||
catch {
|
||||
// Repo may not exist yet — just init
|
||||
(0, child_process_1.execSync)(`git init`, { cwd: dir, stdio: 'pipe' });
|
||||
(0, child_process_1.execSync)(`git remote add origin "${authedUrl}"`, { cwd: dir, stdio: 'pipe' });
|
||||
(0, child_process_1.execSync)(`git init`, { cwd: mainRepoDir, stdio: "pipe" });
|
||||
(0, child_process_1.execSync)(`git remote add origin "${authedUrl}"`, {
|
||||
cwd: mainRepoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
// 2. If no sessionId, fall back to main repo clone directly
|
||||
if (!sessionId) {
|
||||
return mainRepoDir;
|
||||
}
|
||||
// 3. Isolated Worktree Directory per task session
|
||||
const taskWorktreePath = path.join(base, "tasks", sessionId);
|
||||
fs.mkdirSync(path.join(base, "tasks"), { recursive: true });
|
||||
// 4. Create isolated worktree if not yet active
|
||||
if (!fs.existsSync(path.join(taskWorktreePath, ".git"))) {
|
||||
// Clean up any stale directory from previous failed runs before adding worktree
|
||||
if (fs.existsSync(taskWorktreePath)) {
|
||||
try {
|
||||
fs.rmSync(taskWorktreePath, { recursive: true, force: true });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
try {
|
||||
console.log(`[worktree] Adding isolated git worktree for session ${sessionId} at ${taskWorktreePath}...`);
|
||||
// Check if the branch task-sessionId already exists in the main repository
|
||||
let branchExists = false;
|
||||
try {
|
||||
const branches = (0, child_process_1.execSync)(`git branch --list "task-${sessionId}"`, {
|
||||
cwd: mainRepoDir,
|
||||
}).toString();
|
||||
branchExists = branches.trim().length > 0;
|
||||
}
|
||||
catch {
|
||||
branchExists = false;
|
||||
}
|
||||
if (branchExists) {
|
||||
// Checkout the existing branch into the new worktree path
|
||||
(0, child_process_1.execSync)(`git worktree add -f "${taskWorktreePath}" "task-${sessionId}"`, { cwd: mainRepoDir, stdio: "pipe" });
|
||||
}
|
||||
else {
|
||||
// Create and checkout a new isolated branch
|
||||
(0, child_process_1.execSync)(`git worktree add -f -b "task-${sessionId}" "${taskWorktreePath}"`, { cwd: mainRepoDir, stdio: "pipe" });
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error("[worktree] Failed to add git worktree, falling back to main clone:", e.message || String(e));
|
||||
return mainRepoDir;
|
||||
}
|
||||
}
|
||||
return taskWorktreePath;
|
||||
}
|
||||
function buildContext(repo) {
|
||||
const workspaceRoot = ensureWorkspace(repo);
|
||||
function buildContext(repo, sessionId) {
|
||||
const workspaceRoot = ensureWorkspace(repo, sessionId);
|
||||
return {
|
||||
workspaceRoot,
|
||||
gitea: {
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
username: process.env.GITEA_USERNAME || ''
|
||||
apiUrl: process.env.GITEA_API_URL || "",
|
||||
apiToken: process.env.GITEA_API_TOKEN || "",
|
||||
username: process.env.GITEA_USERNAME || "",
|
||||
},
|
||||
coolify: {
|
||||
apiUrl: process.env.COOLIFY_API_URL || '',
|
||||
apiToken: process.env.COOLIFY_API_TOKEN || ''
|
||||
apiUrl: process.env.COOLIFY_API_URL || "",
|
||||
apiToken: process.env.COOLIFY_API_TOKEN || "",
|
||||
},
|
||||
mcpToken: '',
|
||||
vibnApiUrl: process.env.VIBN_API_URL ?? 'https://vibnai.com',
|
||||
memoryUpdates: []
|
||||
mcpToken: "",
|
||||
vibnApiUrl: process.env.VIBN_API_URL ?? "https://vibnai.com",
|
||||
memoryUpdates: [],
|
||||
};
|
||||
}
|
||||
function cleanupWorkspace(repo, sessionId) {
|
||||
const base = process.env.WORKSPACE_BASE || "/workspaces";
|
||||
const mainRepoDir = path.join(base, repo.replace("/", "_"));
|
||||
const taskWorktreePath = path.join(base, "tasks", sessionId);
|
||||
if (fs.existsSync(taskWorktreePath)) {
|
||||
try {
|
||||
console.log(`[worktree] Pruning and removing git worktree for session ${sessionId}...`);
|
||||
// 1. Tell git to remove the worktree references
|
||||
(0, child_process_1.execSync)(`git worktree remove --force "${taskWorktreePath}"`, {
|
||||
cwd: mainRepoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
// 2. Delete the temporary branch from the main repository index
|
||||
(0, child_process_1.execSync)(`git branch -D "task-${sessionId}"`, {
|
||||
cwd: mainRepoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
// 3. Force clean directory
|
||||
if (fs.existsSync(taskWorktreePath)) {
|
||||
fs.rmSync(taskWorktreePath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.warn(`[worktree] Non-fatal cleanup error for session ${sessionId}:`, e.message || String(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Routes
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health check
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitHub mirror — clone a public GitHub repo and push to Gitea as-is
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/api/mirror', async (req, res) => {
|
||||
app.post("/api/mirror", async (req, res) => {
|
||||
const { github_url, gitea_repo, project_name, github_token } = req.body;
|
||||
if (!github_url || !gitea_repo) {
|
||||
res.status(400).json({ error: '"github_url" and "gitea_repo" are required' });
|
||||
res
|
||||
.status(400)
|
||||
.json({ error: '"github_url" and "gitea_repo" are required' });
|
||||
return;
|
||||
}
|
||||
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
||||
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
||||
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
||||
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
||||
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
||||
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
||||
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
||||
const mirrorId = `mirror_${Date.now()}`;
|
||||
const tmpDir = path.join(os.tmpdir(), mirrorId);
|
||||
const gitea = {
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
username: process.env.GITEA_USERNAME || ''
|
||||
apiUrl: process.env.GITEA_API_URL || "",
|
||||
apiToken: process.env.GITEA_API_TOKEN || "",
|
||||
username: process.env.GITEA_USERNAME || "",
|
||||
};
|
||||
try {
|
||||
// Build authenticated Gitea push URL
|
||||
// GITEA_API_URL is like https://git.vibnai.com — strip /api/v1 if present
|
||||
const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, '');
|
||||
const authedPushUrl = `${giteaBase}/${gitea_repo}.git`
|
||||
.replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
|
||||
const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, "");
|
||||
const authedPushUrl = `${giteaBase}/${gitea_repo}.git`.replace("https://", `https://${gitea.username}:${gitea.apiToken}@`);
|
||||
console.log(`[mirror] Cloning ${github_url} → ${tmpDir}`);
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
// Build authenticated clone URL for private repos
|
||||
let cloneUrl = github_url;
|
||||
if (github_token) {
|
||||
cloneUrl = github_url.replace('https://', `https://${github_token}@`);
|
||||
cloneUrl = github_url.replace("https://", `https://${github_token}@`);
|
||||
}
|
||||
// Mirror-clone the GitHub repo (preserves all branches + tags)
|
||||
execSync(`git clone --mirror "${cloneUrl}" "${tmpDir}/.git"`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 120000
|
||||
stdio: "pipe",
|
||||
timeout: 120000,
|
||||
});
|
||||
execSync(`git config --bool core.bare false`, { cwd: tmpDir, stdio: 'pipe' });
|
||||
execSync(`git checkout`, { cwd: tmpDir, stdio: 'pipe' });
|
||||
execSync(`git config --bool core.bare false`, {
|
||||
cwd: tmpDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
execSync(`git checkout`, { cwd: tmpDir, stdio: "pipe" });
|
||||
// Point origin at Gitea and push all refs
|
||||
execSync(`git remote set-url origin "${authedPushUrl}"`, { cwd: tmpDir, stdio: 'pipe' });
|
||||
execSync(`git push --mirror origin`, { cwd: tmpDir, stdio: 'pipe', timeout: 120000 });
|
||||
execSync(`git remote set-url origin "${authedPushUrl}"`, {
|
||||
cwd: tmpDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
execSync(`git push --mirror origin`, {
|
||||
cwd: tmpDir,
|
||||
stdio: "pipe",
|
||||
timeout: 120000,
|
||||
});
|
||||
console.log(`[mirror] Pushed ${gitea_repo} successfully`);
|
||||
res.json({ success: true, gitea_repo, github_url });
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[mirror] Failed:`, msg);
|
||||
res.status(500).json({ error: 'Mirror failed', details: msg });
|
||||
res.status(500).json({ error: "Mirror failed", details: msg });
|
||||
}
|
||||
finally {
|
||||
// Clean up temp dir
|
||||
try {
|
||||
const { execSync: rm } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
||||
rm(`rm -rf "${tmpDir}"`, { stdio: 'pipe' });
|
||||
const { execSync: rm } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
||||
rm(`rm -rf "${tmpDir}"`, { stdio: "pipe" });
|
||||
}
|
||||
catch {
|
||||
/* best effort */
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
}
|
||||
});
|
||||
// List available agents
|
||||
app.get('/api/agents', (_req, res) => {
|
||||
const agents = Object.values(agents_1.AGENTS).map(a => ({
|
||||
app.get("/api/agents", (_req, res) => {
|
||||
const agents = Object.values(agents_1.AGENTS).map((a) => ({
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
tools: a.tools.map(t => t.name)
|
||||
tools: a.tools.map((t) => t.name),
|
||||
}));
|
||||
res.json(agents);
|
||||
});
|
||||
const activeSessions = new Map();
|
||||
app.post('/agent/execute', async (req, res) => {
|
||||
const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, mcpToken, } = req.body;
|
||||
app.post("/agent/execute", async (req, res) => {
|
||||
const { sessionId, projectId, appName, appPath: rawAppPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, mcpToken, } = req.body;
|
||||
const appPath = rawAppPath === undefined || rawAppPath === null || rawAppPath === ""
|
||||
? "."
|
||||
: rawAppPath;
|
||||
if (!sessionId || !projectId || !appPath || !task) {
|
||||
res.status(400).json({ error: 'sessionId, projectId, appPath and task are required' });
|
||||
res
|
||||
.status(400)
|
||||
.json({ error: "sessionId, projectId, appPath and task are required" });
|
||||
return;
|
||||
}
|
||||
const vibnApiUrl = process.env.VIBN_API_URL ?? 'https://vibnai.com';
|
||||
const vibnApiUrl = process.env.VIBN_API_URL ?? "https://vibnai.com";
|
||||
const patchHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
...(process.env.AGENT_RUNNER_SECRET
|
||||
? { "x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET }
|
||||
: {}),
|
||||
};
|
||||
// Register session as active
|
||||
const sessionState = { stopped: false };
|
||||
activeSessions.set(sessionId, sessionState);
|
||||
// Respond immediately — execution is async
|
||||
res.status(202).json({ sessionId, status: 'running' });
|
||||
res.status(202).json({ sessionId, status: "running" });
|
||||
// Build workspace context — clone/update the Gitea repo if provided
|
||||
let ctx;
|
||||
try {
|
||||
ctx = buildContext(giteaRepo);
|
||||
ctx = buildContext(giteaRepo, sessionId);
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error('[agent/execute] buildContext failed:', msg);
|
||||
console.error("[agent/execute] buildContext failed:", msg);
|
||||
// Notify VIBN DB of failure
|
||||
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'failed', error: msg }),
|
||||
method: "PATCH",
|
||||
headers: patchHeaders,
|
||||
body: JSON.stringify({ status: "failed", error: msg }),
|
||||
}).catch(() => { });
|
||||
activeSessions.delete(sessionId);
|
||||
return;
|
||||
@@ -217,17 +314,20 @@ app.post('/agent/execute', async (req, res) => {
|
||||
ctx.projectId = projectId;
|
||||
// Scope workspace to the app subdirectory so the agent works there naturally
|
||||
if (appPath) {
|
||||
const path = require('path');
|
||||
const path = require("path");
|
||||
ctx.workspaceRoot = path.join(ctx.workspaceRoot, appPath);
|
||||
const fs = require('fs');
|
||||
const fs = require("fs");
|
||||
fs.mkdirSync(ctx.workspaceRoot, { recursive: true });
|
||||
}
|
||||
const agentConfig = agents_1.AGENTS['Coder'];
|
||||
const agentConfig = agents_1.AGENTS["Coder"];
|
||||
if (!agentConfig) {
|
||||
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'failed', error: 'Coder agent not registered' }),
|
||||
method: "PATCH",
|
||||
headers: patchHeaders,
|
||||
body: JSON.stringify({
|
||||
status: "failed",
|
||||
error: "Coder agent not registered",
|
||||
}),
|
||||
}).catch(() => { });
|
||||
activeSessions.delete(sessionId);
|
||||
return;
|
||||
@@ -251,38 +351,41 @@ app.post('/agent/execute', async (req, res) => {
|
||||
coolifyApiUrl: process.env.COOLIFY_API_URL,
|
||||
coolifyApiToken: process.env.COOLIFY_API_TOKEN,
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[agent/execute] session ${sessionId} crashed:`, msg);
|
||||
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'failed', error: msg }),
|
||||
method: "PATCH",
|
||||
headers: patchHeaders,
|
||||
body: JSON.stringify({ status: "failed", error: msg }),
|
||||
}).catch(() => { });
|
||||
})
|
||||
.finally(() => {
|
||||
activeSessions.delete(sessionId);
|
||||
if (giteaRepo && sessionId) {
|
||||
cleanupWorkspace(giteaRepo, sessionId);
|
||||
}
|
||||
});
|
||||
});
|
||||
app.post('/agent/stop', (req, res) => {
|
||||
app.post("/agent/stop", (req, res) => {
|
||||
const { sessionId } = req.body;
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ error: 'sessionId required' });
|
||||
res.status(400).json({ error: "sessionId required" });
|
||||
return;
|
||||
}
|
||||
const session = activeSessions.get(sessionId);
|
||||
if (session) {
|
||||
session.stopped = true;
|
||||
res.json({ status: 'stopped' });
|
||||
res.json({ status: "stopped" });
|
||||
}
|
||||
else {
|
||||
res.status(404).json({ error: 'session not found or not running' });
|
||||
res.status(404).json({ error: "session not found or not running" });
|
||||
}
|
||||
});
|
||||
app.listen(PORT, () => {
|
||||
console.log(`AgentRunner listening on port ${PORT}`);
|
||||
console.log(`Agents available: ${Object.keys(agents_1.AGENTS).join(', ')}`);
|
||||
console.log(`Agents available: ${Object.keys(agents_1.AGENTS).join(", ")}`);
|
||||
if (!process.env.GOOGLE_API_KEY) {
|
||||
console.warn('WARNING: GOOGLE_API_KEY is not set — agents will fail');
|
||||
console.warn("WARNING: GOOGLE_API_KEY is not set — agents will fail");
|
||||
}
|
||||
});
|
||||
|
||||
1
vibn-agent-runner/dist/test-execute-hardening.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/test-execute-hardening.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
139
vibn-agent-runner/dist/test-execute-hardening.js
vendored
Normal file
139
vibn-agent-runner/dist/test-execute-hardening.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const child_process_1 = require("child_process");
|
||||
const http_1 = __importDefault(require("http"));
|
||||
// We will start the runner server on port 3334
|
||||
const PORT = 3334;
|
||||
const BASE_URL = `http://localhost:${PORT}`;
|
||||
console.log("🧪 Starting AgentRunner Hardening Test Suite...");
|
||||
// Set up environment variables
|
||||
const env = {
|
||||
...process.env,
|
||||
PORT: String(PORT),
|
||||
AGENT_RUNNER_SECRET: "test-secret-123",
|
||||
GOOGLE_API_KEY: "dummy-key-for-testing", // Pass dummy key to avoid Gemini API initialization crash
|
||||
VIBN_API_URL: "http://localhost:3335", // Mock backend
|
||||
};
|
||||
// Start mock backend on port 3335 to catch PATCH callbacks and verify headers
|
||||
let receivedHeaders = null;
|
||||
let receivedBody = null;
|
||||
const mockBackend = http_1.default.createServer((req, res) => {
|
||||
receivedHeaders = req.headers;
|
||||
let body = "";
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on("end", () => {
|
||||
try {
|
||||
receivedBody = JSON.parse(body);
|
||||
}
|
||||
catch {
|
||||
receivedBody = body;
|
||||
}
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
});
|
||||
});
|
||||
mockBackend.listen(3335, () => {
|
||||
console.log("✓ Mock backend server listening on port 3335");
|
||||
});
|
||||
// Spawn the runner server
|
||||
const serverProcess = (0, child_process_1.spawn)("npx", ["ts-node", "src/server.ts"], {
|
||||
env,
|
||||
stdio: "pipe",
|
||||
});
|
||||
// Wait for server to start
|
||||
serverProcess.stdout.on("data", (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`[Server Out] ${output.trim()}`);
|
||||
});
|
||||
serverProcess.stderr.on("data", (data) => {
|
||||
console.error(`[Server Err] ${data.toString()}`);
|
||||
});
|
||||
// Helper function to sleep
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
async function runTests() {
|
||||
// Wait 4 seconds for server to boot
|
||||
await sleep(4000);
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
const assert = (condition, message) => {
|
||||
if (condition) {
|
||||
console.log(` 🟢 PASSED: ${message}`);
|
||||
passed++;
|
||||
}
|
||||
else {
|
||||
console.error(` 🔴 FAILED: ${message}`);
|
||||
failed++;
|
||||
}
|
||||
};
|
||||
try {
|
||||
// Test 1: Empty appPath should be accepted and fall back to "."
|
||||
console.log("\n1️⃣ Testing appPath empty string fallback...");
|
||||
const res1 = await fetch(`${BASE_URL}/agent/execute`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "test-session-1",
|
||||
projectId: "test-project-1",
|
||||
task: "Test empty appPath",
|
||||
appPath: "", // Empty string!
|
||||
giteaRepo: "test-repo",
|
||||
}),
|
||||
});
|
||||
assert(res1.status === 202, `Should return 202, got ${res1.status}`);
|
||||
const data1 = (await res1.json());
|
||||
assert(data1.sessionId === "test-session-1", `Should return correct sessionId, got ${data1.sessionId}`);
|
||||
// Test 2: Missing sessionId should return 400
|
||||
console.log("\n2️⃣ Testing missing required parameters (sessionId)...");
|
||||
const res2 = await fetch(`${BASE_URL}/agent/execute`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
projectId: "test-project-1",
|
||||
task: "Test missing sessionId",
|
||||
appPath: ".",
|
||||
}),
|
||||
});
|
||||
assert(res2.status === 400, `Should return 400, got ${res2.status}`);
|
||||
// Test 3: Emergency callback headers should include x-agent-runner-secret
|
||||
console.log("\n3️⃣ Testing early failure callback headers...");
|
||||
// Trigger a clone failure by passing a malformed giteaRepo containing slash,
|
||||
// which triggers clone instead of default workspace but will fail clone.
|
||||
console.log("Triggering clone failure on mock Gitea...");
|
||||
const res3 = await fetch(`${BASE_URL}/agent/execute`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "test-session-3",
|
||||
projectId: "test-project-3",
|
||||
task: "Trigger crash",
|
||||
appPath: ".",
|
||||
giteaRepo: "invalid_owner/invalid_repo",
|
||||
}),
|
||||
});
|
||||
assert(res3.status === 202, `Should return 202 Accepted, got ${res3.status}`);
|
||||
// Wait for server to process async task and fail, calling our mock backend PATCH
|
||||
console.log("Waiting for runner callback on mock backend...");
|
||||
await sleep(4000);
|
||||
assert(receivedHeaders !== null, "Should call mock backend PATCH endpoint");
|
||||
if (receivedHeaders) {
|
||||
assert(receivedHeaders["x-agent-runner-secret"] === "test-secret-123", `Callback should include secret header 'test-secret-123', got '${receivedHeaders["x-agent-runner-secret"]}'`);
|
||||
assert(receivedBody && receivedBody.status === "failed", `Callback body should have status 'failed', got '${receivedBody?.status}'`);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Test execution failed with exception:", err);
|
||||
}
|
||||
finally {
|
||||
console.log("\n🧹 Cleaning up test servers...");
|
||||
serverProcess.kill();
|
||||
mockBackend.close();
|
||||
console.log(`\n📊 Tests complete. Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
runTests();
|
||||
@@ -37,5 +37,6 @@
|
||||
"@types/uuid": "^9.0.8",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8"
|
||||
}
|
||||
|
||||
@@ -417,7 +417,25 @@ Do NOT run git commit or git push — the platform handles committing after you
|
||||
|
||||
history.push({
|
||||
role: "user",
|
||||
content: `Your previous edits completed, but the project's build check failed with compilation errors. Please fix these errors immediately so the build compiles clean:\n\n\`\`\`text\n${verification.error}\n\`\`\``,
|
||||
content: `Your previous edits completed, but the project's build check failed with compilation errors.
|
||||
|
||||
=========================================
|
||||
🚨 SURGICAL HEALING PROTOCOL ACTIVE 🚨
|
||||
=========================================
|
||||
The project's compilation/build has failed. You are currently in an autonomous, auto-correcting healing loop and must fix this compilation error immediately.
|
||||
|
||||
To prevent cognitive loop spirals and command limits, you MUST follow this strict, non-negotiable troubleshooting protocol:
|
||||
|
||||
1. 🚫 STRICTLY BLOCK EXPLORATION: DO NOT execute general directory exploration or orientation commands such as 'ls', 'find', 'pwd', 'grep', 'git status', 'git diff', or other search commands. You do not need to look around.
|
||||
2. 🎯 SURGICAL TARGETING: Scan the compiler error logs below to locate the EXACT filename, line number, and column where the compilation failed.
|
||||
3. 🛠️ IMMEDIATE CORRECTION: Read that file immediately using your specific file-reading tool (using precise start/end lines if it is large) and apply a targeted, surgical edit to correct the exact syntax or type error. Do not write a placeholder or partial fix.
|
||||
|
||||
Here are the precise compilation errors from the compiler:
|
||||
\`\`\`text
|
||||
${verification.error}
|
||||
\`\`\`
|
||||
|
||||
Implement the exact fix directly in the code now.`,
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user