mastra-ai/mastra · error

Root escapes workspace

Error message

Root escapes workspace

What it means

listWorkspaceRenderedPath resolves an approved rendered root (e.g. '.artifacts') against the workspace directory and then checks the computed absolute path stays within the workspace. 'Root escapes workspace' is thrown when resolving the approved root name against the workspace produces a path outside the workspace — practically only possible if the approved-root allowlist or the workspace resolution changed, since safeRoot has already been validated as relative and '..'-free. It is a defense-in-depth containment check before any filesystem access.

Source

Thrown at mastracode/factory/src/routes/fs.ts:345

        type: 'file',
        size: info.size,
        updatedAt: info.mtime.toISOString(),
      });
    }
  }

  return entries.sort((a, b) => a.path.localeCompare(b.path));
}

export async function listWorkspaceRenderedPath(
  root: string,
  workspacePath: string,
  renderedRoot: string,
): Promise<WorkspaceRenderedListing> {
  const safeRoot = assertApprovedRenderedRoot(renderedRoot);
  const { workspace } = await confinedWorkspacePath(root, workspacePath);
  const renderedPath = resolve(workspace, safeRoot);
  if (!isWithinRoot(renderedPath, workspace)) throw new Error('Root escapes workspace');

  const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);
  if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };

  const info = await stat(confinedRootPath);
  if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };

  return {
    workspacePath: workspace,
    root: safeRoot,
    rootPath: confinedRootPath,
    entries: await listRenderedEntries(confinedRootPath),
  };
}

export async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {
  const safePath = assertRelativePath(path, 'path');
  const relativeRoot = safePath.split('/')[0] ?? '';

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Check the renderedRoot argument — it must be one of the approved roots (e.g. '.artifacts') and contain no '..' or absolute components.
  2. Inspect APPROVED_RENDERED_ROOTS and remove/fix any entry that could resolve outside the workspace.
  3. Confirm the workspace itself resolved correctly (confinedWorkspacePath) — an empty or odd workspacePath can make resolve() land somewhere unexpected.
  4. Ensure .artifacts (or the relevant root) is a real directory inside the workspace, not a symlink out of it.

Example fix

// before
APPROVED_RENDERED_ROOTS = new Set(['.artifacts', '../shared-out'])
// after
APPROVED_RENDERED_ROOTS = new Set(['.artifacts'])
Defensive patterns

Strategy: validation

Validate before calling

import { resolve } from 'node:path';
const APPROVED = new Set(['.artifacts']);
function isApprovedRoot(root: string): boolean {
  const t = root.trim();
  return APPROVED.has(t) && !t.split(/[\\/]+/).includes('..') && resolve('/workspace', t).startsWith('/workspace');
}
if (!isApprovedRoot(renderedRoot)) throw new Error('refusing request: root not approved');

Type guard

function isApprovedRenderedRoot(root: string): root is '.artifacts' {
  return root === '.artifacts';
}

Try / catch

try {
  const listing = await listWorkspaceRenderedPath(root, ws, renderedRoot);
} catch (e) {
  if (e instanceof Error && e.message === 'Root escapes workspace') {
    // containment violation: log and return empty listing
    return { workspacePath: ws, root: renderedRoot, entries: [] };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling listWorkspaceRenderedPath with a renderedRoot that resolves outside the workspace. With the current allowlist this is nearly unreachable from user input (assertApprovedRenderedRoot rejects non-approved roots and any '..'), so hits usually mean a modified APPROVED_RENDERED_ROOTS entry, a mutated resolve/isWithinRoot helper, or a workspace resolution inconsistency.

Common situations: A developer adding a new entry to APPROVED_RENDERED_ROOTS that contains a traversal or absolute component; a symlink at workspace/.artifacts pointing outside the workspace combined with a helper bypass; custom forks changing the confinement helpers.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/5ed6c4cb1d0d1d6f. Report an issue: GitHub.