ruvnet/ruflo · error · Error
x.ruv.io: response carries more than one untrusted-data…
Error message
x.ruv.io: response carries more than one untrusted-data envelope (tampered response)
What it means
parseGatewayText rejects gateway responses from x.ruv.io that contain more than one newline-anchored opening marker `<<<UNTRUSTED_RELAY_DATA <uuid>>>`. A well-formed relay response carries exactly one untrusted-data envelope; seeing two means an upstream hop spliced an envelope-shaped string into the response (a tampered or maliciously crafted payload). Choosing either envelope would be a guess with security implications, so the parser refuses rather than picks one. The count is newline-anchored so a publisher merely typing the marker inside a message body cannot trigger a denial of service.
Solutions
- Treat the response as untrustworthy: discard it and do not act on either envelope's contents.
- Retry the gateway request once in case the response was corrupted in transit; if it repeats, the tampering is upstream, not transient.
- Investigate the relay path (gateway, proxies, MCP transport) for code that concatenates multiple tool/resource responses into one text payload.
- If this fires repeatedly for one endpoint, block or flag that gateway URL as compromised and require operator review.
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-tools.ts:79 when the library encounters an invalid state.
Common situations: See trigger scenarios.
AI-assisted analysis of ruvnet/ruflo@9c61c86f06 (2026-09-22).
Data as JSON: /api/errors/b5ecc95324adabd9.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-tools.ts:79
* errors) parse unchanged.
*/
const UNTRUSTED_FENCE =
/<<<UNTRUSTED_RELAY_DATA ([0-9a-fA-F-]{36})>>>\n([\s\S]*?)\n<<<END_UNTRUSTED_RELAY_DATA \1>>>/;
// Count only NEWLINE-ANCHORED opening markers. A marker inside the body is just
// characters — the body is one JSON line, so it can never be preceded by a raw
// newline and can never open a fence. Counting raw occurrences instead would make
// a publisher able to hard-fail every read simply by typing the marker into a
// message, which trades a parse bug for a denial of service.
const OPEN_MARKER_ANCHORED = /(?:^|\n)<<<UNTRUSTED_RELAY_DATA /g;
export function parseGatewayText(text: string): Record<string, unknown> {
// One response carries exactly one envelope. More than one means something
// upstream spliced an envelope-shaped string into the response, and picking
// either is a guess — refuse rather than choose.
const opens = (text.match(OPEN_MARKER_ANCHORED) ?? []).length;
if (opens > 1) {
throw new Error('x.ruv.io: response carries more than one untrusted-data envelope (tampered response)');
}
const fenced = UNTRUSTED_FENCE.exec(text);
if (fenced) return JSON.parse(fenced[2]) as Record<string, unknown>;
// An opening marker with no matching close is a truncated or tampered response.
// Fail loudly: parsing the remainder would silently drop relay content.
if (opens === 1) {
throw new Error('x.ruv.io: untrusted-data envelope is unterminated (truncated or tampered response)');
}
return JSON.parse(text) as Record<string, unknown>;
}
/**
* The payload, for consumers INSIDE this package that immediately index the
* value (`recent.messages`, `Object.keys(roster)`).
*
* At the MCP boundary we return the whole envelope so the caller can see whose
* words these are. Internally that shape is a hazard: reading `.messages` off an
* envelope yields undefined and `Object.keys()` yields the envelope's own fiveView on GitHub (pinned to 9c61c86f06)