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

  1. Confirm the taskId came from the task_create response in the same server process — re-create the task if the process restarted
  2. List tasks (task list) and verify the exact ID before polling status
  3. 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
  4. 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

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


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)