ruvnet/ruflo · error · Error
x.ruv.io: untrusted-data envelope is unterminated…
Error message
x.ruv.io: untrusted-data envelope is unterminated (truncated or tampered response)
What it means
parseGatewayText throws when it counts exactly one newline-anchored `<<<UNTRUSTED_RELAY_DATA` opening marker but the matching close `<<<END_UNTRUSTED_RELAY_DATA <same-uuid>>>` never appears. A complete envelope always ends with its close; an opener without one means the response was truncated mid-envelope (transport or gateway failure) or deliberately cut/tampered with to smuggle content past the fence. Parsing the remainder would silently drop relay content, so the parser fails loudly instead.
Solutions
- Retry the gateway call — truncation is often a transient transport or size-limit issue.
- Check whether a proxy, MCP transport, or log sanitizer is cutting long responses and raise the limit or remove the culprit.
- Verify the gateway endpoint's integrity; an attacker may be truncating the close marker to escape the untrusted-data fence.
- Surface the error to the caller instead of partially consuming the payload — any content after the opener is unverified.
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-tools.ts:86 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/d9adfdb1d7650f2a.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-tools.ts:86
// 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 five
* keys, so a miscount looks like a real answer. Never do a bare property read on
* a parseGatewayText result — come through here.
*/
export function relayPayload(text: string): Record<string, unknown> {
const parsed = parseGatewayText(text);
return (parsed.untrusted === true && parsed.data !== undefined
? (parsed.data as Record<string, unknown>)View on GitHub (pinned to 9c61c86f06)