UNPKG

@mastra/core

Version:
1 lines 729 kB
{"version":3,"file":"workspace-SsEU6Si6.cjs","names":["posixPath","PermissionError","MastraBase","RegisteredLogger","FilesystemNotReadyError","resolveToBasePath","nodePath","expandTilde","isEnoentError","PermissionError","WorkspaceReadOnlyError","fs","IsDirectoryError","FileNotFoundError","NotDirectoryError","DirectoryNotFoundError","StaleFileError","isEexistError","FileExistsError","fsConstants","DirectoryNotEmptyError","fsExists","fsStat","path","nodePath","nodePath","fs","Readable","Writable","StringDecoder","MastraBase","RegisteredLogger","path","expandTilde","fs","#tokenizeOptions","#documents","#docCount","#invertedIndex","#documentFrequency","#updateAvgDocLength","#avgDocLength","#computeIDF","#computeTermScore","#tokenizeOptions","#bm25Index","#vectorConfig","#lazyVectorIndex","#indexedIds","#pendingVectorDocs","#vectorIndexBuilt","#indexVector","#determineSearchMode","#searchBM25","#searchVector","#searchHybrid","#embedOne","#embedAll","#vectorIndexReady","#flushVectorBatch","#ensureVectorIndex","#dedupePendingVectorDocsLastWins","#adjustLineRange","#normalizeBM25Scores","#tree","#blobStore","#versionCreatedAt","#directories","#computeDirectories","#normalizePath","#sources","#fallback","#fallbackSkills","#maxVersionCreatedAt","#normalizePath","#routePath","joinPath","createTool","z","WorkspaceError","LocalSkillSource","WorkspaceSkillsImpl","SearchNotAvailableError","path","resolvePathPattern","pMapSkip","RequestContext","WorkspaceNotAvailableError","FilesystemNotAvailableError","SandboxNotAvailableError","createTool","z","WorkspaceReadOnlyError","FileNotFoundError","createTool","z","WorkspaceReadOnlyError","createTool","z","WorkspaceReadOnlyError","z","SandboxFeatureNotSupportedError","createTool","createTool","z","FileNotFoundError","createTool","z","SandboxFeatureNotSupportedError","createTool","z","isGlobPattern","extractGlobBase","createGlobMatcher","isTextFile","createTool","z","createTool","z","SandboxFeatureNotSupportedError","createGlobMatcher","createTool","z","fs","createTool","z","createTool","z","WorkspaceReadOnlyError","createTool","z","z","createTool","createTool","z","WorkspaceReadOnlyError","RequestContext","FileReadRequiredError","FileNotFoundError","z"],"sources":["../src/workspace/lifecycle.ts","../src/workspace/filesystem/composite-filesystem.ts","../src/workspace/filesystem/mastra-filesystem.ts","../src/workspace/utils.ts","../src/workspace/filesystem/local-filesystem.ts","../src/workspace/filesystem/file-read-tracker.ts","../src/workspace/filesystem/file-write-lock.ts","../src/workspace/lsp/language.ts","../src/workspace/lsp/client.ts","../src/workspace/lsp/servers.ts","../src/workspace/lsp/manager.ts","../src/workspace/sandbox/errors.ts","../src/workspace/sandbox/execa.ts","../src/workspace/sandbox/process-manager/process-handle.ts","../src/workspace/sandbox/process-manager/process-manager.ts","../src/workspace/sandbox/local-process-manager.ts","../src/workspace/sandbox/mounts/types.ts","../src/workspace/sandbox/mount-manager.ts","../src/workspace/sandbox/utils.ts","../src/workspace/sandbox/mastra-sandbox.ts","../src/workspace/sandbox/native-sandbox/detect.ts","../src/workspace/sandbox/native-sandbox/seatbelt.ts","../src/workspace/sandbox/native-sandbox/bubblewrap.ts","../src/workspace/sandbox/native-sandbox/wrapper.ts","../src/workspace/sandbox/local-sandbox.ts","../src/workspace/line-utils.ts","../src/workspace/search/bm25.ts","../src/workspace/search/search-engine.ts","../src/workspace/skills/versioned-skill-source.ts","../src/workspace/skills/composite-versioned-skill-source.ts","../src/workspace/skills/publish.ts","../src/workspace/tools/tracing.ts","../src/workspace/skills/tools.ts","../src/workspace/workspace.ts","../src/workspace/sandbox/sandbox.ts","../src/workspace/constants/index.ts","../src/workspace/tools/helpers.ts","../src/workspace/tools/ast-edit.ts","../src/workspace/tools/delete-file.ts","../src/workspace/tools/edit-file.ts","../src/browser/cli-handler.ts","../src/workspace/tools/output-helpers.ts","../src/workspace/tools/execute-command.ts","../src/workspace/tools/file-stat.ts","../src/workspace/tools/get-process-output.ts","../src/workspace/gitignore.ts","../src/workspace/tools/grep.ts","../src/workspace/tools/index-content.ts","../src/workspace/tools/kill-process.ts","../src/workspace/tools/tree-formatter.ts","../src/workspace/tools/list-files.ts","../src/workspace/tools/lsp-inspect.ts","../src/workspace/tools/mkdir.ts","../src/workspace/tools/read-file.ts","../src/workspace/tools/search.ts","../src/workspace/tools/write-file.ts","../src/workspace/tools/tools.ts"],"sourcesContent":["/**\n * Workspace Lifecycle Interfaces\n *\n * Defines lifecycle contracts for workspace providers (filesystem, sandbox).\n * The base `Lifecycle` holds shared members while `FilesystemLifecycle` and\n * `SandboxLifecycle` add the methods each provider kind actually uses.\n */\n\n// =============================================================================\n// Base Lifecycle Interface\n// =============================================================================\n\n/**\n * Shared lifecycle base for workspace providers.\n *\n * Contains status tracking, destroy, readiness check, and info retrieval.\n * Provider-specific lifecycle methods live in the extended interfaces:\n * - {@link FilesystemLifecycle} adds `init()`\n * - {@link SandboxLifecycle} adds `start()` / `stop()`\n *\n * @typeParam TInfo - The type returned by getInfo() (e.g., FilesystemInfo, SandboxInfo)\n */\nexport interface Lifecycle<TInfo = unknown> {\n /** Current status */\n status: ProviderStatus;\n\n /** Error message when status is 'error' */\n error?: string;\n\n /**\n * Clean up all resources.\n *\n * Called when the workspace is being permanently shut down.\n * Use for operations like:\n * - Terminating cloud instances\n * - Closing all connections\n * - Cleaning up temporary files\n */\n destroy?(): void | Promise<void>;\n\n /** @deprecated Use `status === 'running'` instead. */\n isReady?(): boolean | Promise<boolean>;\n\n /**\n * Get status and metadata.\n *\n * Returns information about the current state of the provider.\n */\n getInfo?(): TInfo | Promise<TInfo>;\n}\n\n// =============================================================================\n// Filesystem Lifecycle\n// =============================================================================\n\n/**\n * Lifecycle interface for filesystem providers (two-phase: init → destroy).\n *\n * @typeParam TInfo - The type returned by getInfo()\n */\nexport interface FilesystemLifecycle<TInfo = unknown> extends Lifecycle<TInfo> {\n /**\n * One-time setup operations.\n *\n * Called once when the workspace is first initialized.\n * Use for operations like:\n * - Creating base directories\n * - Setting up database tables\n * - Provisioning cloud resources\n * - Installing dependencies\n */\n init?(): void | Promise<void>;\n}\n\n// =============================================================================\n// Sandbox Lifecycle\n// =============================================================================\n\n/**\n * Lifecycle interface for sandbox providers (three-phase: start → stop → destroy).\n *\n * @typeParam TInfo - The type returned by getInfo()\n */\nexport interface SandboxLifecycle<TInfo = unknown> extends Lifecycle<TInfo> {\n /**\n * Begin active operation.\n *\n * Called to transition from initialized to running state.\n * Use for operations like:\n * - Establishing connection pools\n * - Spinning up cloud instances\n * - Starting background processes\n * - Warming up caches\n */\n start?(): void | Promise<void>;\n\n /**\n * Pause operation, keeping state for potential restart.\n *\n * Called to temporarily stop without full cleanup.\n * Use for operations like:\n * - Closing connections (but keeping config)\n * - Pausing cloud instances\n * - Flushing buffers\n */\n stop?(): void | Promise<void>;\n}\n\n// =============================================================================\n// Status Types\n// =============================================================================\n\n/**\n * Common status values for stateful providers.\n *\n * Not all providers need status tracking - local/stateless providers\n * may not use this. But providers with connection pools or cloud\n * instances can use these states.\n */\nexport type ProviderStatus =\n | 'pending' // Created but not initialized\n | 'initializing' // Running init()\n | 'ready' // Initialized, waiting to start (or stateless and ready)\n | 'starting' // Running start()\n | 'running' // Active and accepting requests\n | 'stopping' // Running stop()\n | 'stopped' // Stopped but can restart\n | 'destroying' // Running destroy()\n | 'destroyed' // Fully cleaned up\n | 'error'; // Something went wrong\n\n// =============================================================================\n// Lifecycle Helper\n// =============================================================================\n\n/**\n * Provider that may have lifecycle methods.\n * Used by `callLifecycle` to dispatch to the correct method.\n */\ninterface LifecycleProvider {\n _init?(): void | Promise<void>;\n _start?(): void | Promise<void>;\n _stop?(): void | Promise<void>;\n _destroy?(): void | Promise<void>;\n init?(): void | Promise<void>;\n start?(): void | Promise<void>;\n stop?(): void | Promise<void>;\n destroy?(): void | Promise<void>;\n}\n\n/**\n * Call a lifecycle method on a provider, preferring the `_`-prefixed wrapper\n * (which adds status tracking & race-condition safety) when available,\n * falling back to the plain method for interface-only implementations.\n *\n * @example\n * ```typescript\n * await callLifecycle(sandbox, 'start'); // calls sandbox._start() ?? sandbox.start()\n * await callLifecycle(filesystem, 'init'); // calls filesystem._init() ?? filesystem.init()\n * ```\n */\nexport async function callLifecycle(\n provider: LifecycleProvider,\n method: 'init' | 'start' | 'stop' | 'destroy',\n): Promise<void> {\n const wrapped = `_${method}` as const;\n const wrappedFn = provider[wrapped];\n if (typeof wrappedFn === 'function') {\n await wrappedFn.call(provider);\n } else {\n const plainFn = provider[method];\n if (typeof plainFn === 'function') {\n await plainFn.call(provider);\n }\n }\n}\n","/**\n * CompositeFilesystem - Routes operations to mounted filesystems based on path.\n *\n * Creates a unified filesystem view by combining multiple filesystems at different\n * mount points. Useful for composing local storage, S3, and other backends.\n *\n * @example\n * ```typescript\n * const cfs = new CompositeFilesystem({\n * mounts: {\n * '/local': new LocalFilesystem({ basePath: './data' }),\n * '/s3': new S3Filesystem({ bucket: 'my-bucket', ... }),\n * }\n * });\n *\n * // readdir('/') returns ['local', 's3']\n * // readFile('/local/file.txt') reads from LocalFilesystem\n * // readFile('/s3/data.json') reads from S3Filesystem\n * ```\n */\n\nimport posixPath from 'node:path/posix';\n\nimport type { RequestContext } from '../../request-context';\nimport { PermissionError } from '../errors';\nimport { callLifecycle } from '../lifecycle';\nimport type { ProviderStatus } from '../lifecycle';\nimport type {\n WorkspaceFilesystem,\n FileContent,\n FileEntry,\n FileStat,\n FilesystemInfo,\n ReadOptions,\n WriteOptions,\n ListOptions,\n CopyOptions,\n RemoveOptions,\n} from './filesystem';\n\n/**\n * Configuration for CompositeFilesystem.\n */\nexport interface CompositeFilesystemConfig<\n TMounts extends Record<string, WorkspaceFilesystem> = Record<string, WorkspaceFilesystem>,\n> {\n /** Map of mount paths to filesystem instances */\n mounts: TMounts;\n}\n\ninterface ResolvedMount {\n fs: WorkspaceFilesystem;\n fsPath: string;\n mountPath: string;\n}\n\n/**\n * CompositeFilesystem implementation.\n *\n * Routes file operations to the appropriate underlying filesystem based on path.\n * Supports cross-mount operations (copy/move between different filesystems).\n *\n * The generic parameter preserves the concrete types of mounted filesystems,\n * enabling typed access via `mounts.get()`.\n *\n * @example\n * ```typescript\n * const cfs = new CompositeFilesystem({\n * mounts: {\n * '/local': new LocalFilesystem({ basePath: './data' }),\n * '/s3': new S3Filesystem({ bucket: 'my-bucket' }),\n * },\n * });\n *\n * cfs.mounts.get('/local') // LocalFilesystem\n * cfs.mounts.get('/s3') // S3Filesystem\n * ```\n */\nexport class CompositeFilesystem<\n TMounts extends Record<string, WorkspaceFilesystem> = Record<string, WorkspaceFilesystem>,\n> implements WorkspaceFilesystem {\n readonly id: string;\n readonly name = 'CompositeFilesystem';\n readonly provider = 'composite';\n\n readonly readOnly?: boolean;\n status: ProviderStatus = 'ready';\n\n private readonly _mounts: Map<string, WorkspaceFilesystem>;\n\n constructor(config: CompositeFilesystemConfig<TMounts>) {\n this.id = `cfs-${Date.now().toString(36)}`;\n this._mounts = new Map();\n\n for (const [path, fs] of Object.entries(config.mounts)) {\n const normalized = this.normalizePath(path);\n this._mounts.set(normalized, fs);\n }\n\n if (this._mounts.size === 0) {\n throw new Error('CompositeFilesystem requires at least one mount');\n }\n\n // Composite is read-only when every mount is read-only\n this.readOnly = [...this._mounts.values()].every(fs => fs.readOnly) || undefined;\n\n // Validate no nested mount paths (e.g., /data and /data/sub)\n const mountPaths = [...this._mounts.keys()];\n for (const a of mountPaths) {\n for (const b of mountPaths) {\n if (a !== b && b.startsWith(a + '/')) {\n throw new Error(`Nested mount paths are not supported: \"${b}\" is nested under \"${a}\"`);\n }\n }\n }\n }\n\n /**\n * Get all mount paths.\n */\n get mountPaths(): string[] {\n return Array.from(this._mounts.keys());\n }\n\n /**\n * Get the mounts map.\n * Returns a typed map where `get()` preserves the concrete filesystem type per mount path.\n */\n get mounts(): ReadonlyMountMap<TMounts> {\n return this._mounts as unknown as ReadonlyMountMap<TMounts>;\n }\n\n /**\n * Get status and metadata for this composite filesystem.\n * Includes info from each mounted filesystem in `metadata.mounts`.\n */\n async getInfo(): Promise<FilesystemInfo> {\n const mounts: Record<string, FilesystemInfo | null> = {};\n for (const [mountPath, fs] of this._mounts) {\n mounts[mountPath] = (await fs.getInfo?.()) ?? null;\n }\n\n return {\n id: this.id,\n name: this.name,\n provider: this.provider,\n status: this.status,\n readOnly: this.readOnly,\n metadata: { mounts },\n };\n }\n\n /**\n * Get the underlying filesystem for a given path.\n * Returns undefined if the path doesn't resolve to any mount.\n */\n getFilesystemForPath(path: string): WorkspaceFilesystem | undefined {\n const resolved = this.resolveMount(path);\n return resolved?.fs;\n }\n\n /**\n * Get the mount path for a given path.\n * Returns undefined if the path doesn't resolve to any mount.\n */\n getMountPathForPath(path: string): string | undefined {\n const resolved = this.resolveMount(path);\n return resolved?.mountPath;\n }\n\n /**\n * Resolve a workspace-relative path to an absolute disk path.\n * Strips the mount prefix and delegates to the underlying filesystem.\n */\n resolveAbsolutePath(path: string): string | undefined {\n const r = this.resolveMount(path);\n if (!r) return undefined;\n return r.fs.resolveAbsolutePath?.(r.fsPath);\n }\n\n private normalizePath(path: string): string {\n if (!path || path === '/' || path === '.') return '/';\n // posix.normalize resolves dot segments (./foo → foo, a/../b → b)\n let n = posixPath.normalize(path);\n if (n === '.') return '/';\n if (!n.startsWith('/')) n = `/${n}`;\n if (n.length > 1 && n.endsWith('/')) n = n.slice(0, -1);\n return n;\n }\n\n private resolveMount(path: string): ResolvedMount | null {\n const normalized = this.normalizePath(path);\n let best: { mountPath: string; fs: WorkspaceFilesystem } | null = null;\n\n for (const [mountPath, fs] of this._mounts) {\n if (normalized === mountPath || normalized.startsWith(mountPath + '/')) {\n if (!best || mountPath.length > best.mountPath.length) {\n best = { mountPath, fs };\n }\n }\n }\n\n if (!best) return null;\n\n let fsPath = normalized.slice(best.mountPath.length);\n // Strip the leading slash so the path is relative to the mounted filesystem's basePath\n if (fsPath === '/') fsPath = '';\n else if (fsPath.startsWith('/')) fsPath = fsPath.slice(1);\n\n return { fs: best.fs, fsPath, mountPath: best.mountPath };\n }\n\n private getVirtualEntries(path: string): FileEntry[] | null {\n const normalized = this.normalizePath(path);\n if (this.resolveMount(normalized)) return null;\n\n const entriesMap = new Map<string, FileEntry>();\n for (const [mountPath, fs] of this._mounts.entries()) {\n const isUnder = normalized === '/' ? mountPath.startsWith('/') : mountPath.startsWith(normalized + '/');\n\n if (isUnder) {\n const remaining = normalized === '/' ? mountPath.slice(1) : mountPath.slice(normalized.length + 1);\n const next = remaining.split('/')[0];\n if (next && !entriesMap.has(next)) {\n // Check if this is a direct mount point (e.g., listing '/' and mount is '/s3')\n const isDirectMount = remaining === next;\n const entry: FileEntry = { name: next, type: 'directory' as const };\n\n // If it's a direct mount point, include filesystem metadata\n if (isDirectMount) {\n entry.mount = {\n provider: fs.provider,\n icon: fs.icon,\n displayName: fs.displayName,\n description: fs.description,\n status: fs.status,\n error: fs.error,\n };\n }\n\n entriesMap.set(next, entry);\n }\n }\n }\n\n return entriesMap.size > 0 ? Array.from(entriesMap.values()) : null;\n }\n\n private isVirtualPath(path: string): boolean {\n const normalized = this.normalizePath(path);\n if (normalized === '/' && !this._mounts.has('/')) return true;\n for (const mountPath of this._mounts.keys()) {\n if (mountPath.startsWith(normalized + '/')) return true;\n }\n return false;\n }\n\n /**\n * Assert that a filesystem is writable (not read-only).\n * @throws {PermissionError} if the filesystem is read-only\n */\n private assertWritable(fs: WorkspaceFilesystem, path: string, operation: string): void {\n if (fs.readOnly) {\n throw new PermissionError(path, `${operation} (filesystem is read-only)`);\n }\n }\n\n // ===========================================================================\n // WorkspaceFilesystem Implementation\n // ===========================================================================\n\n async init(): Promise<void> {\n this.status = 'initializing';\n for (const [mountPath, fs] of this._mounts.entries()) {\n try {\n await callLifecycle(fs, 'init');\n } catch (e) {\n // Individual mount failed - it will have status='error'\n // Log but continue with other mounts\n const message = e instanceof Error ? e.message : String(e);\n console.warn(`[CompositeFilesystem] Mount \"${mountPath}\" failed to initialize: ${message}`);\n }\n }\n // CompositeFilesystem is ready even if some mounts failed\n // Operations on errored mounts will be handled by the underlying filesystem\n this.status = 'ready';\n }\n\n async destroy(): Promise<void> {\n this.status = 'destroying';\n const errors: Error[] = [];\n for (const fs of this._mounts.values()) {\n try {\n await callLifecycle(fs, 'destroy');\n } catch (e) {\n errors.push(e instanceof Error ? e : new Error(String(e)));\n }\n }\n if (errors.length > 0) {\n this.status = 'error';\n throw new AggregateError(errors, 'Some filesystems failed to destroy');\n }\n this.status = 'destroyed';\n }\n\n async readFile(path: string, options?: ReadOptions): Promise<string | Buffer> {\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n return r.fs.readFile(r.fsPath, options);\n }\n\n async writeFile(path: string, content: FileContent, options?: WriteOptions): Promise<void> {\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n this.assertWritable(r.fs, path, 'writeFile');\n return r.fs.writeFile(r.fsPath, content, options);\n }\n\n async appendFile(path: string, content: FileContent): Promise<void> {\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n this.assertWritable(r.fs, path, 'appendFile');\n return r.fs.appendFile(r.fsPath, content);\n }\n\n async deleteFile(path: string, options?: RemoveOptions): Promise<void> {\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n this.assertWritable(r.fs, path, 'deleteFile');\n return r.fs.deleteFile(r.fsPath, options);\n }\n\n async copyFile(src: string, dest: string, options?: CopyOptions): Promise<void> {\n const srcR = this.resolveMount(src);\n const destR = this.resolveMount(dest);\n if (!srcR) throw new Error(`No mount for source: ${src}`);\n if (!destR) throw new Error(`No mount for dest: ${dest}`);\n this.assertWritable(destR.fs, dest, 'copyFile');\n\n // Same mount - delegate\n if (srcR.mountPath === destR.mountPath) {\n return srcR.fs.copyFile(srcR.fsPath, destR.fsPath, options);\n }\n\n // Cross-mount copy - read then write\n const content = await srcR.fs.readFile(srcR.fsPath);\n await destR.fs.writeFile(destR.fsPath, content, { overwrite: options?.overwrite });\n }\n\n async moveFile(src: string, dest: string, options?: CopyOptions): Promise<void> {\n const srcR = this.resolveMount(src);\n const destR = this.resolveMount(dest);\n if (!srcR) throw new Error(`No mount for source: ${src}`);\n if (!destR) throw new Error(`No mount for dest: ${dest}`);\n this.assertWritable(destR.fs, dest, 'moveFile');\n this.assertWritable(srcR.fs, src, 'moveFile'); // Source must be writable for delete\n\n // Same mount - delegate\n if (srcR.mountPath === destR.mountPath) {\n return srcR.fs.moveFile(srcR.fsPath, destR.fsPath, options);\n }\n\n // Cross-mount move - copy then delete\n await this.copyFile(src, dest, options);\n await srcR.fs.deleteFile(srcR.fsPath);\n }\n\n async readdir(path: string, options?: ListOptions): Promise<FileEntry[]> {\n const virtual = this.getVirtualEntries(path);\n if (virtual) return virtual;\n\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n return r.fs.readdir(r.fsPath, options);\n }\n\n async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n this.assertWritable(r.fs, path, 'mkdir');\n return r.fs.mkdir(r.fsPath, options);\n }\n\n async rmdir(path: string, options?: RemoveOptions): Promise<void> {\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n this.assertWritable(r.fs, path, 'rmdir');\n return r.fs.rmdir(r.fsPath, options);\n }\n\n async exists(path: string): Promise<boolean> {\n if (this.isVirtualPath(path)) return true;\n const r = this.resolveMount(path);\n if (!r) return false;\n // Mount point root always exists (even if errored)\n if (r.fsPath === '') return true;\n return r.fs.exists(r.fsPath);\n }\n\n async stat(path: string): Promise<FileStat> {\n const normalized = this.normalizePath(path);\n\n if (this.isVirtualPath(path)) {\n const parts = normalized.split('/').filter(Boolean);\n const now = new Date();\n return {\n name: parts[parts.length - 1] || '',\n path: normalized,\n type: 'directory',\n size: 0,\n createdAt: now,\n modifiedAt: now,\n };\n }\n\n const r = this.resolveMount(path);\n if (!r) throw new Error(`No mount for path: ${path}`);\n\n // Mount point root always returns directory stat (even if errored)\n if (r.fsPath === '') {\n const parts = normalized.split('/').filter(Boolean);\n const now = new Date();\n return {\n name: parts[parts.length - 1] || '',\n path: normalized,\n type: 'directory',\n size: 0,\n createdAt: now,\n modifiedAt: now,\n };\n }\n\n return r.fs.stat(r.fsPath);\n }\n\n async isFile(path: string): Promise<boolean> {\n if (this.isVirtualPath(path)) return false;\n const r = this.resolveMount(path);\n if (!r) return false;\n try {\n const stat = await r.fs.stat(r.fsPath);\n return stat.type === 'file';\n } catch {\n return false;\n }\n }\n\n async isDirectory(path: string): Promise<boolean> {\n if (this.isVirtualPath(path)) return true;\n const r = this.resolveMount(path);\n if (!r) return false;\n // Mount point root is always a directory (even if errored)\n if (r.fsPath === '') return true;\n try {\n const stat = await r.fs.stat(r.fsPath);\n return stat.type === 'directory';\n } catch {\n return false;\n }\n }\n\n /**\n * Get instructions describing the mounted filesystems.\n * Used by agents to understand available storage locations.\n */\n getInstructions(_opts?: { requestContext?: RequestContext }): string {\n const mountDescriptions = Array.from(this._mounts.entries())\n .map(([mountPath, fs]) => {\n const name = fs.displayName || fs.provider;\n const access = fs.readOnly ? '(read-only)' : '(read-write)';\n return `- ${mountPath}: ${name} ${access}`;\n })\n .join('\\n');\n\n return `Filesystem mount points:\\n${mountDescriptions}`;\n }\n}\n\n/**\n * Distributive mapped type that produces a union of correlated `[key, value]` tuples.\n *\n * For `{ '/local': LocalFilesystem, '/s3': S3Filesystem }` this yields:\n * `['/local', LocalFilesystem] | ['/s3', S3Filesystem]`\n *\n * This enables discriminated-union narrowing when iterating entries without destructuring:\n * ```typescript\n * for (const entry of mounts.entries()) {\n * if (entry[0] === '/local') {\n * entry[1] // LocalFilesystem\n * }\n * }\n * ```\n */\nexport type MountMapEntry<TMounts extends Record<string, WorkspaceFilesystem>> = {\n [K in string & keyof TMounts]: [K, TMounts[K]];\n}[string & keyof TMounts];\n\n/**\n * A read-only view of mounted filesystems with typed per-key access.\n *\n * Unlike `ReadonlyMap<string, WorkspaceFilesystem>`, this preserves the\n * concrete filesystem type for each mount path via an overloaded `get()`.\n *\n * Iteration methods return correlated `[key, value]` tuples ({@link MountMapEntry})\n * so that checking `entry[0]` narrows `entry[1]` to the concrete filesystem type.\n *\n * @example\n * ```typescript\n * const mounts = cfs.mounts;\n * mounts.get('/local') // LocalFilesystem\n * mounts.get('/s3') // S3Filesystem\n * ```\n */\nexport interface ReadonlyMountMap<TMounts extends Record<string, WorkspaceFilesystem>> {\n /** Get a mounted filesystem by path. Returns the concrete type for known mount paths. */\n get<K extends string & keyof TMounts>(key: K): TMounts[K];\n get(key: string): WorkspaceFilesystem | undefined;\n\n has(key: string): boolean;\n readonly size: number;\n\n keys(): IterableIterator<string & keyof TMounts>;\n values(): IterableIterator<TMounts[keyof TMounts & string]>;\n entries(): IterableIterator<MountMapEntry<TMounts>>;\n forEach(\n callbackfn: (\n value: TMounts[keyof TMounts & string],\n key: string & keyof TMounts,\n map: ReadonlyMountMap<TMounts>,\n ) => void,\n ): void;\n [Symbol.iterator](): IterableIterator<MountMapEntry<TMounts>>;\n}\n","/**\n * MastraFilesystem Base Class\n *\n * Abstract base class for filesystem providers that want automatic logger integration\n * and lifecycle management.\n *\n * Extends MastraBase to receive the Mastra logger when registered with a Mastra instance.\n *\n * ## Lifecycle Management\n *\n * The base class provides race-condition-safe lifecycle wrappers:\n * - `_init()` - Handles concurrent calls, status management\n * - `_destroy()` - Handles concurrent calls and status management\n *\n * Subclasses override the plain `init()` and `destroy()` methods to provide\n * their implementation. Callers use the `_`-prefixed wrappers (or `callLifecycle()`)\n * which add status tracking and race-condition safety.\n *\n * External providers can extend this class to get logger support, or implement\n * the WorkspaceFilesystem interface directly if they don't need logging.\n */\n\nimport { MastraBase } from '../../base';\nimport { RegisteredLogger } from '../../logger/constants';\nimport { FilesystemNotReadyError } from '../errors';\nimport type { ProviderStatus } from '../lifecycle';\nimport type {\n WorkspaceFilesystem,\n FileContent,\n FileStat,\n FileEntry,\n ReadOptions,\n WriteOptions,\n ListOptions,\n RemoveOptions,\n CopyOptions,\n} from './filesystem';\n\n/**\n * Lifecycle hook that fires during filesystem state transitions.\n * Receives the filesystem instance so users can inspect state, log, etc.\n */\nexport type FilesystemLifecycleHook = (args: { filesystem: WorkspaceFilesystem }) => void | Promise<void>;\n\n/**\n * Options for the MastraFilesystem base class constructor.\n * Providers extend this to add their own options while inheriting lifecycle hooks.\n */\nexport interface MastraFilesystemOptions {\n /** Called after the filesystem reaches 'ready' status */\n onInit?: FilesystemLifecycleHook;\n /** Called before the filesystem is destroyed */\n onDestroy?: FilesystemLifecycleHook;\n}\n\n/**\n * Abstract base class for filesystem providers with logger support and lifecycle management.\n *\n * Providers that extend this class automatically receive the Mastra logger\n * when the filesystem is used with a Mastra instance.\n *\n * @example\n * ```typescript\n * class MyCustomFilesystem extends MastraFilesystem {\n * readonly id = 'my-fs';\n * readonly name = 'MyCustomFilesystem';\n * readonly provider = 'custom';\n * status: ProviderStatus = 'pending';\n *\n * constructor() {\n * super({ name: 'MyCustomFilesystem' });\n * }\n *\n * // Override init() to provide initialization logic\n * async init(): Promise<void> {\n * // Your initialization logic here\n * }\n *\n * async readFile(path: string): Promise<string | Buffer> {\n * await this.ensureReady();\n * this.logger.debug('Reading file', { path });\n * // Implementation...\n * }\n * // ... implement other WorkspaceFilesystem methods\n * }\n * ```\n */\nexport abstract class MastraFilesystem extends MastraBase implements WorkspaceFilesystem {\n /** Unique identifier for this filesystem instance */\n abstract readonly id: string;\n\n /** Human-readable name (e.g., 'LocalFilesystem', 'AgentFS') */\n abstract readonly name: string;\n\n /** Provider type identifier */\n abstract readonly provider: string;\n\n /** Current status of the filesystem */\n abstract status: ProviderStatus;\n\n /** Error message when status is 'error' */\n error?: string;\n\n // ---------------------------------------------------------------------------\n // Lifecycle Promise Tracking (prevents race conditions)\n // ---------------------------------------------------------------------------\n\n /** Promise for _init() to prevent race conditions from concurrent calls */\n private _initPromise?: Promise<void>;\n\n /** Promise for _destroy() to prevent race conditions from concurrent calls */\n private _destroyPromise?: Promise<void>;\n\n /** Lifecycle callbacks */\n private readonly _onInit?: FilesystemLifecycleHook;\n private readonly _onDestroy?: FilesystemLifecycleHook;\n\n constructor(options: { name: string } & MastraFilesystemOptions) {\n super({ name: options.name, component: RegisteredLogger.WORKSPACE });\n\n this._onInit = options.onInit;\n this._onDestroy = options.onDestroy;\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle Wrappers (race-condition-safe)\n // ---------------------------------------------------------------------------\n\n /**\n * Initialize the filesystem (wrapper with status management and race-condition safety).\n *\n * This method is race-condition-safe - concurrent calls will return the same promise.\n * Handles status management automatically.\n *\n * Subclasses override `init()` to provide their initialization logic.\n */\n async _init(): Promise<void> {\n // Already ready\n // Note: intentionally allows re-init after destroy() for reconnect scenarios\n if (this.status === 'ready') {\n return;\n }\n\n // Wait for any in-progress destroy to complete before (re-)initializing\n if (this._destroyPromise) {\n try {\n await this._destroyPromise;\n } catch {\n // Ignore destroy errors — we're re-initializing anyway\n }\n }\n\n // Init already in progress - return existing promise\n if (this._initPromise) {\n return this._initPromise;\n }\n\n // Create and store the init promise\n this._initPromise = this._executeInit();\n\n try {\n await this._initPromise;\n } finally {\n this._initPromise = undefined;\n }\n }\n\n /**\n * Internal init execution - handles status.\n */\n private async _executeInit(): Promise<void> {\n this.status = 'initializing';\n this.error = undefined;\n\n try {\n await this.init();\n this.status = 'ready';\n\n // Fire onInit callback after filesystem is ready — treat failure as non-fatal\n // so that a bad callback doesn't kill an otherwise healthy filesystem\n try {\n await this._onInit?.({ filesystem: this });\n } catch (error) {\n this.logger.warn('onInit callback failed', { error });\n }\n } catch (error) {\n this.status = 'error';\n this.error = error instanceof Error ? error.message : String(error);\n this.logger.error('Failed to initialize filesystem', { error, id: this.id });\n throw error;\n }\n }\n\n /**\n * Override this method to implement filesystem initialization logic.\n *\n * Called by `_init()` after status is set to 'initializing'.\n * Status will be set to 'ready' on success, 'error' on failure.\n *\n * @example\n * ```typescript\n * async init(): Promise<void> {\n * this._client = new StorageClient({ ... });\n * await this._client.connect();\n * }\n * ```\n */\n async init(): Promise<void> {\n // Default no-op - subclasses override\n }\n\n /**\n * Ensure the filesystem is ready.\n *\n * Calls `_init()` if status is not 'ready'. Useful for lazy initialization\n * where operations should automatically initialize the filesystem if needed.\n *\n * @throws {FilesystemNotReadyError} if the filesystem fails to reach 'ready' status\n *\n * @example\n * ```typescript\n * async readFile(path: string): Promise<string | Buffer> {\n * await this.ensureReady();\n * // Now safe to use the filesystem\n * }\n * ```\n */\n protected async ensureReady(): Promise<void> {\n if (this.status !== 'ready') {\n await this._init();\n }\n if (this.status !== 'ready') {\n throw new FilesystemNotReadyError(this.id);\n }\n }\n\n /**\n * Destroy the filesystem and clean up all resources (wrapper with status management).\n *\n * This method is race-condition-safe - concurrent calls will return the same promise.\n * Handles status management.\n *\n * Subclasses override `destroy()` to provide their destroy logic.\n */\n async _destroy(): Promise<void> {\n // Already destroyed\n if (this.status === 'destroyed') {\n return;\n }\n\n // Never initialized — nothing to tear down\n if (this.status === 'pending') {\n this.status = 'destroyed';\n return;\n }\n\n // Destroy already in progress - return existing promise\n if (this._destroyPromise) {\n return this._destroyPromise;\n }\n\n // Create and store the destroy promise\n this._destroyPromise = this._executeDestroy();\n\n try {\n await this._destroyPromise;\n } finally {\n this._destroyPromise = undefined;\n }\n }\n\n /**\n * Internal destroy execution - handles status.\n */\n private async _executeDestroy(): Promise<void> {\n // Wait for any in-progress init to complete before destroying\n if (this._initPromise) {\n try {\n await this._initPromise;\n } catch {\n // Ignore init errors — we're destroying anyway\n }\n }\n this.status = 'destroying';\n\n try {\n // Fire onDestroy callback before destroying\n await this._onDestroy?.({ filesystem: this });\n\n await this.destroy();\n this.status = 'destroyed';\n } catch (error) {\n this.status = 'error';\n this.logger.error('Failed to destroy filesystem', { error, id: this.id });\n throw error;\n }\n }\n\n /**\n * Override this method to implement filesystem destroy logic.\n *\n * Called by `_destroy()` after status is set to 'destroying'.\n * Status will be set to 'destroyed' on success, 'error' on failure.\n */\n async destroy(): Promise<void> {\n // Default no-op - subclasses override\n }\n\n // ---------------------------------------------------------------------------\n // Abstract methods - implementations must provide these\n // ---------------------------------------------------------------------------\n\n abstract readFile(path: string, options?: ReadOptions): Promise<string | Buffer>;\n abstract writeFile(path: string, content: FileContent, options?: WriteOptions): Promise<void>;\n abstract appendFile(path: string, content: FileContent): Promise<void>;\n abstract deleteFile(path: string, options?: RemoveOptions): Promise<void>;\n abstract copyFile(src: string, dest: string, options?: CopyOptions): Promise<void>;\n abstract moveFile(src: string, dest: string, options?: CopyOptions): Promise<void>;\n abstract mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;\n abstract rmdir(path: string, options?: RemoveOptions): Promise<void>;\n abstract readdir(path: string, options?: ListOptions): Promise<FileEntry[]>;\n abstract exists(path: string): Promise<boolean>;\n abstract stat(path: string): Promise<FileStat>;\n}\n","import type { RequestContext } from '../request-context';\nimport type { InstructionsOption } from './types';\n\n/**\n * Resolve an instructions override against default instructions.\n *\n * - `undefined` → return default\n * - `string` → return the string as-is\n * - `function` → call with { defaultInstructions, requestContext }\n */\nexport function resolveInstructions(\n override: InstructionsOption | undefined,\n getDefault: () => string,\n requestContext?: RequestContext,\n): string {\n if (typeof override === 'string') return override;\n const defaultInstructions = getDefault();\n if (override === undefined) return defaultInstructions;\n return override({ defaultInstructions, requestContext });\n}\n","/**\n * Local Filesystem Provider\n *\n * A filesystem implementation backed by a folder on the local disk.\n * This is the default filesystem for development and local agents.\n */\n\nimport { constants as fsConstants, realpathSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as nodePath from 'node:path';\nimport type { RequestContext } from '../../request-context';\nimport {\n FileNotFoundError,\n DirectoryNotFoundError,\n FileExistsError,\n IsDirectoryError,\n NotDirectoryError,\n DirectoryNotEmptyError,\n PermissionError,\n StaleFileError,\n WorkspaceReadOnlyError,\n} from '../errors';\nimport type { ProviderStatus } from '../lifecycle';\nimport type { InstructionsOption } from '../types';\nimport { resolveInstructions } from '../utils';\nimport type {\n FilesystemInfo,\n FileContent,\n FileStat,\n FileEntry,\n ReadOptions,\n WriteOptions,\n ListOptions,\n RemoveOptions,\n CopyOptions,\n} from './filesystem';\nimport { expandTilde, fsExists, fsStat, isEnoentError, isEexistError, resolveToBasePath } from './fs-utils';\nimport { MastraFilesystem } from './mastra-filesystem';\nimport type { MastraFilesystemOptions } from './mastra-filesystem';\nimport type { FilesystemMountConfig } from './mount';\n\n/**\n * Local filesystem provider configuration.\n */\nexport interface LocalFilesystemOptions extends MastraFilesystemOptions {\n /** Unique identifier for this filesystem instance */\n id?: string;\n /** Base directory path on disk */\n basePath: string;\n /**\n * When true, all file operations are restricted to stay within basePath.\n * Prevents path traversal attacks and symlink escapes.\n *\n * - `contained: true` (default) — File access is restricted to basePath\n * (and any allowedPaths). Paths that escape these boundaries throw a\n * PermissionError.\n * - `contained: false` — No access restrictions. Any path on the host\n * filesystem is accessible.\n *\n * Set to `false` when the filesystem needs to access paths outside basePath,\n * such as global skills directories or user home directories.\n *\n * @default true\n */\n contained?: boolean;\n /**\n * When true, all write operations to this filesystem are blocked.\n * Read operations are still allowed.\n * @default false\n */\n readOnly?: boolean;\n /**\n * Additional directories the agent can access outside of `basePath`.\n *\n * Relative paths resolve against `basePath`.\n * Absolute and tilde paths are used as-is.\n *\n * @example\n * ```typescript\n * new LocalFilesystem({\n * basePath: './workspace',\n * contained: true,\n * allowedPaths: ['../skills', '~/.claude/skills'],\n * })\n * ```\n */\n allowedPaths?: string[];\n /**\n * Custom instructions that override the default instructions\n * returned by `getInstructions()`.\n *\n * - `string` — Fully replaces the default instructions.\n * Pass an empty string to suppress instructions entirely.\n * - `(opts) => string` — Receives the default instructions and\n * optional request context so you can extend or customise per-request.\n */\n instructions?: InstructionsOption;\n}\n\n/**\n * Mount configuration for local filesystems.\n *\n * When a `LocalFilesystem` is used as a mount in a Workspace with `LocalSandbox`,\n * the sandbox creates a symlink from `<workingDir>/<mountPath>` → `basePath`.\n * No FUSE tools are needed for local mounts.\n *\n * **Note:** When mounted with `contained: false`, the agent can access any\n * path on the host filesystem through this mount. Workspace logs a warning\n * at construction time if this combination is detected.\n */\nexport interface LocalMountConfig extends FilesystemMountConfig {\n type: 'local';\n basePath: string;\n}\n\n/**\n * Local filesystem implementation.\n *\n * Stores files in a folder on the user's machine.\n * This is the recommended filesystem for development and persistent local storage.\n *\n * @example\n * ```typescript\n * import { Workspace, LocalFilesystem } from '@mastra/core';\n *\n * const workspace = new Workspace({\n * filesystem: new LocalFilesystem({ basePath: './my-workspace' }),\n * });\n *\n * await workspace.init();\n * await workspace.writeFile('hello.txt', 'Hello World!');\n * ```\n */\nexport class LocalFilesystem extends MastraFilesystem {\n readonly id: string;\n readonly name = 'LocalFilesystem';\n readonly provider = 'local';\n readonly readOnly?: boolean;\n\n status: ProviderStatus = 'pending';\n\n private readonly _basePath: string;\n private readonly _contained: boolean;\n private _allowedPaths: string[];\n private readonly _instructionsOverride?: InstructionsOption;\n\n /**\n * The absolute base path on disk where files are stored.\n * Useful for understanding how workspace paths map to disk paths.\n */\n get basePath(): string {\n return this._basePath;\n }\n\n /**\n * Whether file operations are restricted to stay within basePath.\n *\n * When `true` (default), relative paths resolve against basePath and\n * absolute paths are kept as-is. Any resolved path that falls outside\n * basePath (and allowedPaths) throws a PermissionError. When `false`,\n * no containment check is applied.\n *\n * **Note:** When used as a CompositeFilesystem mount with `contained: false`,\n * the agent can access any path on the host filesystem through this mount.\n */\n get contained(): boolean {\n return this._contained;\n }\n\n /**\n * Current set of resolved allowed paths.\n * These paths are permitted beyond basePath when containment is enabled.\n */\n get allowedPaths(): readonly string[] {\n return this._allowedPaths;\n }\n\n /**\n * Update allowed paths. Accepts a direct array or an updater callback\n * receiving the current paths (React setState pattern).\n *\n * @example\n * ```typescript\n * // Set directly\n * fs.setAllowedPaths(['../shared-data']);\n *\n * // Update with callback\n * fs.setAllowedPaths(prev => [...prev, '~/.claude/skills']);\n * ```\n */\n setAllowedPaths(pathsOrUpdater: string[] | ((current: readonly string[]) => string[])): void {\n const newPaths = typeof pathsOrUpdater === 'function' ? pathsOrUpdater(this._allowedPaths) : pathsOrUpdater;\n this._allowedPaths = newPaths.map(p => resolveToBasePath(this._basePath, p));\n }\n\n constructor(options: LocalFilesystemOptions) {\n super({ ...options, name: 'LocalFilesystem' });\n this.id = options.id ?? this.generateId();\n this._basePath = nodePath.resolve(expandTilde(options.basePath));\n this._contained = options.contained ?? true;\n this.readOnly = options.readOnly;\n this._allowedPaths = (options.allowedPaths ?? []).map(p => resolveToBasePath(this._basePath, p));\n this._instructionsOverride = options.instructions;\n }\n\n /**\n * Return mount config for sandbox integration.\n * LocalSandbox uses this to create a symlink from the mount path to basePath.\n */\n getMountConfig(): LocalMountConfig {\n return { type: 'local', basePath: this._basePath };\n }\n\n private generateId(): string {\n return `local-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n }\n\n /**\n * Check if an absolute path falls within basePath or any allowed path.\n */\n private _isWithinRoot(absolutePath: string, root: string): boolean {\n const relative = nodePath.relative(root, absolutePath);\n return !relative.startsWith('..') && !nodePath.isAbsolute(relative);\n }\n\n private _resolvePathForContainment(absolutePath: string): string | undefined {\n let currentPath = absolutePath;\n\n while (true) {\n try {\n const realPath = realpathSync(currentPath);\n if (currentPath === absolutePath) {\n return realPath;\n }\n\n const remainder = nodePath.relative(currentPath, absolutePath);\n return nodePath.join(realPath, remainder);\n } catch (error: unknown) {\n if (!isEnoentError(error)) return undefined;\n }\n\n const parentPath = nodePath.dirname(currentPath);\n if (parentPath === currentPath) {\n return undefined;\n }\n currentPath = parentPath;\n }\n }\n\n private _isWithinAnyRoot(absolutePath: string): boolean {\n const roots = [this._basePath, ...this._allowedPaths];\n if (roots.some(root => this._isWithinRoot(absolutePath, root))) {\n return true;\n }\n\n const resolvedPath = this._resolvePathForContainment(absolutePath);\n if (!resolvedPath) {\n return false;\n }\n\n return roots.some(root => {\n const resolvedRoot = this._resolvePathForContainment(root);\n return resolvedRoot ? this._isWithinRoot(resolvedPath, resolvedRoot) : false;\n });\n }\n\n private toBuffer(content: FileContent): Buffer {\n if (Buffer.isBuffer(content)) return content;\n if (content instanceof Uint8Array) return Buffer.from(content);\n return Buffer.from(content, 'utf-8');\n }\n\n private resolvePath(inputPath: string): string {\n const absolutePath = resolveToBasePath(this._basePath, inputPath);\n\n if (this._contained) {\n if (!this._isWithinAnyRoot(absolutePath)) {\n throw new PermissionError(inputPath, this._accessOperationHint(inputPath));\n }\n }\n\n return absolutePath;\n }\n\n /**\n * Build the operation string for a containment-violation `PermissionError`.\n *\n * When the caller passed an absolute path, suggest a concrete relative form\n * only when that suffix names an existing entry under the workspace (e.g.\n * `/src/app.ts` → `src/app.ts` if `<basePath>/src` exists). Otherwise emit a\n * soft hint that doesn't lie about specific paths — agents that mistake `/`\n * for the workspace root learn the workspace is sandboxed without us\n * inventing a fictitious in-workspace location for `/etc/passwd`.\n */\n private _accessOperationHint(inputPath: string): string {\n if (!nodePath.isAbsolute(inputPath)) return 'access';\n\n const