affaan-m/ECC · error

Refusing unsafe repair source metadata: sources must stay…

Error message

Refusing unsafe repair source metadata: sources must stay within the repository.

What it means

Error raised by assertSafeRepairSourcePath in scripts/lib/install-lifecycle.js:148 (via createUnsafeRepairSourceError). Before a repair/hydration operation reads a recorded source file, the path is checked with assertWithinTrustedRoot against the repository root. If the recorded repair source resolves outside the repo (or the trust check itself throws), the lifecycle layer refuses the read with the message that repair sources must stay within the repository, preventing operations recorded with escaped or absolute foreign paths from being replayed.

Solutions

  1. Inspect the operation record's sourceRelativePath/sourcePath and rewrite it as a path relative to the repository root.
  2. Confirm the referenced file actually exists inside the repo; re-record the operation if the original file was moved.
  3. Re-generate the operations manifest with the current ECC tooling instead of editing recorded paths by hand.
  4. If symlinks are involved, replace them with real in-repo copies so the trust check resolves within the root.

Example fix

// before (recorded operation)
{ "sourceRelativePath": "../../outside/secret-file.md" }

// after
{ "sourceRelativePath": "agents/code-reviewer.md" }
Defensive patterns

Strategy: validation

Validate before calling

const path = require('path');
function isWithinRepo(rel, repoRoot) {
  if (typeof rel !== 'string') return false;
  const resolved = path.resolve(repoRoot, rel);
  return resolved.startsWith(path.resolve(repoRoot) + path.sep);
}
if (!isWithinRepo(op.sourceRelativePath, repoRoot)) throw new Error('repair source outside repo');

Type guard

function isSafeRepairSource(op, repoRoot) {
  return typeof op.sourceRelativePath === 'string' &&
    !op.sourceRelativePath.split(/[/\\]+/).includes('..') &&
    path.resolve(repoRoot, op.sourceRelativePath).startsWith(path.resolve(repoRoot));
}

Try / catch

try { const src = resolveOperationSourcePath(repoRoot, op); }
catch (e) {
  console.error('Recorded repair source is unsafe; re-record the operation with an in-repo relative path.');
  process.exitCode = 1;
}

Prevention

When it happens

Trigger: Replaying or inspecting a recorded install/repair operation whose sourcePath fails assertWithinTrustedRoot — e.g. the recorded path is absolute outside the repo (`/etc/...`, `C:\...`), or the trust check throws (non-existent path, symlink escape) and the catch re-raises as the unsafe-repair-source error.

Common situations: A manifest/record file was hand-edited or generated by an older tool version storing absolute paths; the repo was moved/renamed so previously relative-trusted paths now resolve outside the new root; symlinked source directories pointing outside the repository.

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/4d3a5c73095f53cc. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install-lifecycle.js:148

  const stdout = typeof error.stdout === 'string' ? error.stdout.trim() : '';
  return stderr || stdout || error.message || 'Failed to build OpenCode payload';
}

function getManagedOperations(state) {
  return Array.isArray(state && state.operations) ? state.operations.filter(operation => operation.ownership === 'managed') : [];
}

function createUnsafeRepairSourceError() {
  return new Error(
    'Refusing unsafe repair source metadata: sources must stay within the repository.'
  );
}

function assertSafeRepairSourcePath(sourcePath, repoRoot) {
  try {
    return assertWithinTrustedRoot(sourcePath, repoRoot, 'read repair source');
  } catch {
    throw createUnsafeRepairSourceError();
  }
}

function resolveOperationSourcePath(repoRoot, operation) {
  if (operation.sourceRelativePath) {
    if (typeof operation.sourceRelativePath !== 'string') {
      throw createUnsafeRepairSourceError();
    }

    const sourceRelativePath = operation.sourceRelativePath;
    const hasParentTraversal = sourceRelativePath
      .split(/[/\\]+/)
      .includes('..');
    const isAbsolute = path.isAbsolute(sourceRelativePath)
      || path.win32.isAbsolute(sourceRelativePath);
    if (isAbsolute || hasParentTraversal) {
      throw createUnsafeRepairSourceError();
    }

View on GitHub (pinned to 8321021c54)