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

  1. Retry the gateway call — truncation is often a transient transport or size-limit issue.
  2. Check whether a proxy, MCP transport, or log sanitizer is cutting long responses and raise the limit or remove the culprit.
  3. Verify the gateway endpoint's integrity; an attacker may be truncating the close marker to escape the untrusted-data fence.
  4. 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)