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
- Deduplicate toolchains by `${name}\0${version}` before calling createBuildEvidence
- To change a toolchain's digest, update the existing (name, version) entry rather than adding a duplicate
- 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
- Key toolchain config by `${name}@${version}` so duplicates are structurally impossible
- Layer presets with object spreads keyed by name+version, not array concatenation
- Treat a duplicate (name, version) as an upsert, never an append
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
- duplicate declared build input
- ${label} must be non-empty
- ${label} must be a canonical sha256 digest
- build evidence path is not a file or symlink: ${path}
- unsafe build input path: ${value}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/d22d8691988bf6d7.
Report an issue: GitHub.