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

  1. Treat the response as untrustworthy: discard it and do not act on either envelope's contents.
  2. Retry the gateway request once in case the response was corrupted in transit; if it repeats, the tampering is upstream, not transient.
  3. Investigate the relay path (gateway, proxies, MCP transport) for code that concatenates multiple tool/resource responses into one text payload.
  4. 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 five

View on GitHub (pinned to 9c61c86f06)