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
- 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
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
- 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
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
- SSRF guard: private/loopback host rejected
- SSRF guard: invalid URL
- SSRF guard: only HTTPS URLs are permitted, got
- SSRF guard: private/loopback host rejected
- User not found
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)