affaan-m/ECC · error · GateError

gate.variant_invalid

gate.variant_invalid

Error message

variant trees must contain only regular files and directories

What it means

listFiles() walks a variant tree and rejects anything that is not a regular file or directory — most importantly symbolic links, but also sockets, FIFOs, and device nodes — with 'gate.variant_invalid'. Symlinks are forbidden because variants are content-addressed by digest; a symlink could escape the tree (path traversal) or make the digest non-reproducible. The same guard backs digestDir, loadVariant, and scanTripwires.

Solutions

  1. Find the offending entry with: find <variant-dir> -type l -o ! -type f -o ! -type d (excluding node_modules/.git).
  2. Replace symlinks with real copies of their targets (cp -L) or remove them if unnecessary.
  3. Delete stray sockets/FIFOs (e.g. leftover *.sock files) from the tree.
  4. Re-copy the variant tree with dereferencing (rsync -L or cp -rL) so only regular files/dirs remain.

Example fix

// before: variant dir contains a symlink to shared fixtures
const variant = loadVariant('./variants/base'); // gate.variant_invalid

// after: materialize links as real files first
// cp -rL ./variants/base ./variants/base-resolved && rm -rf ./variants/base-resolved/node_modules
const variant = loadVariant('./variants/base-resolved');
Defensive patterns

Strategy: validation

Validate before calling

const { execSync } = require('child_process');
const links = execSync(`find ${dir} -type l -not -path '*/node_modules/*' -not -path '*/.git/*'`).toString().trim();
if (links) throw new Error(`symlinks in variant tree must be resolved first:\n${links}`);

Type guard

function isRegularTree(dir) {
  for (const e of fs.readdirSync(dir, { withFileTypes: true, recursive: true })) {
    if (e.name === 'node_modules' || e.name === '.git') continue;
    if (e.isSymbolicLink() || (!e.isFile() && !e.isDirectory())) return false;
  }
  return true;
}

Try / catch

try {
  const files = listFiles(variantDir, variantDir, []);
} catch (e) {
  if (e instanceof GateError && e.code === 'gate.variant_invalid') {
    throw new Error(`${e.message}; fix with: find ${variantDir} -type l -exec cp -L {} {}.real \\; -exec mv {}.real {} \\;`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling loadVariant/gate on a variant directory containing a symlink (e.g. node_modules-style links, a symlinked config, or .git worktree links not skipped), a Unix socket left by a dev server, or a named pipe inside the tree.

Common situations: Checking out fixtures via a tool that creates symlinks; copying variant trees with cp -r that preserved links; running a dev server in the variant dir that left a .sock file; pnpm-style symlinked node_modules layouts (only node_modules and .git are skipped, not other link farms).

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/503924edb097cc4c. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/eval-harness/gate.js:62

]);

class GateError extends Error {
  constructor(code, message, details = {}) {
    super(message);
    this.name = 'GateError';
    this.code = code;
    Object.assign(this, details);
  }
}

function listFiles(dir, base = dir, acc = []) {
  for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
    if (entry.name === 'node_modules' || entry.name === '.git') {
      continue;
    }
    const full = path.join(dir, entry.name);
    if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {
      throw new GateError('gate.variant_invalid', 'variant trees must contain only regular files and directories');
    }
    if (entry.isDirectory()) {
      listFiles(full, base, acc);
    } else if (entry.isFile()) {
      acc.push(path.relative(base, full).split(path.sep).join('/'));
    }
  }
  return acc;
}

/** Read the opened regular file, never reopen a previously checked pathname.
 * No-follow/nonblocking flags reduce symlink and special-file hazards where
 * supported. Descriptor/path identity also rejects symlinks on other hosts.
 * This is static inspection of a caller-controlled tree, not OS containment.
 */
function readRegularFile(filePath, encoding) {
  const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0);
  let fd;

View on GitHub (pinned to 8321021c54)