ruvnet/ruflo · error · Error

SSRF guard: only HTTPS URLs are permitted, got

Error message

SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}

What it means

SONA trajectories live in an in-memory Map on the SONAState singleton, populated only by sona_trajectory_begin. handleTrajectoryStep looks up input.trajectoryId in that Map and throws this error when it is absent — meaning the ID was never begun in this process, was already ended (end removes finalization context), or belongs to a previous server process. There is no disk persistence for trajectories.

Solutions

  1. Always capture trajectoryId from the sona_trajectory_begin response and use that exact value for step/context/end
  2. Keep the begin -> step -> end lifecycle inside one MCP server process; if the server restarts, begin a new trajectory instead of reusing the old ID
  3. Verify the ID exists by checking the SONA status stats (trajectoryCount) or your own registry of begun IDs before stepping
  4. If you need cross-process trajectories, persist step data yourself — the built-in Map is process-local by design

Example fix

// before
await client.callTool('sona_trajectory_step', { trajectoryId: staleIdFromYesterday, action: 'deploy', observation: 'ok' }); // throws [1129]

// after
const { trajectoryId } = await client.callTool('sona_trajectory_begin', {});
await client.callTool('sona_trajectory_step', { trajectoryId, action: 'deploy', observation: 'ok' });
Defensive patterns

Strategy: validation

Validate before calling

const activeTrajectories = new Set<string>(); // your registry
function registerTrajectory(id: string) { activeTrajectories.add(id); }
function isKnownTrajectory(id: string): boolean { return activeTrajectories.has(id); }

Type guard

function isLiveTrajectoryId(id: string): boolean {
  return /^traj_[a-z0-9]+_[a-z0-9]+$/.test(id) && isKnownTrajectory(id);
}

Try / catch

try {
  await client.callTool('sona_trajectory_step', { trajectoryId, action, observation });
} catch (e) {
  if (e instanceof Error && e.message.includes('not found')) {
    const { trajectoryId: fresh } = await client.callTool('sona_trajectory_begin', {});
    trajectoryId = fresh; // restart lifecycle, then retry once
    return client.callTool('sona_trajectory_step', { trajectoryId, action, observation });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling sona_trajectory_step with an ID from a previous MCP server run (state lost on restart); a typo'd or truncated trajectoryId; calling step after the process that began the trajectory exited; interleaving clients where one restarts the server mid-run.

Common situations: Long agent workflows spanning server restarts or deployments; passing IDs across process boundaries (worker -> orchestrator); copy-paste of IDs from old logs.

Related errors


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

Appendix: source

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

  return { guidance: `Unknown topic '${topic}'. Use 'overview', 'groups', or a specific group name.`, topic };
}

// =============================================================================
// 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, {

View on GitHub (pinned to fa13ee4ad6)