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
- Only end trajectories you began in the current process; keep the begin response's trajectoryId and thread it through to end
- If the server restarted mid-run, begin a new trajectory and replay the important steps, then end that one
- Guard the call: skip end (and log) when your local records show the trajectory predates the current process
- 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
- Store both trajectoryId and sessionId from begin and destructure carefully — mixing them is the top cause
- End trajectories in the same finally-block scope where you began them
- Treat end-after-restart as a no-op, not an error, in long-running pipelines
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
- SSRF guard: only HTTPS URLs are permitted, got
- SSRF guard: private/loopback host rejected
- SSRF guard: only HTTPS URLs are permitted, got
- Trajectory not found
- Agent not found
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)