ruvnet/ruflo · error · Error
SSRF guard: private/loopback host rejected
Error message
SSRF guard: private/loopback host rejected — ${host} What it means
task_status first tries the swarm orchestrator when one is attached to the ToolContext, and on failure falls through to the 'simple implementation': an in-memory Map<string, Task> that is only populated by task_create in the same process. If taskStore.get(input.taskId) returns undefined, it throws 'Task not found: <id>'. So the error means the ID was never created here, came from another process, or the orchestrator lookup failed and the local store had no fallback copy.
Solutions
- Confirm the taskId came from the task_create response in the same server process — re-create the task if the process restarted
- List tasks (task list) and verify the exact ID before polling status
- Check server console for 'Failed to get task status via orchestrator' — if present, the orchestrator path is broken; fix or restart the swarm coordinator so tasks are reachable there
- Keep your own mapping of logical job name -> taskId so you never hand-type IDs
Example fix
// before
await client.callTool('task_status', { taskId: 'tsk-001' }); // hand-written id -> throws [1133]
// after
const { task } = await client.callTool('task_create', { type: 'implementation', description: '...' });
await client.callTool('task_status', { taskId: task.id, includeMetrics: true }); Defensive patterns
Strategy: validation
Validate before calling
const { tasks } = await client.callTool('task_list', { status: 'all' });
const taskExists = tasks.some(t => t.id === taskId);
if (!taskExists) throw new Error(`task ${taskId} not in store — recreate before polling`); Type guard
function isLiveTaskId(id: string, knownIds: Set<string>): boolean {
return knownIds.has(id);
} Try / catch
try {
await client.callTool('task_status', { taskId });
} catch (e) {
if (e instanceof Error && e.message.startsWith('Task not found')) {
// store was reset (restart) or orchestrator fell through — recreate or stop polling
return null;
}
throw e;
} Prevention
- Capture task.id from every task_create response into a durable registry
- Reset your registry whenever the MCP server process restarts
- Watch for 'Failed to ... via orchestrator' console lines — they signal the fallback path is in use
When it happens
Trigger: Calling task_status with an ID returned by task_create in a previous MCP server run; a typo'd/truncated taskId; the orchestrator call threw (you will see 'Failed to get task status via orchestrator:' on the server console) and the local Map never contained the task; asking for a task created by a different swarm coordinator instance.
Common situations: Server restarts between creating and polling tasks (the store is process-local with no persistence); multi-client setups where one client's restart orphans another's task IDs; IDs copied from logs with formatting artifacts.
Related errors
- User not found
- Failed to create share link
- JSON.stringify(response.error)
- SSRF guard: only HTTPS URLs are permitted, got
- SSRF guard: private/loopback host rejected
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/418af34851200c5d.
Report an issue: GitHub.
Appendix: source
Thrown at ruflo/src/ruvocal/mcp-bridge/index.js:750
// =============================================================================
// SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)
// =============================================================================
const PRIVATE_IP_RE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|::1|fc|fd)/i;
function assertSafeUrl(rawUrl) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);
}
if (parsed.protocol !== "https:") {
throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);
}
const host = parsed.hostname;
if (PRIVATE_IP_RE.test(host) || host === "localhost" || host.endsWith(".local")) {
throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);
}
}
// =============================================================================
// HELPER — Call a backend Cloud Function / API
// =============================================================================
async function callCloudFunction(url, payload, timeoutMs = 25000) {
// Validate the URL before making any network request.
assertSafeUrl(url);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: controller.signal,View on GitHub (pinned to fa13ee4ad6)