ruvnet/ruflo · error · Error

SSRF guard: invalid URL

Error message

SSRF guard: invalid URL — ${rawUrl}

What it means

handleTrajectoryEnd finalizes a trajectory: it stamps endedAt and the caller's verdict, computes duration/step metrics, and triggers learning if requested. The lookup state.trajectories.get(input.trajectoryId) must find the trajectory in the current process's in-memory Map, otherwise 'Trajectory <id> not found' is thrown and nothing is finalized. Ending is the terminal call for a trajectory lifecycle that must have started with begin in the same process.

Solutions

  1. Only end trajectories you began in the current process; keep the begin response's trajectoryId and thread it through to end
  2. If the server restarted mid-run, begin a new trajectory and replay the important steps, then end that one
  3. Guard the call: skip end (and log) when your local records show the trajectory predates the current process
  4. Make sure you pass trajectoryId, not sessionId — begin returns both and mixing them up is a common cause

Example fix

// before
await client.callTool('sona_trajectory_end', { trajectoryId: sessionId, verdict: 'success' }); // wrong id kind -> throws [1131]

// after
const { trajectoryId, sessionId } = await client.callTool('sona_trajectory_begin', { sessionId });
// ... steps ...
await client.callTool('sona_trajectory_end', { trajectoryId, verdict: 'success', triggerLearning: true });
Defensive patterns

Strategy: validation

Validate before calling

function shouldEndTrajectory(trajId: string, begunIds: Set<string>): boolean {
  return begunIds.has(trajId);
}
// call only when shouldEndTrajectory(id) is true; otherwise skip end (nothing to finalize)

Try / catch

try {
  await client.callTool('sona_trajectory_end', { trajectoryId, verdict });
} catch (e) {
  if (e instanceof Error && e.message.includes('Trajectory') && e.message.includes('not found')) {
    return { completed: false, reason: 'trajectory predates current process; nothing to finalize' };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling sona_trajectory_end with an ID from before a server restart; ending an ID that was never begun (e.g. fabricated or from documentation examples like 'traj_123'); double-ending after the Map was cleared; passing the sessionId field instead of trajectoryId.

Common situations: Agent pipelines that span deployments and try to close old trajectories in a new process; log-replay tooling replaying end calls without the matching begins; tests that only exercise the end handler.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/1ebf251b0af49b7f. Report an issue: GitHub.

Appendix: source

Thrown at ruflo/src/ruvocal/mcp-bridge/index.js:743

    };
  } catch (err) {
    if (err.name === "AbortError" || err.name === "TimeoutError") return { error: "Search timed out" };
    return { error: err.message };
  }
}

// =============================================================================
// 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();

View on GitHub (pinned to fa13ee4ad6)