fix(runner): wire ToolContext vibnApiUrl + mcpToken so agent tools reach the frontend MCP
buildContext() hardcoded vibnApiUrl='http://localhost:3000' and mcpToken='', so every agent tool call (projects_list, workspace_describe, apps_list, ...) fetched the runner itself on a dead port and failed with 'fetch failed'. Now /agent/execute reads mcpToken from the request body and sets ctx.vibnApiUrl (from VIBN_API_URL), ctx.mcpToken, and ctx.projectId before running the agent.
This commit is contained in:
12
vibn-agent-runner/dist/agent-session-runner.d.ts
vendored
12
vibn-agent-runner/dist/agent-session-runner.d.ts
vendored
@@ -1,15 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* agent-session-runner.ts
|
* agent-session-runner.ts
|
||||||
*
|
*
|
||||||
* Streaming variant of runAgent wired to a VIBN agent_sessions row.
|
* Upgraded Cloud Agent Executor for VibnCode.
|
||||||
* After every LLM turn + tool call, it PATCHes the session in the VIBN DB
|
* Implements 4-level Smart Concurrency (parallel reads/lookups) and the
|
||||||
* so the frontend can poll (and later WebSocket) the live output.
|
* Ralph Loop (autonomous self-correction) entirely inside your secure Cloud VM.
|
||||||
*
|
|
||||||
* Key differences from runAgent:
|
|
||||||
* - Accepts an `emit` callback instead of updating job-store
|
|
||||||
* - Accepts an `isStopped` check so the frontend can cancel mid-run
|
|
||||||
* - Tracks which files were written/modified for the changed_files panel
|
|
||||||
* - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid]
|
|
||||||
*/
|
*/
|
||||||
import { AgentConfig } from "./agents";
|
import { AgentConfig } from "./agents";
|
||||||
import { ToolContext } from "./tools";
|
import { ToolContext } from "./tools";
|
||||||
|
|||||||
203
vibn-agent-runner/dist/agent-session-runner.js
vendored
203
vibn-agent-runner/dist/agent-session-runner.js
vendored
@@ -2,15 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* agent-session-runner.ts
|
* agent-session-runner.ts
|
||||||
*
|
*
|
||||||
* Streaming variant of runAgent wired to a VIBN agent_sessions row.
|
* Upgraded Cloud Agent Executor for VibnCode.
|
||||||
* After every LLM turn + tool call, it PATCHes the session in the VIBN DB
|
* Implements 4-level Smart Concurrency (parallel reads/lookups) and the
|
||||||
* so the frontend can poll (and later WebSocket) the live output.
|
* Ralph Loop (autonomous self-correction) entirely inside your secure Cloud VM.
|
||||||
*
|
|
||||||
* Key differences from runAgent:
|
|
||||||
* - Accepts an `emit` callback instead of updating job-store
|
|
||||||
* - Accepts an `isStopped` check so the frontend can cancel mid-run
|
|
||||||
* - Tracks which files were written/modified for the changed_files panel
|
|
||||||
* - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid]
|
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.runSessionAgent = runSessionAgent;
|
exports.runSessionAgent = runSessionAgent;
|
||||||
@@ -18,7 +12,7 @@ const child_process_1 = require("child_process");
|
|||||||
const vibn_chat_model_1 = require("./llm/vibn-chat-model");
|
const vibn_chat_model_1 = require("./llm/vibn-chat-model");
|
||||||
const tools_1 = require("./tools");
|
const tools_1 = require("./tools");
|
||||||
const loader_1 = require("./prompts/loader");
|
const loader_1 = require("./prompts/loader");
|
||||||
const MAX_TURNS = 60;
|
const MAX_TURNS = 45;
|
||||||
// ── VIBN DB bridge ────────────────────────────────────────────────────────────
|
// ── VIBN DB bridge ────────────────────────────────────────────────────────────
|
||||||
async function patchSession(opts, payload) {
|
async function patchSession(opts, payload) {
|
||||||
const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`;
|
const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`;
|
||||||
@@ -33,7 +27,6 @@ async function patchSession(opts, payload) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// Log but don't crash — output will be lost for this line but loop continues
|
|
||||||
console.warn("[session-runner] PATCH failed:", err instanceof Error ? err.message : err);
|
console.warn("[session-runner] PATCH failed:", err instanceof Error ? err.message : err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +38,8 @@ const FILE_WRITE_TOOLS = new Set([
|
|||||||
"write_file",
|
"write_file",
|
||||||
"replace_in_file",
|
"replace_in_file",
|
||||||
"create_file",
|
"create_file",
|
||||||
|
"fs_write",
|
||||||
|
"fs_edit",
|
||||||
]);
|
]);
|
||||||
function extractChangedFile(toolName, args, workspaceRoot, appPath) {
|
function extractChangedFile(toolName, args, workspaceRoot, appPath) {
|
||||||
if (!FILE_WRITE_TOOLS.has(toolName))
|
if (!FILE_WRITE_TOOLS.has(toolName))
|
||||||
@@ -56,7 +51,7 @@ function extractChangedFile(toolName, args, workspaceRoot, appPath) {
|
|||||||
const fullPrefix = `${workspaceRoot}/${appPath}/`;
|
const fullPrefix = `${workspaceRoot}/${appPath}/`;
|
||||||
const appPrefix = `${appPath}/`;
|
const appPrefix = `${appPath}/`;
|
||||||
let displayPath = rawPath.replace(fullPrefix, "").replace(appPrefix, "");
|
let displayPath = rawPath.replace(fullPrefix, "").replace(appPrefix, "");
|
||||||
const fileStatus = toolName === "write_file" ? "added" : "modified";
|
const fileStatus = toolName === "write_file" || toolName === "fs_write" ? "added" : "modified";
|
||||||
return { path: displayPath, status: fileStatus };
|
return { path: displayPath, status: fileStatus };
|
||||||
}
|
}
|
||||||
// ── Auto-commit helper ────────────────────────────────────────────────────────
|
// ── Auto-commit helper ────────────────────────────────────────────────────────
|
||||||
@@ -166,8 +161,7 @@ async function runSessionAgent(config, task, ctx, opts) {
|
|||||||
// Scope the system prompt to the specific app within the monorepo
|
// Scope the system prompt to the specific app within the monorepo
|
||||||
const basePrompt = (0, loader_1.resolvePrompt)(config.promptId);
|
const basePrompt = (0, loader_1.resolvePrompt)(config.promptId);
|
||||||
const scopedPrompt = `${basePrompt}
|
const scopedPrompt = `${basePrompt}
|
||||||
|
\n\n## Active context
|
||||||
## Active context
|
|
||||||
You are working inside the monorepo directory: ${opts.appPath}
|
You are working inside the monorepo directory: ${opts.appPath}
|
||||||
All file paths you use should be relative to this directory unless otherwise specified.
|
All file paths you use should be relative to this directory unless otherwise specified.
|
||||||
When running commands, always cd into ${opts.appPath} first unless already there.
|
When running commands, always cd into ${opts.appPath} first unless already there.
|
||||||
@@ -179,18 +173,25 @@ Do NOT run git commit or git push — the platform handles committing after you
|
|||||||
let roundsSinceText = 0;
|
let roundsSinceText = 0;
|
||||||
const toolFingerprints = [];
|
const toolFingerprints = [];
|
||||||
let loopBreakReason = null;
|
let loopBreakReason = null;
|
||||||
|
let ralphIteration = 0;
|
||||||
function fingerprintToolCall(tc) {
|
function fingerprintToolCall(tc) {
|
||||||
if (tc.name === "shell_exec") {
|
if (tc.name === "shell_exec") {
|
||||||
const cmd = String(tc.args?.command ?? "").trim();
|
const cmd = String(tc.args?.command ?? "").trim();
|
||||||
const verb = cmd.split("&&").map(s => s.trim()).find(s => !s.startsWith("cd "))?.split(/\s+/)[0] ?? "shell";
|
const verb = cmd
|
||||||
|
.split("&&")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.find((s) => !s.startsWith("cd "))
|
||||||
|
?.split(/\s+/)[0] ?? "shell";
|
||||||
return `shell_exec:${verb}`;
|
return `shell_exec:${verb}`;
|
||||||
}
|
}
|
||||||
if (tc.name === "fs_write" || tc.name === "fs_edit" || tc.name === "fs_read") {
|
if (tc.name === "fs_write" ||
|
||||||
|
tc.name === "fs_edit" ||
|
||||||
|
tc.name === "fs_read") {
|
||||||
return `${tc.name}:${tc.args?.path}`;
|
return `${tc.name}:${tc.args?.path}`;
|
||||||
}
|
}
|
||||||
return `${tc.name}:${Object.values(tc.args ?? {})[0]}`;
|
return `${tc.name}:${Object.values(tc.args ?? {})[0]}`;
|
||||||
}
|
}
|
||||||
while (turn < 30) {
|
while (turn < MAX_TURNS) {
|
||||||
if (opts.isStopped()) {
|
if (opts.isStopped()) {
|
||||||
await emit({ ts: now(), type: "info", text: "Stopped by user." });
|
await emit({ ts: now(), type: "info", text: "Stopped by user." });
|
||||||
await patchSession(opts, { status: "stopped" });
|
await patchSession(opts, { status: "stopped" });
|
||||||
@@ -203,7 +204,7 @@ Do NOT run git commit or git push — the platform handles committing after you
|
|||||||
`${toolCallsSinceText} tool call(s) over ${roundsSinceText} round(s) ` +
|
`${toolCallsSinceText} tool call(s) over ${roundsSinceText} round(s) ` +
|
||||||
"without sending the user any text. Before any more tool calls, " +
|
"without sending the user any text. Before any more tool calls, " +
|
||||||
"send ONE short sentence describing what you are currently working " +
|
"send ONE short sentence describing what you are currently working " +
|
||||||
"on and why. The user is staring at silent tool pills."
|
"on and why."
|
||||||
: "";
|
: "";
|
||||||
let resp;
|
let resp;
|
||||||
try {
|
try {
|
||||||
@@ -211,7 +212,7 @@ Do NOT run git commit or git push — the platform handles committing after you
|
|||||||
systemPrompt: scopedPrompt + extraSystem,
|
systemPrompt: scopedPrompt + extraSystem,
|
||||||
messages: history,
|
messages: history,
|
||||||
tools: config.tools,
|
tools: config.tools,
|
||||||
temperature: 0.2
|
temperature: 0.2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -221,7 +222,11 @@ Do NOT run git commit or git push — the platform handles committing after you
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resp.error) {
|
if (resp.error) {
|
||||||
await emit({ ts: now(), type: "error", text: `LLM error: ${resp.error}` });
|
await emit({
|
||||||
|
ts: now(),
|
||||||
|
type: "error",
|
||||||
|
text: `LLM error: ${resp.error}`,
|
||||||
|
});
|
||||||
await patchSession(opts, { status: "failed", error: resp.error });
|
await patchSession(opts, { status: "failed", error: resp.error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -234,8 +239,39 @@ Do NOT run git commit or git push — the platform handles committing after you
|
|||||||
roundsSinceText++;
|
roundsSinceText++;
|
||||||
toolCallsSinceText += resp.toolCalls.length;
|
toolCallsSinceText += resp.toolCalls.length;
|
||||||
}
|
}
|
||||||
|
// ── Self-Correcting Ralph Loop Autonomy ──
|
||||||
if (!resp.toolCalls.length) {
|
if (!resp.toolCalls.length) {
|
||||||
await patchSession(opts, { status: "completed" });
|
const text = resp.text || "";
|
||||||
|
const incompleteSignals = [
|
||||||
|
"I need to",
|
||||||
|
"Let me",
|
||||||
|
"Next, I should",
|
||||||
|
"I should also",
|
||||||
|
"Additionally",
|
||||||
|
"I will now",
|
||||||
|
"I need first to",
|
||||||
|
];
|
||||||
|
const needsMoreWork = incompleteSignals.some((signal) => text.includes(signal));
|
||||||
|
if (needsMoreWork && ralphIteration < 3) {
|
||||||
|
ralphIteration++;
|
||||||
|
await emit({
|
||||||
|
ts: now(),
|
||||||
|
type: "info",
|
||||||
|
text: `🔄 [Ralph Loop] Self-reflection triggered (iteration ${ralphIteration}/3). Resuming execution...`,
|
||||||
|
});
|
||||||
|
history.push({
|
||||||
|
role: "user",
|
||||||
|
content: "Please continue implementing the outstanding next steps to complete the task.",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If fully complete, trigger auto-commit and finish
|
||||||
|
if (opts.autoApprove) {
|
||||||
|
await autoCommitAndDeploy(opts, task, emit);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await patchSession(opts, { status: "completed" });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const tc of resp.toolCalls) {
|
for (const tc of resp.toolCalls) {
|
||||||
@@ -260,28 +296,119 @@ Do NOT run git commit or git push — the platform handles committing after you
|
|||||||
history.push({
|
history.push({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: resp.text,
|
content: resp.text,
|
||||||
toolCalls: resp.toolCalls
|
toolCalls: resp.toolCalls,
|
||||||
});
|
});
|
||||||
for (const tc of resp.toolCalls) {
|
// ── 4-Level Smart Concurrency Tool Grouping ──
|
||||||
await emit({ ts: now(), type: "step", text: `Running ${tc.name}...` });
|
const parallelReads = resp.toolCalls.filter((tc) => [
|
||||||
let result;
|
"fs_read",
|
||||||
try {
|
"fs_tree",
|
||||||
result = await (0, tools_1.executeTool)(tc.name, tc.args, ctx);
|
"fs_list",
|
||||||
}
|
"fs_glob",
|
||||||
catch (err) {
|
"fs_grep",
|
||||||
result = { error: err instanceof Error ? err.message : String(err) };
|
"projects_list",
|
||||||
}
|
"project_recent_errors",
|
||||||
const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
].includes(tc.name));
|
||||||
history.push({
|
const sequentialWrites = resp.toolCalls.filter((tc) => [
|
||||||
role: "tool",
|
"fs_write",
|
||||||
content: resultStr,
|
"fs_edit",
|
||||||
toolCallId: tc.id,
|
"create_file",
|
||||||
toolName: tc.name
|
"write_file",
|
||||||
|
"replace_in_file",
|
||||||
|
"apps_create",
|
||||||
|
"databases_create",
|
||||||
|
].includes(tc.name));
|
||||||
|
const otherTools = resp.toolCalls.filter((tc) => !parallelReads.includes(tc) && !sequentialWrites.includes(tc));
|
||||||
|
// Stage 1: Parallel Reads
|
||||||
|
if (parallelReads.length > 0) {
|
||||||
|
await emit({
|
||||||
|
ts: now(),
|
||||||
|
type: "step",
|
||||||
|
text: `Executing ${parallelReads.length} read operations concurrently...`,
|
||||||
});
|
});
|
||||||
|
await Promise.all(parallelReads.map(async (tc) => {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await (0, tools_1.executeTool)(tc.name, tc.args, ctx);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
result = {
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const resultStr = typeof result === "string"
|
||||||
|
? result
|
||||||
|
: JSON.stringify(result, null, 2);
|
||||||
|
history.push({
|
||||||
|
role: "tool",
|
||||||
|
content: resultStr,
|
||||||
|
toolCallId: tc.id,
|
||||||
|
toolName: tc.name,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Stage 2: Parallelizable Other Tools
|
||||||
|
if (otherTools.length > 0) {
|
||||||
|
await Promise.all(otherTools.map(async (tc) => {
|
||||||
|
await emit({
|
||||||
|
ts: now(),
|
||||||
|
type: "step",
|
||||||
|
text: `Running ${tc.name}...`,
|
||||||
|
});
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await (0, tools_1.executeTool)(tc.name, tc.args, ctx);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
result = {
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const resultStr = typeof result === "string"
|
||||||
|
? result
|
||||||
|
: JSON.stringify(result, null, 2);
|
||||||
|
history.push({
|
||||||
|
role: "tool",
|
||||||
|
content: resultStr,
|
||||||
|
toolCallId: tc.id,
|
||||||
|
toolName: tc.name,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Stage 3: Sequential User-Safe Writes/Edits
|
||||||
|
if (sequentialWrites.length > 0) {
|
||||||
|
for (const tc of sequentialWrites) {
|
||||||
|
await emit({
|
||||||
|
ts: now(),
|
||||||
|
type: "step",
|
||||||
|
text: `Writing modifications: ${tc.name}...`,
|
||||||
|
});
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await (0, tools_1.executeTool)(tc.name, tc.args, ctx);
|
||||||
|
const changedFile = extractChangedFile(tc.name, tc.args, ctx.workspaceRoot, opts.appPath);
|
||||||
|
if (changedFile) {
|
||||||
|
await patchSession(opts, { changedFile });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
result = { error: err instanceof Error ? err.message : String(err) };
|
||||||
|
}
|
||||||
|
const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
||||||
|
history.push({
|
||||||
|
role: "tool",
|
||||||
|
content: resultStr,
|
||||||
|
toolCallId: tc.id,
|
||||||
|
toolName: tc.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loopBreakReason) {
|
if (loopBreakReason) {
|
||||||
await emit({ ts: now(), type: "error", text: `Loop broken: ${loopBreakReason}` });
|
await emit({
|
||||||
|
ts: now(),
|
||||||
|
type: "error",
|
||||||
|
text: `Loop broken: ${loopBreakReason}`,
|
||||||
|
});
|
||||||
await patchSession(opts, { status: "failed", error: loopBreakReason });
|
await patchSession(opts, { status: "failed", error: loopBreakReason });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
12
vibn-agent-runner/dist/server.js
vendored
12
vibn-agent-runner/dist/server.js
vendored
@@ -95,7 +95,7 @@ function buildContext(repo) {
|
|||||||
apiToken: process.env.COOLIFY_API_TOKEN || ''
|
apiToken: process.env.COOLIFY_API_TOKEN || ''
|
||||||
},
|
},
|
||||||
mcpToken: '',
|
mcpToken: '',
|
||||||
vibnApiUrl: 'http://localhost:3000',
|
vibnApiUrl: process.env.VIBN_API_URL ?? 'https://vibnai.com',
|
||||||
memoryUpdates: []
|
memoryUpdates: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ app.get('/api/agents', (_req, res) => {
|
|||||||
});
|
});
|
||||||
const activeSessions = new Map();
|
const activeSessions = new Map();
|
||||||
app.post('/agent/execute', async (req, res) => {
|
app.post('/agent/execute', async (req, res) => {
|
||||||
const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, } = req.body;
|
const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, mcpToken, } = req.body;
|
||||||
if (!sessionId || !projectId || !appPath || !task) {
|
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;
|
return;
|
||||||
@@ -207,6 +207,14 @@ app.post('/agent/execute', async (req, res) => {
|
|||||||
}
|
}
|
||||||
// Capture repo root before scoping to appPath — needed for git commit in auto-approve
|
// Capture repo root before scoping to appPath — needed for git commit in auto-approve
|
||||||
const repoRoot = ctx.workspaceRoot;
|
const repoRoot = ctx.workspaceRoot;
|
||||||
|
// Wire the ToolContext so its tools can call back into the VIBN frontend MCP
|
||||||
|
// with the right URL and auth. buildContext() defaults these to safe values,
|
||||||
|
// but the authoritative ones come from env (VIBN_API_URL) and the frontend
|
||||||
|
// (mcpToken passed in the /agent/execute body). Without this, tools fetch
|
||||||
|
// http://localhost:3000 with no token and fail with "fetch failed".
|
||||||
|
ctx.vibnApiUrl = vibnApiUrl;
|
||||||
|
ctx.mcpToken = mcpToken ?? ctx.mcpToken;
|
||||||
|
ctx.projectId = projectId;
|
||||||
// Scope workspace to the app subdirectory so the agent works there naturally
|
// Scope workspace to the app subdirectory so the agent works there naturally
|
||||||
if (appPath) {
|
if (appPath) {
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function buildContext(repo?: string): ToolContext {
|
|||||||
apiToken: process.env.COOLIFY_API_TOKEN || ''
|
apiToken: process.env.COOLIFY_API_TOKEN || ''
|
||||||
},
|
},
|
||||||
mcpToken: '',
|
mcpToken: '',
|
||||||
vibnApiUrl: 'http://localhost:3000',
|
vibnApiUrl: process.env.VIBN_API_URL ?? 'https://vibnai.com',
|
||||||
memoryUpdates: []
|
memoryUpdates: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ const activeSessions = new Map<string, { stopped: boolean }>();
|
|||||||
app.post('/agent/execute', async (req: Request, res: Response) => {
|
app.post('/agent/execute', async (req: Request, res: Response) => {
|
||||||
const {
|
const {
|
||||||
sessionId, projectId, appName, appPath, giteaRepo, task, continueTask,
|
sessionId, projectId, appName, appPath, giteaRepo, task, continueTask,
|
||||||
autoApprove, coolifyAppUuid,
|
autoApprove, coolifyAppUuid, mcpToken,
|
||||||
} = req.body as {
|
} = req.body as {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
@@ -183,6 +183,7 @@ app.post('/agent/execute', async (req: Request, res: Response) => {
|
|||||||
continueTask?: string;
|
continueTask?: string;
|
||||||
autoApprove?: boolean;
|
autoApprove?: boolean;
|
||||||
coolifyAppUuid?: string;
|
coolifyAppUuid?: string;
|
||||||
|
mcpToken?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!sessionId || !projectId || !appPath || !task) {
|
if (!sessionId || !projectId || !appPath || !task) {
|
||||||
@@ -219,6 +220,15 @@ app.post('/agent/execute', async (req: Request, res: Response) => {
|
|||||||
// Capture repo root before scoping to appPath — needed for git commit in auto-approve
|
// Capture repo root before scoping to appPath — needed for git commit in auto-approve
|
||||||
const repoRoot = ctx.workspaceRoot;
|
const repoRoot = ctx.workspaceRoot;
|
||||||
|
|
||||||
|
// Wire the ToolContext so its tools can call back into the VIBN frontend MCP
|
||||||
|
// with the right URL and auth. buildContext() defaults these to safe values,
|
||||||
|
// but the authoritative ones come from env (VIBN_API_URL) and the frontend
|
||||||
|
// (mcpToken passed in the /agent/execute body). Without this, tools fetch
|
||||||
|
// http://localhost:3000 with no token and fail with "fetch failed".
|
||||||
|
ctx.vibnApiUrl = vibnApiUrl;
|
||||||
|
ctx.mcpToken = mcpToken ?? ctx.mcpToken;
|
||||||
|
ctx.projectId = projectId;
|
||||||
|
|
||||||
// Scope workspace to the app subdirectory so the agent works there naturally
|
// Scope workspace to the app subdirectory so the agent works there naturally
|
||||||
if (appPath) {
|
if (appPath) {
|
||||||
const path = require('path') as typeof import('path');
|
const path = require('path') as typeof import('path');
|
||||||
|
|||||||
Reference in New Issue
Block a user