{"record":{"id":"fdfda431f2904f87","repo":"ruvnet/ruflo","slug":"ssrf-guard-only-https-urls-are-permitted-got-p","errorCode":null,"errorMessage":"SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}","messagePattern":"SSRF guard: only HTTPS URLs are permitted, got (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/mcp-bridge/index.js","lineNumber":657,"sourceCode":"\n  return { guidance: `Unknown topic '${topic}'. Use 'overview', 'groups', or a specific group name.`, topic };\n}\n\n// =============================================================================\n// SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)\n// =============================================================================\n\nconst PRIVATE_IP_RE = /^(?:10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|192\\.168\\.|127\\.|0\\.|::1|fc|fd)/i;\n\nfunction assertSafeUrl(rawUrl) {\n  let parsed;\n  try {\n    parsed = new URL(rawUrl);\n  } catch {\n    throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);\n  }\n  if (parsed.protocol !== \"https:\") {\n    throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);\n  }\n  const host = parsed.hostname;\n  if (PRIVATE_IP_RE.test(host) || host === \"localhost\" || host.endsWith(\".local\")) {\n    throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);\n  }\n}\n\n// =============================================================================\n// HELPER — Call a backend Cloud Function / API\n// =============================================================================\n\nasync function callCloudFunction(url, payload, timeoutMs = 25000) {\n  // Validate the URL before making any network request.\n  assertSafeUrl(url);\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n  try {\n    const resp = await fetch(url, {","sourceCodeStart":639,"sourceCodeEnd":675,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/ruflo/src/mcp-bridge/index.js#L639-L675","documentation":"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.","triggerScenarios":"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.","commonSituations":"Long agent workflows spanning server restarts or deployments; passing IDs across process boundaries (worker -> orchestrator); copy-paste of IDs from old logs.","solutions":["Always capture trajectoryId from the sona_trajectory_begin response and use that exact value for step/context/end","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","Verify the ID exists by checking the SONA status stats (trajectoryCount) or your own registry of begun IDs before stepping","If you need cross-process trajectories, persist step data yourself — the built-in Map is process-local by design"],"exampleFix":"// before\nawait client.callTool('sona_trajectory_step', { trajectoryId: staleIdFromYesterday, action: 'deploy', observation: 'ok' }); // throws [1129]\n\n// after\nconst { trajectoryId } = await client.callTool('sona_trajectory_begin', {});\nawait client.callTool('sona_trajectory_step', { trajectoryId, action: 'deploy', observation: 'ok' });","handlingStrategy":"validation","validationCode":"const activeTrajectories = new Set<string>(); // your registry\nfunction registerTrajectory(id: string) { activeTrajectories.add(id); }\nfunction isKnownTrajectory(id: string): boolean { return activeTrajectories.has(id); }","typeGuard":"function isLiveTrajectoryId(id: string): boolean {\n  return /^traj_[a-z0-9]+_[a-z0-9]+$/.test(id) && isKnownTrajectory(id);\n}","tryCatchPattern":"try {\n  await client.callTool('sona_trajectory_step', { trajectoryId, action, observation });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('not found')) {\n    const { trajectoryId: fresh } = await client.callTool('sona_trajectory_begin', {});\n    trajectoryId = fresh; // restart lifecycle, then retry once\n    return client.callTool('sona_trajectory_step', { trajectoryId, action, observation });\n  }\n  throw e;\n}","preventionTips":["Thread the begin() response's trajectoryId through every subsequent call — never retype it","Keep the whole trajectory lifecycle in one server process; re-begin after restarts","Prefix-check IDs: traj_* for trajectories, step_* and session_* are different namespaces"],"tags":["mcp","sona","trajectory","not-found","in-memory-state"],"backgroundTag":"trajectory-not-found","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}