@mastra/core
Version:
Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.
1,509 lines (1,499 loc) • 371 kB
JavaScript
'use strict';
var chunkDEZHPHA5_cjs = require('./chunk-DEZHPHA5.cjs');
var chunkFCQNDFEW_cjs = require('./chunk-FCQNDFEW.cjs');
var chunk7GW2TQXP_cjs = require('./chunk-7GW2TQXP.cjs');
var chunkCAVARKYS_cjs = require('./chunk-CAVARKYS.cjs');
var nodePath = require('path');
var pMap = require('p-map');
var posixPath = require('path/posix');
var fs = require('fs');
var fs2 = require('fs/promises');
var os3 = require('os');
var picomatch = require('picomatch');
var module$1 = require('module');
var url = require('url');
var child_process = require('child_process');
var crypto = require('crypto');
var string_decoder = require('string_decoder');
var stream = require('stream');
var matter = require('gray-matter');
var v4 = require('zod/v4');
var tokenx = require('tokenx');
var ignore = require('ignore');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var nodePath__namespace = /*#__PURE__*/_interopNamespace(nodePath);
var pMap__default = /*#__PURE__*/_interopDefault(pMap);
var posixPath__default = /*#__PURE__*/_interopDefault(posixPath);
var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
var os3__namespace = /*#__PURE__*/_interopNamespace(os3);
var picomatch__default = /*#__PURE__*/_interopDefault(picomatch);
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
var matter__default = /*#__PURE__*/_interopDefault(matter);
var ignore__default = /*#__PURE__*/_interopDefault(ignore);
// src/workspace/errors.ts
var WorkspaceError = class extends Error {
constructor(message, code, workspaceId) {
super(message);
this.code = code;
this.workspaceId = workspaceId;
this.name = "WorkspaceError";
}
code;
workspaceId;
};
var WorkspaceNotAvailableError = class extends WorkspaceError {
constructor() {
super("Workspace not available. Ensure the agent has a workspace configured.", "NO_WORKSPACE");
this.name = "WorkspaceNotAvailableError";
}
};
var FilesystemNotAvailableError = class extends WorkspaceError {
constructor() {
super("Workspace does not have a filesystem configured", "NO_FILESYSTEM");
this.name = "FilesystemNotAvailableError";
}
};
var SandboxNotAvailableError = class extends WorkspaceError {
constructor(message) {
super(message ?? "Workspace does not have a sandbox configured", "NO_SANDBOX");
this.name = "SandboxNotAvailableError";
}
};
var SandboxFeatureNotSupportedError = class extends WorkspaceError {
constructor(feature) {
super(`Sandbox does not support ${feature}`, "FEATURE_NOT_SUPPORTED");
this.name = "SandboxFeatureNotSupportedError";
}
};
var SearchNotAvailableError = class extends WorkspaceError {
constructor() {
super("Workspace does not have search configured (enable bm25 or provide vectorStore + embedder)", "NO_SEARCH");
this.name = "SearchNotAvailableError";
}
};
var WorkspaceNotReadyError = class extends WorkspaceError {
constructor(workspaceId, status) {
super(`Workspace is not ready (status: ${status})`, "NOT_READY", workspaceId);
this.name = "WorkspaceNotReadyError";
}
};
var WorkspaceReadOnlyError = class extends WorkspaceError {
constructor(operation) {
super(`Workspace is in read-only mode. Cannot perform: ${operation}`, "READ_ONLY");
this.name = "WorkspaceReadOnlyError";
}
};
var FilesystemError = class extends Error {
constructor(message, code, path9) {
super(message);
this.code = code;
this.path = path9;
this.name = "FilesystemError";
}
code;
path;
};
var FileNotFoundError = class extends FilesystemError {
constructor(path9) {
super(`File not found: ${path9}`, "ENOENT", path9);
this.name = "FileNotFoundError";
}
};
var DirectoryNotFoundError = class extends FilesystemError {
constructor(path9) {
super(`Directory not found: ${path9}`, "ENOENT", path9);
this.name = "DirectoryNotFoundError";
}
};
var FileExistsError = class extends FilesystemError {
constructor(path9) {
super(`File already exists: ${path9}`, "EEXIST", path9);
this.name = "FileExistsError";
}
};
var IsDirectoryError = class extends FilesystemError {
constructor(path9) {
super(`Path is a directory: ${path9}`, "EISDIR", path9);
this.name = "IsDirectoryError";
}
};
var NotDirectoryError = class extends FilesystemError {
constructor(path9) {
super(`Path is not a directory: ${path9}`, "ENOTDIR", path9);
this.name = "NotDirectoryError";
}
};
var DirectoryNotEmptyError = class extends FilesystemError {
constructor(path9) {
super(`Directory not empty: ${path9}`, "ENOTEMPTY", path9);
this.name = "DirectoryNotEmptyError";
}
};
var PermissionError = class extends FilesystemError {
constructor(path9, operation) {
super(`Permission denied: ${operation} on ${path9}`, "EACCES", path9);
this.operation = operation;
this.name = "PermissionError";
}
operation;
};
var FileReadRequiredError = class extends FilesystemError {
constructor(path9, reason) {
super(reason, "EREAD_REQUIRED", path9);
this.name = "FileReadRequiredError";
}
};
var StaleFileError = class extends FilesystemError {
constructor(path9, expectedMtime, actualMtime) {
super(
`File was modified externally: ${path9} (expected mtime ${expectedMtime.toISOString()}, actual ${actualMtime.toISOString()})`,
"ESTALE",
path9
);
this.expectedMtime = expectedMtime;
this.actualMtime = actualMtime;
this.name = "StaleFileError";
}
expectedMtime;
actualMtime;
};
var FilesystemNotReadyError = class extends FilesystemError {
constructor(id) {
super(`Filesystem "${id}" is not ready. Call init() first or use ensureReady().`, "ENOTREADY", id);
this.name = "FilesystemNotReadyError";
}
};
// src/workspace/lifecycle.ts
async function callLifecycle(provider, method) {
const wrapped = `_${method}`;
const wrappedFn = provider[wrapped];
if (typeof wrappedFn === "function") {
await wrappedFn.call(provider);
} else {
const plainFn = provider[method];
if (typeof plainFn === "function") {
await plainFn.call(provider);
}
}
}
// src/workspace/filesystem/composite-filesystem.ts
var CompositeFilesystem = class {
id;
name = "CompositeFilesystem";
provider = "composite";
readOnly;
status = "ready";
_mounts;
constructor(config) {
this.id = `cfs-${Date.now().toString(36)}`;
this._mounts = /* @__PURE__ */ new Map();
for (const [path9, fs6] of Object.entries(config.mounts)) {
const normalized = this.normalizePath(path9);
this._mounts.set(normalized, fs6);
}
if (this._mounts.size === 0) {
throw new Error("CompositeFilesystem requires at least one mount");
}
this.readOnly = [...this._mounts.values()].every((fs6) => fs6.readOnly) || void 0;
const mountPaths = [...this._mounts.keys()];
for (const a of mountPaths) {
for (const b of mountPaths) {
if (a !== b && b.startsWith(a + "/")) {
throw new Error(`Nested mount paths are not supported: "${b}" is nested under "${a}"`);
}
}
}
}
/**
* Get all mount paths.
*/
get mountPaths() {
return Array.from(this._mounts.keys());
}
/**
* Get the mounts map.
* Returns a typed map where `get()` preserves the concrete filesystem type per mount path.
*/
get mounts() {
return this._mounts;
}
/**
* Get status and metadata for this composite filesystem.
* Includes info from each mounted filesystem in `metadata.mounts`.
*/
async getInfo() {
const mounts = {};
for (const [mountPath, fs6] of this._mounts) {
mounts[mountPath] = await fs6.getInfo?.() ?? null;
}
return {
id: this.id,
name: this.name,
provider: this.provider,
status: this.status,
readOnly: this.readOnly,
metadata: { mounts }
};
}
/**
* Get the underlying filesystem for a given path.
* Returns undefined if the path doesn't resolve to any mount.
*/
getFilesystemForPath(path9) {
const resolved = this.resolveMount(path9);
return resolved?.fs;
}
/**
* Get the mount path for a given path.
* Returns undefined if the path doesn't resolve to any mount.
*/
getMountPathForPath(path9) {
const resolved = this.resolveMount(path9);
return resolved?.mountPath;
}
/**
* Resolve a workspace-relative path to an absolute disk path.
* Strips the mount prefix and delegates to the underlying filesystem.
*/
resolveAbsolutePath(path9) {
const r = this.resolveMount(path9);
if (!r) return void 0;
return r.fs.resolveAbsolutePath?.(r.fsPath);
}
normalizePath(path9) {
if (!path9 || path9 === "/" || path9 === ".") return "/";
let n = posixPath__default.default.normalize(path9);
if (n === ".") return "/";
if (!n.startsWith("/")) n = `/${n}`;
if (n.length > 1 && n.endsWith("/")) n = n.slice(0, -1);
return n;
}
resolveMount(path9) {
const normalized = this.normalizePath(path9);
let best = null;
for (const [mountPath, fs6] of this._mounts) {
if (normalized === mountPath || normalized.startsWith(mountPath + "/")) {
if (!best || mountPath.length > best.mountPath.length) {
best = { mountPath, fs: fs6 };
}
}
}
if (!best) return null;
let fsPath = normalized.slice(best.mountPath.length);
if (fsPath === "/") fsPath = "";
else if (fsPath.startsWith("/")) fsPath = fsPath.slice(1);
return { fs: best.fs, fsPath, mountPath: best.mountPath };
}
getVirtualEntries(path9) {
const normalized = this.normalizePath(path9);
if (this.resolveMount(normalized)) return null;
const entriesMap = /* @__PURE__ */ new Map();
for (const [mountPath, fs6] of this._mounts.entries()) {
const isUnder = normalized === "/" ? mountPath.startsWith("/") : mountPath.startsWith(normalized + "/");
if (isUnder) {
const remaining = normalized === "/" ? mountPath.slice(1) : mountPath.slice(normalized.length + 1);
const next = remaining.split("/")[0];
if (next && !entriesMap.has(next)) {
const isDirectMount = remaining === next;
const entry = { name: next, type: "directory" };
if (isDirectMount) {
entry.mount = {
provider: fs6.provider,
icon: fs6.icon,
displayName: fs6.displayName,
description: fs6.description,
status: fs6.status,
error: fs6.error
};
}
entriesMap.set(next, entry);
}
}
}
return entriesMap.size > 0 ? Array.from(entriesMap.values()) : null;
}
isVirtualPath(path9) {
const normalized = this.normalizePath(path9);
if (normalized === "/" && !this._mounts.has("/")) return true;
for (const mountPath of this._mounts.keys()) {
if (mountPath.startsWith(normalized + "/")) return true;
}
return false;
}
/**
* Assert that a filesystem is writable (not read-only).
* @throws {PermissionError} if the filesystem is read-only
*/
assertWritable(fs6, path9, operation) {
if (fs6.readOnly) {
throw new PermissionError(path9, `${operation} (filesystem is read-only)`);
}
}
// ===========================================================================
// WorkspaceFilesystem Implementation
// ===========================================================================
async init() {
this.status = "initializing";
for (const [mountPath, fs6] of this._mounts.entries()) {
try {
await callLifecycle(fs6, "init");
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
console.warn(`[CompositeFilesystem] Mount "${mountPath}" failed to initialize: ${message}`);
}
}
this.status = "ready";
}
async destroy() {
this.status = "destroying";
const errors = [];
for (const fs6 of this._mounts.values()) {
try {
await callLifecycle(fs6, "destroy");
} catch (e) {
errors.push(e instanceof Error ? e : new Error(String(e)));
}
}
if (errors.length > 0) {
this.status = "error";
throw new AggregateError(errors, "Some filesystems failed to destroy");
}
this.status = "destroyed";
}
async readFile(path9, options) {
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
return r.fs.readFile(r.fsPath, options);
}
async writeFile(path9, content, options) {
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
this.assertWritable(r.fs, path9, "writeFile");
return r.fs.writeFile(r.fsPath, content, options);
}
async appendFile(path9, content) {
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
this.assertWritable(r.fs, path9, "appendFile");
return r.fs.appendFile(r.fsPath, content);
}
async deleteFile(path9, options) {
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
this.assertWritable(r.fs, path9, "deleteFile");
return r.fs.deleteFile(r.fsPath, options);
}
async copyFile(src, dest, options) {
const srcR = this.resolveMount(src);
const destR = this.resolveMount(dest);
if (!srcR) throw new Error(`No mount for source: ${src}`);
if (!destR) throw new Error(`No mount for dest: ${dest}`);
this.assertWritable(destR.fs, dest, "copyFile");
if (srcR.mountPath === destR.mountPath) {
return srcR.fs.copyFile(srcR.fsPath, destR.fsPath, options);
}
const content = await srcR.fs.readFile(srcR.fsPath);
await destR.fs.writeFile(destR.fsPath, content, { overwrite: options?.overwrite });
}
async moveFile(src, dest, options) {
const srcR = this.resolveMount(src);
const destR = this.resolveMount(dest);
if (!srcR) throw new Error(`No mount for source: ${src}`);
if (!destR) throw new Error(`No mount for dest: ${dest}`);
this.assertWritable(destR.fs, dest, "moveFile");
this.assertWritable(srcR.fs, src, "moveFile");
if (srcR.mountPath === destR.mountPath) {
return srcR.fs.moveFile(srcR.fsPath, destR.fsPath, options);
}
await this.copyFile(src, dest, options);
await srcR.fs.deleteFile(srcR.fsPath);
}
async readdir(path9, options) {
const virtual = this.getVirtualEntries(path9);
if (virtual) return virtual;
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
return r.fs.readdir(r.fsPath, options);
}
async mkdir(path9, options) {
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
this.assertWritable(r.fs, path9, "mkdir");
return r.fs.mkdir(r.fsPath, options);
}
async rmdir(path9, options) {
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
this.assertWritable(r.fs, path9, "rmdir");
return r.fs.rmdir(r.fsPath, options);
}
async exists(path9) {
if (this.isVirtualPath(path9)) return true;
const r = this.resolveMount(path9);
if (!r) return false;
if (r.fsPath === "") return true;
return r.fs.exists(r.fsPath);
}
async stat(path9) {
const normalized = this.normalizePath(path9);
if (this.isVirtualPath(path9)) {
const parts = normalized.split("/").filter(Boolean);
const now = /* @__PURE__ */ new Date();
return {
name: parts[parts.length - 1] || "",
path: normalized,
type: "directory",
size: 0,
createdAt: now,
modifiedAt: now
};
}
const r = this.resolveMount(path9);
if (!r) throw new Error(`No mount for path: ${path9}`);
if (r.fsPath === "") {
const parts = normalized.split("/").filter(Boolean);
const now = /* @__PURE__ */ new Date();
return {
name: parts[parts.length - 1] || "",
path: normalized,
type: "directory",
size: 0,
createdAt: now,
modifiedAt: now
};
}
return r.fs.stat(r.fsPath);
}
async isFile(path9) {
if (this.isVirtualPath(path9)) return false;
const r = this.resolveMount(path9);
if (!r) return false;
try {
const stat4 = await r.fs.stat(r.fsPath);
return stat4.type === "file";
} catch {
return false;
}
}
async isDirectory(path9) {
if (this.isVirtualPath(path9)) return true;
const r = this.resolveMount(path9);
if (!r) return false;
if (r.fsPath === "") return true;
try {
const stat4 = await r.fs.stat(r.fsPath);
return stat4.type === "directory";
} catch {
return false;
}
}
/**
* Get instructions describing the mounted filesystems.
* Used by agents to understand available storage locations.
*/
getInstructions(_opts) {
const mountDescriptions = Array.from(this._mounts.entries()).map(([mountPath, fs6]) => {
const name = fs6.displayName || fs6.provider;
const access3 = fs6.readOnly ? "(read-only)" : "(read-write)";
return `- ${mountPath}: ${name} ${access3}`;
}).join("\n");
return `Filesystem mount points:
${mountDescriptions}`;
}
};
// src/workspace/filesystem/mastra-filesystem.ts
var MastraFilesystem = class extends chunkFCQNDFEW_cjs.MastraBase {
/** Error message when status is 'error' */
error;
// ---------------------------------------------------------------------------
// Lifecycle Promise Tracking (prevents race conditions)
// ---------------------------------------------------------------------------
/** Promise for _init() to prevent race conditions from concurrent calls */
_initPromise;
/** Promise for _destroy() to prevent race conditions from concurrent calls */
_destroyPromise;
/** Lifecycle callbacks */
_onInit;
_onDestroy;
constructor(options) {
super({ name: options.name, component: chunk7GW2TQXP_cjs.RegisteredLogger.WORKSPACE });
this._onInit = options.onInit;
this._onDestroy = options.onDestroy;
}
// ---------------------------------------------------------------------------
// Lifecycle Wrappers (race-condition-safe)
// ---------------------------------------------------------------------------
/**
* Initialize the filesystem (wrapper with status management and race-condition safety).
*
* This method is race-condition-safe - concurrent calls will return the same promise.
* Handles status management automatically.
*
* Subclasses override `init()` to provide their initialization logic.
*/
async _init() {
if (this.status === "ready") {
return;
}
if (this._destroyPromise) {
try {
await this._destroyPromise;
} catch {
}
}
if (this._initPromise) {
return this._initPromise;
}
this._initPromise = this._executeInit();
try {
await this._initPromise;
} finally {
this._initPromise = void 0;
}
}
/**
* Internal init execution - handles status.
*/
async _executeInit() {
this.status = "initializing";
this.error = void 0;
try {
await this.init();
this.status = "ready";
try {
await this._onInit?.({ filesystem: this });
} catch (error) {
this.logger.warn("onInit callback failed", { error });
}
} catch (error) {
this.status = "error";
this.error = error instanceof Error ? error.message : String(error);
this.logger.error("Failed to initialize filesystem", { error, id: this.id });
throw error;
}
}
/**
* Override this method to implement filesystem initialization logic.
*
* Called by `_init()` after status is set to 'initializing'.
* Status will be set to 'ready' on success, 'error' on failure.
*
* @example
* ```typescript
* async init(): Promise<void> {
* this._client = new StorageClient({ ... });
* await this._client.connect();
* }
* ```
*/
async init() {
}
/**
* Ensure the filesystem is ready.
*
* Calls `_init()` if status is not 'ready'. Useful for lazy initialization
* where operations should automatically initialize the filesystem if needed.
*
* @throws {FilesystemNotReadyError} if the filesystem fails to reach 'ready' status
*
* @example
* ```typescript
* async readFile(path: string): Promise<string | Buffer> {
* await this.ensureReady();
* // Now safe to use the filesystem
* }
* ```
*/
async ensureReady() {
if (this.status !== "ready") {
await this._init();
}
if (this.status !== "ready") {
throw new FilesystemNotReadyError(this.id);
}
}
/**
* Destroy the filesystem and clean up all resources (wrapper with status management).
*
* This method is race-condition-safe - concurrent calls will return the same promise.
* Handles status management.
*
* Subclasses override `destroy()` to provide their destroy logic.
*/
async _destroy() {
if (this.status === "destroyed") {
return;
}
if (this.status === "pending") {
this.status = "destroyed";
return;
}
if (this._destroyPromise) {
return this._destroyPromise;
}
this._destroyPromise = this._executeDestroy();
try {
await this._destroyPromise;
} finally {
this._destroyPromise = void 0;
}
}
/**
* Internal destroy execution - handles status.
*/
async _executeDestroy() {
if (this._initPromise) {
try {
await this._initPromise;
} catch {
}
}
this.status = "destroying";
try {
await this._onDestroy?.({ filesystem: this });
await this.destroy();
this.status = "destroyed";
} catch (error) {
this.status = "error";
this.logger.error("Failed to destroy filesystem", { error, id: this.id });
throw error;
}
}
/**
* Override this method to implement filesystem destroy logic.
*
* Called by `_destroy()` after status is set to 'destroying'.
* Status will be set to 'destroyed' on success, 'error' on failure.
*/
async destroy() {
}
};
// src/workspace/utils.ts
function resolveInstructions(override, getDefault, requestContext) {
if (typeof override === "string") return override;
const defaultInstructions = getDefault();
if (override === void 0) return defaultInstructions;
return override({ defaultInstructions, requestContext });
}
function expandTilde(p) {
if (p === "~") return os3__namespace.homedir();
if (p.startsWith("~/") || p.startsWith("~\\")) {
return nodePath__namespace.join(os3__namespace.homedir(), p.slice(2));
}
return p;
}
function isEnoentError(error) {
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
}
function isEexistError(error) {
return error !== null && typeof error === "object" && "code" in error && error.code === "EEXIST";
}
var MIME_TYPES = {
// Text
txt: "text/plain",
html: "text/html",
htm: "text/html",
css: "text/css",
csv: "text/csv",
md: "text/markdown",
// Code
js: "application/javascript",
mjs: "application/javascript",
ts: "application/typescript",
tsx: "application/typescript",
jsx: "application/javascript",
json: "application/json",
xml: "application/xml",
yaml: "text/yaml",
yml: "text/yaml",
// Programming languages
py: "text/x-python",
rb: "text/x-ruby",
go: "text/x-go",
rs: "text/x-rust",
java: "text/x-java",
c: "text/x-c",
cpp: "text/x-c++",
h: "text/x-c",
hpp: "text/x-c++",
sh: "text/x-sh",
bash: "text/x-sh",
zsh: "text/x-sh",
// Config
toml: "text/toml",
ini: "text/plain",
env: "text/plain",
// Database/Query
sql: "text/x-sql",
graphql: "application/graphql",
gql: "application/graphql",
// Frameworks
vue: "text/x-vue",
// Images
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
svg: "image/svg+xml",
webp: "image/webp",
ico: "image/x-icon",
bmp: "image/bmp",
tiff: "image/tiff",
tif: "image/tiff",
heic: "image/heic",
heif: "image/heif",
avif: "image/avif",
// Documents
pdf: "application/pdf",
// Audio
mp3: "audio/mpeg",
wav: "audio/wav",
ogg: "audio/ogg",
flac: "audio/flac",
m4a: "audio/mp4",
aac: "audio/aac",
// Video
mp4: "video/mp4",
webm: "video/webm",
mov: "video/quicktime",
avi: "video/x-msvideo",
mkv: "video/x-matroska",
// Archives
zip: "application/zip",
tar: "application/x-tar",
gz: "application/gzip",
tgz: "application/gzip",
bz2: "application/x-bzip2",
"7z": "application/x-7z-compressed",
rar: "application/vnd.rar",
// Executables / binaries
exe: "application/vnd.microsoft.portable-executable",
dll: "application/vnd.microsoft.portable-executable",
so: "application/x-sharedlib",
dylib: "application/x-sharedlib",
bin: "application/x-binary",
dat: "application/x-binary",
// Disk images / packages
dmg: "application/x-apple-diskimage",
iso: "application/x-iso9660-image",
deb: "application/vnd.debian.binary-package",
rpm: "application/x-rpm",
// Office documents
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ppt: "application/vnd.ms-powerpoint",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
// Fonts
ttf: "font/ttf",
otf: "font/otf",
woff: "font/woff",
woff2: "font/woff2",
// Compiled code
wasm: "application/wasm",
class: "application/java-vm",
pyc: "application/x-python-code"
};
function getMimeType(filename) {
const ext = nodePath__namespace.extname(filename).slice(1).toLowerCase();
return MIME_TYPES[ext] ?? "application/octet-stream";
}
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
".md",
".txt",
".json",
".yaml",
".yml",
".js",
".mjs",
".ts",
".tsx",
".jsx",
".py",
".rb",
".go",
".rs",
".java",
".c",
".cpp",
".h",
".hpp",
".sh",
".bash",
".zsh",
".html",
".htm",
".css",
".xml",
".toml",
".ini",
".env",
".csv",
".sql",
".graphql",
".gql",
".vue",
".svg"
]);
function isTextFile(filename) {
const ext = nodePath__namespace.extname(filename).toLowerCase();
return TEXT_EXTENSIONS.has(ext);
}
function resolveToBasePath(basePath, filePath) {
const expanded = expandTilde(filePath);
if (nodePath__namespace.isAbsolute(expanded)) {
return nodePath__namespace.normalize(expanded);
}
return nodePath__namespace.resolve(basePath, expanded);
}
async function fsExists(absolutePath) {
try {
await fs2__namespace.access(absolutePath);
return true;
} catch {
return false;
}
}
async function fsStat(absolutePath, userPath) {
try {
const stats = await fs2__namespace.stat(absolutePath);
return {
name: nodePath__namespace.basename(absolutePath),
type: stats.isDirectory() ? "directory" : "file",
size: stats.size,
createdAt: stats.birthtime,
modifiedAt: stats.mtime,
mimeType: stats.isFile() ? getMimeType(absolutePath) : void 0
};
} catch (error) {
if (isEnoentError(error)) {
throw new FileNotFoundError(userPath);
}
throw error;
}
}
// src/workspace/filesystem/local-filesystem.ts
var LocalFilesystem = class extends MastraFilesystem {
id;
name = "LocalFilesystem";
provider = "local";
readOnly;
status = "pending";
_basePath;
_contained;
_allowedPaths;
_instructionsOverride;
/**
* The absolute base path on disk where files are stored.
* Useful for understanding how workspace paths map to disk paths.
*/
get basePath() {
return this._basePath;
}
/**
* Whether file operations are restricted to stay within basePath.
*
* When `true` (default), relative paths resolve against basePath and
* absolute paths are kept as-is. Any resolved path that falls outside
* basePath (and allowedPaths) throws a PermissionError. When `false`,
* no containment check is applied.
*
* **Note:** When used as a CompositeFilesystem mount with `contained: false`,
* the agent can access any path on the host filesystem through this mount.
*/
get contained() {
return this._contained;
}
/**
* Current set of resolved allowed paths.
* These paths are permitted beyond basePath when containment is enabled.
*/
get allowedPaths() {
return this._allowedPaths;
}
/**
* Update allowed paths. Accepts a direct array or an updater callback
* receiving the current paths (React setState pattern).
*
* @example
* ```typescript
* // Set directly
* fs.setAllowedPaths(['../shared-data']);
*
* // Update with callback
* fs.setAllowedPaths(prev => [...prev, '~/.claude/skills']);
* ```
*/
setAllowedPaths(pathsOrUpdater) {
const newPaths = typeof pathsOrUpdater === "function" ? pathsOrUpdater(this._allowedPaths) : pathsOrUpdater;
this._allowedPaths = newPaths.map((p) => resolveToBasePath(this._basePath, p));
}
constructor(options) {
super({ ...options, name: "LocalFilesystem" });
this.id = options.id ?? this.generateId();
this._basePath = nodePath__namespace.resolve(expandTilde(options.basePath));
this._contained = options.contained ?? true;
this.readOnly = options.readOnly;
this._allowedPaths = (options.allowedPaths ?? []).map((p) => resolveToBasePath(this._basePath, p));
this._instructionsOverride = options.instructions;
}
/**
* Return mount config for sandbox integration.
* LocalSandbox uses this to create a symlink from the mount path to basePath.
*/
getMountConfig() {
return { type: "local", basePath: this._basePath };
}
generateId() {
return `local-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
/**
* Check if an absolute path falls within basePath or any allowed path.
*/
_isWithinRoot(absolutePath, root) {
const relative2 = nodePath__namespace.relative(root, absolutePath);
return !relative2.startsWith("..") && !nodePath__namespace.isAbsolute(relative2);
}
_resolvePathForContainment(absolutePath) {
let currentPath = absolutePath;
while (true) {
try {
const realPath = fs.realpathSync(currentPath);
if (currentPath === absolutePath) {
return realPath;
}
const remainder = nodePath__namespace.relative(currentPath, absolutePath);
return nodePath__namespace.join(realPath, remainder);
} catch (error) {
if (!isEnoentError(error)) return void 0;
}
const parentPath = nodePath__namespace.dirname(currentPath);
if (parentPath === currentPath) {
return void 0;
}
currentPath = parentPath;
}
}
_isWithinAnyRoot(absolutePath) {
const roots = [this._basePath, ...this._allowedPaths];
if (roots.some((root) => this._isWithinRoot(absolutePath, root))) {
return true;
}
const resolvedPath = this._resolvePathForContainment(absolutePath);
if (!resolvedPath) {
return false;
}
return roots.some((root) => {
const resolvedRoot = this._resolvePathForContainment(root);
return resolvedRoot ? this._isWithinRoot(resolvedPath, resolvedRoot) : false;
});
}
toBuffer(content) {
if (Buffer.isBuffer(content)) return content;
if (content instanceof Uint8Array) return Buffer.from(content);
return Buffer.from(content, "utf-8");
}
resolvePath(inputPath) {
const absolutePath = resolveToBasePath(this._basePath, inputPath);
if (this._contained) {
if (!this._isWithinAnyRoot(absolutePath)) {
throw new PermissionError(inputPath, this._accessOperationHint(inputPath));
}
}
return absolutePath;
}
/**
* Build the operation string for a containment-violation `PermissionError`.
*
* When the caller passed an absolute path, suggest a concrete relative form
* only when that suffix names an existing entry under the workspace (e.g.
* `/src/app.ts` → `src/app.ts` if `<basePath>/src` exists). Otherwise emit a
* soft hint that doesn't lie about specific paths — agents that mistake `/`
* for the workspace root learn the workspace is sandboxed without us
* inventing a fictitious in-workspace location for `/etc/passwd`.
*/
_accessOperationHint(inputPath) {
if (!nodePath__namespace.isAbsolute(inputPath)) return "access";
const stripped = inputPath.replace(/^[/\\]+/, "");
if (!stripped) return "access";
const firstSegment = stripped.split(/[/\\]/, 1)[0];
if (firstSegment && firstSegment !== "." && firstSegment !== "..") {
try {
if (fs.realpathSync(nodePath__namespace.join(this._basePath, firstSegment))) {
return `access (path is outside the workspace; use a relative path like "${stripped}")`;
}
} catch {
}
}
return 'access (path is outside the workspace; use a path relative to the workspace root, without a leading "/")';
}
/**
* Resolve a workspace-relative path to an absolute disk path.
* Uses the same resolution logic as internal file operations.
* Returns `undefined` if the path violates containment.
*/
resolveAbsolutePath(inputPath) {
try {
return this.resolvePath(inputPath);
} catch {
return void 0;
}
}
toRelativePath(absolutePath) {
return nodePath__namespace.relative(this._basePath, absolutePath).replace(/\\/g, "/");
}
assertWritable(operation) {
if (this.readOnly) {
throw new WorkspaceReadOnlyError(operation);
}
}
/**
* Verify that the resolved path doesn't escape basePath via symlinks.
* Uses realpath to resolve symlinks and check the actual target.
*/
async assertPathContained(absolutePath) {
if (!this._contained) return;
if (this._allowedPaths.some((root) => this._isWithinRoot(absolutePath, root))) {
return;
}
let targetReal;
try {
targetReal = await fs2__namespace.realpath(absolutePath);
} catch (error) {
if (isEnoentError(error)) return;
throw error;
}
const roots = [this._basePath, ...this._allowedPaths];
const rootReals = [];
for (const root of roots) {
try {
rootReals.push(await fs2__namespace.realpath(root));
} catch (error) {
if (isEnoentError(error)) continue;
throw error;
}
}
const isWithinRoot = rootReals.some(
(rootReal) => targetReal === rootReal || targetReal.startsWith(rootReal + nodePath__namespace.sep)
);
if (!isWithinRoot) {
throw new PermissionError(absolutePath, "access");
}
}
async readFile(inputPath, options) {
this.logger.debug("Reading file", { path: inputPath, encoding: options?.encoding });
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
const stats = await fs2__namespace.stat(absolutePath);
if (stats.isDirectory()) {
throw new IsDirectoryError(inputPath);
}
if (options?.encoding) {
return await fs2__namespace.readFile(absolutePath, { encoding: options.encoding });
}
return await fs2__namespace.readFile(absolutePath);
} catch (error) {
if (error instanceof IsDirectoryError) throw error;
if (isEnoentError(error)) {
throw new FileNotFoundError(inputPath);
}
throw error;
}
}
async writeFile(inputPath, content, options) {
const contentSize = Buffer.isBuffer(content) ? content.length : content.length;
this.logger.debug("Writing file", { path: inputPath, size: contentSize, recursive: options?.recursive });
await this.ensureReady();
this.assertWritable("writeFile");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
if (options?.recursive === false) {
const dir = nodePath__namespace.dirname(absolutePath);
const parentPath = nodePath__namespace.dirname(inputPath);
try {
const stat4 = await fs2__namespace.stat(dir);
if (!stat4.isDirectory()) {
throw new NotDirectoryError(parentPath);
}
} catch (error) {
if (error instanceof NotDirectoryError) throw error;
if (isEnoentError(error)) {
throw new DirectoryNotFoundError(parentPath);
}
throw error;
}
}
if (options?.recursive !== false) {
const dir = nodePath__namespace.dirname(absolutePath);
await fs2__namespace.mkdir(dir, { recursive: true });
}
if (options?.expectedMtime) {
try {
const currentStat = await fs2__namespace.stat(absolutePath);
if (currentStat.mtime.getTime() !== options.expectedMtime.getTime()) {
throw new StaleFileError(inputPath, options.expectedMtime, currentStat.mtime);
}
} catch (error) {
if (error instanceof StaleFileError) throw error;
if (!isEnoentError(error)) throw error;
}
}
const writeFlag = options?.overwrite === false ? "wx" : "w";
try {
await fs2__namespace.writeFile(absolutePath, this.toBuffer(content), { flag: writeFlag });
} catch (error) {
if (options?.overwrite === false && isEexistError(error)) {
throw new FileExistsError(inputPath);
}
throw error;
}
}
async appendFile(inputPath, content) {
const contentSize = Buffer.isBuffer(content) ? content.length : content.length;
this.logger.debug("Appending to file", { path: inputPath, size: contentSize });
await this.ensureReady();
this.assertWritable("appendFile");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
const dir = nodePath__namespace.dirname(absolutePath);
await fs2__namespace.mkdir(dir, { recursive: true });
await fs2__namespace.appendFile(absolutePath, this.toBuffer(content));
}
async deleteFile(inputPath, options) {
this.logger.debug("Deleting file", { path: inputPath, force: options?.force });
await this.ensureReady();
this.assertWritable("deleteFile");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
const stats = await fs2__namespace.stat(absolutePath);
if (stats.isDirectory()) {
throw new IsDirectoryError(inputPath);
}
await fs2__namespace.unlink(absolutePath);
} catch (error) {
if (error instanceof IsDirectoryError) throw error;
if (isEnoentError(error)) {
if (!options?.force) {
throw new FileNotFoundError(inputPath);
}
} else {
throw error;
}
}
}
async copyFile(src, dest, options) {
this.logger.debug("Copying file", { src, dest, recursive: options?.recursive });
await this.ensureReady();
this.assertWritable("copyFile");
const srcPath = this.resolvePath(src);
const destPath = this.resolvePath(dest);
await this.assertPathContained(srcPath);
await this.assertPathContained(destPath);
try {
const stats = await fs2__namespace.stat(srcPath);
if (stats.isDirectory()) {
if (!options?.recursive) {
throw new IsDirectoryError(src);
}
await this.copyDirectory(srcPath, destPath, options);
} else {
await fs2__namespace.mkdir(nodePath__namespace.dirname(destPath), { recursive: true });
const copyFlags = options?.overwrite === false ? fs.constants.COPYFILE_EXCL : 0;
try {
await fs2__namespace.copyFile(srcPath, destPath, copyFlags);
} catch (error) {
if (options?.overwrite === false && isEexistError(error)) {
throw new FileExistsError(dest);
}
throw error;
}
}
} catch (error) {
if (error instanceof IsDirectoryError || error instanceof FileExistsError) throw error;
if (isEnoentError(error)) {
throw new FileNotFoundError(src);
}
throw error;
}
}
async copyDirectory(src, dest, options) {
await this.ensureReady();
await fs2__namespace.mkdir(dest, { recursive: true });
const entries = await fs2__namespace.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcEntry = nodePath__namespace.join(src, entry.name);
const destEntry = nodePath__namespace.join(dest, entry.name);
await this.assertPathContained(srcEntry);
await this.assertPathContained(destEntry);
if (entry.isDirectory()) {
await this.copyDirectory(srcEntry, destEntry, options);
} else {
const copyFlags = options?.overwrite === false ? fs.constants.COPYFILE_EXCL : 0;
try {
await fs2__namespace.copyFile(srcEntry, destEntry, copyFlags);
} catch (error) {
if (options?.overwrite === false && isEexistError(error)) {
continue;
}
throw error;
}
}
}
}
async moveFile(src, dest, options) {
this.logger.debug("Moving file", { src, dest, overwrite: options?.overwrite });
await this.ensureReady();
this.assertWritable("moveFile");
const srcPath = this.resolvePath(src);
const destPath = this.resolvePath(dest);
await this.assertPathContained(srcPath);
await this.assertPathContained(destPath);
try {
await fs2__namespace.mkdir(nodePath__namespace.dirname(destPath), { recursive: true });
if (options?.overwrite === false) {
await this.copyFile(src, dest, { ...options, overwrite: false });
await fs2__namespace.rm(srcPath, { recursive: true, force: true });
return;
}
try {
await fs2__namespace.rename(srcPath, destPath);
} catch (error) {
const code = error.code;
if (code !== "EXDEV") {
throw error;
}
await this.copyFile(src, dest, options);
await fs2__namespace.rm(srcPath, { recursive: true, force: true });
}
} catch (error) {
if (error instanceof FileExistsError) throw error;
if (isEnoentError(error)) {
throw new FileNotFoundError(src);
}
throw error;
}
}
async mkdir(inputPath, options) {
this.logger.debug("Creating directory", { path: inputPath, recursive: options?.recursive });
await this.ensureReady();
this.assertWritable("mkdir");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
await fs2__namespace.mkdir(absolutePath, { recursive: options?.recursive ?? true });
} catch (error) {
if (isEexistError(error)) {
const stats = await fs2__namespace.stat(absolutePath);
if (!stats.isDirectory()) {
throw new FileExistsError(inputPath);
}
} else if (isEnoentError(error)) {
const parentPath = nodePath__namespace.dirname(inputPath);
throw new DirectoryNotFoundError(parentPath);
} else {
throw error;
}
}
}
async rmdir(inputPath, options) {
this.logger.debug("Removing directory", { path: inputPath, recursive: options?.recursive, force: options?.force });
await this.ensureReady();
this.assertWritable("rmdir");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
const stats = await fs2__namespace.stat(absolutePath);
if (!stats.isDirectory()) {
throw new NotDirectoryError(inputPath);
}
if (options?.recursive) {
await fs2__namespace.rm(absolutePath, { recursive: true, force: options?.force ?? false });
} else {
const entries = await fs2__namespace.readdir(absolutePath);
if (entries.length > 0) {
throw new DirectoryNotEmptyError(inputPath);
}
await fs2__namespace.rmdir(absolutePath);
}
} catch (error) {
if (error instanceof NotDirectoryError || error instanceof DirectoryNotEmptyError) {
throw error;
}
if (isEnoentError(error)) {
if (!options?.force) {
throw new DirectoryNotFoundError(inputPath);
}
} else {
throw error;
}
}
}
async readdir(inputPath, options) {
this.logger.debug("Reading directory", { path: inputPath, recursive: options?.recursive });
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
const stats = await fs2__namespace.stat(absolutePath);
if (!stats.isDirectory()) {
throw new NotDirectoryError(inputPath);
}
const entries = await fs2__namespace.readdir(absolutePath, { withFileTypes: true });
const result = [];
for (const entry of entries) {
const entryPath = nodePath__namespace.join(absolutePath, entry.name);
if (options?.extension) {
const extensions = Array.isArray(options.extension) ? options.extension : [options.extension];
if (entry.isFile()) {
const ext = nodePath__namespace.extname(entry.name);
if (!extensions.some((e) => e === ext || e === ext.slice(1))) {
continue;
}
}
}
const isSymlink = entry.isSymbolicLink();
let symlinkTarget;
let resolvedType = "file";
if (isSymlink) {
try {
symlinkTarget = await fs2__namespace.readlink(entryPath);
const targetStat = await fs2__namespace.stat(entryPath);
resolvedType = targetStat.isDirectory() ? "directory" : "file";
} catch {
resolvedType = "file";
}
} else {
resolvedType = entry.isDirectory() ? "directory" : "file";
}
const fileEntry = {
name: entry.name,
type: resolvedType,
isSymlink: isSymlink || void 0,
symlinkTarget
};
if (resolvedType === "file" && !isSymlink) {
try {
const stat4 = await fs2__namespace.stat(entryPath);
fileEntry.size = stat4.size;
} catch {
}
}
result.push(fileEntry);
if (options?.recursive && resolvedType === "directory") {
const depth = options.maxDepth ?? 100;
if (depth > 0) {
const subEntries = await this.readdir(this.toRelativePath(entryPath), { ...options, maxDepth: depth - 1 });
result.push(
...subEntries.map((e) => ({
...e,
name: `${entry.name}/${e.name}`
}))
);
}
}
}
return result;
} catch (error) {
if (error instanceof NotDirectoryError) throw error;
if (isEnoentError(error)) {
throw new DirectoryNotFoundError(inputPath);
}
throw error;
}
}
async exists(inputPath) {
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
return fsExists(absolutePath);
}
async stat(inputPath) {
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
const result = await fsStat(absolutePath, inputPath);
return {
...result,
path: this.toRelativePath(absolutePath)
};
}
async realpath(inputPath) {
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
const canonicalPath = await fs2__namespace.realpath(absolutePath);
return this.toRelativePath(canonicalPath);
}
/**
* Initialize the local filesystem by creating the base directory.
* Status management is handled by the base class.
*/
async init() {
this.logger.debug("Initializing filesystem", { basePath: this._basePath });
await fs2__namespace.mkdir(this._basePath, { recursive: true });
this.logger.debug("Filesystem initialized", { basePath: this._basePath });
}
/**
* Clean up the local filesystem.
* LocalFilesystem doesn't delete files on destroy by default.
* Status management is handled by the base class.
*/
async destroy() {
}
getInfo() {
return {
id: this.id,
name: this.name,
provider: this.provider,
readOnly: this.readOnly,
status: this.status,
error: this.error,
metadata: {
basePath: this.basePath,
contained: this._contained,
...this._allowedPaths.length > 0 && { allowedPaths: [...this._allowedPaths] }
}
};
}
getInstructions(opts) {
return resolveInstructions(this._instructionsOverride, () => this._getDefaultInstructions(), opts?.requestContext);
}
_getDefaultInstructions() {
const parts = [`Local filesystem at "${this.basePath}". Relative paths resolve from this directory.`];
if (this._contained) {
if (this._allowedPaths.length > 0) {
parts.push(
`File access is restricted to this directory and the following allowed paths: ${this._allowedPaths.join(", ")}.`
);
} else {
parts.push("File access is restricted to this directory.");
}
} else {
parts.push("Containment is disabled, so any path on the host filesystem is accessible.");
}
return parts.join(" ");
}
};
var InM