ruvnet/ruflo · error

duplicate declared toolchain

Error message

duplicate declared toolchain

What it means

Toolchain declarations are keyed by the pair name\0version; declaring the same toolchain name and version twice throws. Two entries with the same name but different versions are fine — the collision is only the exact (name, version) pair, since each pair binds one digest in the hashed evidence.

Source

Thrown at v3/@claude-flow/codex/src/harness/build-evidence.ts:129

    bytes: input.bytes,
  })).sort((left, right) => compare(left.path, right.path) || compare(left.name, right.name));
  const tools = toolchains.map((toolchain) => ({
    name: requireText(toolchain.name, 'toolchain name'),
    version: requireText(toolchain.version, 'toolchain version'),
    digest: requireDigest(toolchain.digest, 'toolchain digest'),
  })).sort((left, right) => compare(left.name, right.name) || compare(left.version, right.version));

  if (inputs.some(({ bytes }) => !Number.isSafeInteger(bytes) || bytes < 0)) {
    throw new Error('build input bytes must be a non-negative safe integer');
  }
  const inputKeys = inputs.map(({ name, path }) => `${name}\0${path}`);
  if (new Set(inputKeys).size !== inputKeys.length) throw new Error('duplicate declared build input');
  const foldedPaths = inputs.map(({ path }) => portableCaseFold(path));
  if (new Set(foldedPaths).size !== foldedPaths.length) {
    throw new Error('case-fold collision in declared build inputs');
  }
  const toolKeys = tools.map(({ name, version }) => `${name}\0${version}`);
  if (new Set(toolKeys).size !== toolKeys.length) throw new Error('duplicate declared toolchain');

  const body = {
    contractVersion: 1 as const,
    assurance: 'declared-unsigned' as const,
    sourceStateId: requireDigest(sourceState.sourceStateId, 'source state id'),
    buildInputs: inputs,
    toolchains: tools,
  };
  return { ...body, evidenceDigest: sha256(canonicalJson(body)) };
}

/**
 * Recompute declared evidence from local bytes. It does not prove the
 * declaration set is complete and does not sign or authorize a release.
 */
export function captureBuildEvidence(
  repoPath: string,
  sourceState: ExactSourceState,

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Deduplicate toolchains by `${name}\0${version}` before calling createBuildEvidence
  2. To change a toolchain's digest, update the existing (name, version) entry rather than adding a duplicate
  3. Merge toolchain lists with a Map so later entries intentionally replace earlier ones

Example fix

// before
const toolchains = [
  { name: 'node', version: '22.1.0', path: '/usr/bin/node' },
  { name: 'node', version: '22.1.0', path: '/usr/local/bin/node' }, // duplicate pair
];

// after
const toolchains = [...new Map(
  rawTools.map((t) => [`${t.name}\0${t.version}`, t]),
).values()];
Defensive patterns

Strategy: validation

Validate before calling

function dedupeToolchains<T extends { name: string; version: string }>(tools: T[]): T[] {
  return [...new Map(tools.map((t) => [`${t.name}\0${t.version}`, t])).values()];
}

Prevention

When it happens

Trigger: The same { name: 'node', version: '22.1.0' } declaration appearing in both a preset toolchain list and a user override list that were concatenated; copy-pasted toolchain blocks.

Common situations: Layering base and user config without merging toolchain arrays; regenerating declarations by appending instead of upserting; bundling toolchains from multiple build scripts.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/d22d8691988bf6d7. Report an issue: GitHub.