@dbx-tools/appkit-mastra 0.1.93 → 0.1.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import { ChartSchema, ChartTypeSchema, DEFAULT_COMMENT_NAME, DEFAULT_FEEDBACK_NAME, MASTRA_ROUTES, MLFLOW_TRACE_ID_HEADER, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, MastraFeedbackRequestSchema, MastraUpdateThreadRequestSchema, THREAD_ID_HEADER, THREAD_ID_QUERY } from "@dbx-tools/appkit-mastra-shared";
2
2
  import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS, FALLBACK_MODEL_IDS, ModelClass, ModelClass as ModelClass$1, clearServingEndpointsCache, listServingEndpoints, parseModelClass, selectModel } from "@dbx-tools/model";
3
- import { apiUtils, appkitUtils, commonUtils, httpUtils, logUtils, netUtils, projectUtils, stringUtils } from "@dbx-tools/shared";
3
+ import { CompositeFilesystem, DirectoryNotEmptyError, DirectoryNotFoundError, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, NotDirectoryError, PermissionError, Workspace, WorkspaceReadOnlyError } from "@mastra/core/workspace";
4
+ import { apiUtils, appkitUtils, commonUtils, httpUtils, logUtils, netUtils, projectUtils, stringUtils, tokenUtils } from "@dbx-tools/shared";
5
+ import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
6
+ import { ApiError, WorkspaceClient } from "@databricks/sdk-experimental";
7
+ import { posix } from "node:path";
8
+ import { randomUUID } from "node:crypto";
9
+ import { CacheManager, Plugin, genie, getExecutionContext, getUsernameWithApiLookup, lakebase, toPlugin } from "@databricks/appkit";
4
10
  import { Agent } from "@mastra/core/agent";
5
11
  import { createTool, createTool as createTool$1 } from "@mastra/core/tools";
6
- import { CacheManager, Plugin, genie, getExecutionContext, getUsernameWithApiLookup, lakebase, toPlugin } from "@databricks/appkit";
7
12
  import { z } from "zod";
8
- import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
9
- import { WorkspaceClient } from "@databricks/sdk-experimental";
10
13
  import { genieEventChat, genieSampleQuestions, getGenieSpace } from "@dbx-tools/genie";
11
14
  import { GenieMessageSchema } from "@dbx-tools/genie-shared";
12
15
  import { MCPServer } from "@mastra/mcp";
@@ -17,7 +20,6 @@ import { registerApiRoute } from "@mastra/core/server";
17
20
  import { fastembed } from "@mastra/fastembed";
18
21
  import { Memory } from "@mastra/memory";
19
22
  import { PgVector, PostgresStore } from "@mastra/pg";
20
- import { randomUUID } from "node:crypto";
21
23
  import { Pool } from "pg";
22
24
  import { Observability } from "@mastra/observability";
23
25
  import { OtelBridge } from "@mastra/otel-bridge";
@@ -54,6 +56,13 @@ const MASTRA_USER_EMAIL_KEY = "mastra__userEmail";
54
56
  */
55
57
  const MASTRA_REQUEST_ID_KEY = "mastra__requestId";
56
58
  /**
59
+ * `RequestContext` key for OAuth scopes parsed from the forwarded
60
+ * access token by {@link MastraServer.configureRequestContextScopes}.
61
+ * Workspace mounts that touch Databricks workspace files require
62
+ * `workspace` or `all-apis` in this list.
63
+ */
64
+ const MASTRA_SCOPES_KEY = "mastra__scopes";
65
+ /**
57
66
  * Canonical list of `RequestContext` keys we want Mastra to extract
58
67
  * as metadata on every observability span (agent runs, model calls,
59
68
  * tool invocations, workflow steps).
@@ -76,6 +85,965 @@ const TRACE_REQUEST_CONTEXT_KEYS = [
76
85
  "mastra__model_override"
77
86
  ];
78
87
 
88
+ //#endregion
89
+ //#region packages/appkit-mastra/src/filesystems.ts
90
+ /**
91
+ * Mastra {@link WorkspaceFilesystem} implementations for Databricks Apps.
92
+ *
93
+ * {@link DatabricksWorkspaceFilesystem} maps a Mastra workspace namespace onto
94
+ * an absolute Databricks path (Unity Catalog volume, workspace object tree, or
95
+ * DBFS). {@link emptyFilesystem} is a read-only no-op mount used when no
96
+ * dynamic mounts resolve for a request.
97
+ *
98
+ * Path helpers ({@link normalizeDatabricksBasePath}, {@link isDbfsPath}, …)
99
+ * are exported for tests and callers that need to reason about Databricks
100
+ * paths without constructing a filesystem.
101
+ */
102
+ const DBFS_READ_CHUNK_BYTES = 1024 * 1024;
103
+ const DBFS_PUT_MAX_BYTES = 1024 * 1024;
104
+ const EMPTY_FILESYSTEM_EPOCH = /* @__PURE__ */ new Date(0);
105
+ /** Normalize a Databricks base path (POSIX, no trailing slash). */
106
+ function normalizeDatabricksBasePath(basePath) {
107
+ const trimmed = basePath.trim();
108
+ if (!trimmed.startsWith("/")) throw new Error(`Databricks base path must be absolute: ${basePath}`);
109
+ return trimmed.replace(/\/+$/, "") || "/";
110
+ }
111
+ /** True when `absolutePath` is served by DBFS rather than the UC Files API. */
112
+ function isDbfsPath(absolutePath) {
113
+ return absolutePath === "/dbfs" || absolutePath.startsWith("/dbfs/");
114
+ }
115
+ /** True when `absolutePath` is a Databricks workspace object path. */
116
+ function isWorkspaceFilesPath(absolutePath) {
117
+ return absolutePath === "/Workspace" || absolutePath.startsWith("/Workspace/") || absolutePath.startsWith("/Users/") || absolutePath.startsWith("/Repos/");
118
+ }
119
+ /**
120
+ * Resolve a workspace-relative path to an absolute Databricks path under
121
+ * `basePath`.
122
+ */
123
+ function resolveDatabricksAbsolutePath(basePath, inputPath) {
124
+ const root = normalizeDatabricksBasePath(basePath);
125
+ const trimmed = inputPath.trim();
126
+ if (!trimmed || trimmed === "/") return root;
127
+ const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
128
+ if (normalized === root || normalized.startsWith(`${root}/`)) return posix.normalize(normalized);
129
+ return posix.normalize(posix.join(root, normalized));
130
+ }
131
+ /** Map an absolute Databricks path back to the workspace namespace. */
132
+ function toDatabricksWorkspacePath(basePath, absolutePath) {
133
+ const root = normalizeDatabricksBasePath(basePath);
134
+ const normalized = posix.normalize(absolutePath);
135
+ if (normalized === root) return "/";
136
+ if (normalized.startsWith(`${root}/`)) return normalized.slice(root.length) || "/";
137
+ return normalized;
138
+ }
139
+ /** Pick the Databricks Files API backend for an absolute path. */
140
+ function resolveFilesBackend(absolutePath) {
141
+ if (isDbfsPath(absolutePath)) return "dbfs";
142
+ if (isWorkspaceFilesPath(absolutePath)) return "workspace";
143
+ return "uc-files";
144
+ }
145
+ /** Run the handler that matches `absolutePath`'s Databricks backend. */
146
+ async function dispatchFilesBackend(absolutePath, handlers) {
147
+ const backend = resolveFilesBackend(absolutePath);
148
+ if (backend === "dbfs") return handlers.dbfs();
149
+ if (backend === "workspace") return handlers.workspace();
150
+ return handlers.ucFiles();
151
+ }
152
+ /** Return `buffer` as a string when `encoding` is set, otherwise unchanged. */
153
+ function formatReadResult(buffer, encoding) {
154
+ return encoding ? buffer.toString(encoding) : buffer;
155
+ }
156
+ /** Drain a fetch `ReadableStream` into a single `Buffer`. */
157
+ async function readResponseBody(contents) {
158
+ if (!contents) return Buffer.alloc(0);
159
+ const reader = contents.getReader();
160
+ const chunks = [];
161
+ while (true) {
162
+ const { done, value } = await reader.read();
163
+ if (done) break;
164
+ if (value) chunks.push(value);
165
+ }
166
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
167
+ }
168
+ /** Coerce Mastra {@link FileContent} to a `Buffer`. */
169
+ function toBuffer(content) {
170
+ if (typeof content === "string") return Buffer.from(content, "utf8");
171
+ return Buffer.from(content);
172
+ }
173
+ /** Decode a DBFS read payload from base64. */
174
+ function decodeDbfsPayload(data) {
175
+ if (!data) return Buffer.alloc(0);
176
+ return Buffer.from(data, "base64");
177
+ }
178
+ /** Wrap a `Buffer` as a one-shot `ReadableStream` for UC file uploads. */
179
+ function bufferToReadableStream(buffer) {
180
+ return new ReadableStream({ start(controller) {
181
+ controller.enqueue(new Uint8Array(buffer));
182
+ controller.close();
183
+ } });
184
+ }
185
+ /** Parse an HTTP date header; returns epoch when missing or invalid. */
186
+ function parseHttpDate(value) {
187
+ if (!value) return /* @__PURE__ */ new Date(0);
188
+ const parsed = Date.parse(value);
189
+ return Number.isFinite(parsed) ? new Date(parsed) : /* @__PURE__ */ new Date(0);
190
+ }
191
+ /**
192
+ * Mastra filesystem provider that reads and writes through a Databricks
193
+ * {@link WorkspaceClient}.
194
+ *
195
+ * Workspace paths are absolute within the namespace (`/notes.md` maps to
196
+ * `<basePath>/notes.md`). Unity Catalog volumes use the Files API;
197
+ * `/dbfs/...` paths use DBFS.
198
+ */
199
+ var DatabricksWorkspaceFilesystem = class extends MastraFilesystem {
200
+ id;
201
+ name = "DatabricksWorkspaceFilesystem";
202
+ provider = "databricks";
203
+ basePath;
204
+ status = "pending";
205
+ client;
206
+ requireBasePath;
207
+ _readOnly;
208
+ _basePathMissing;
209
+ get readOnly() {
210
+ return this._readOnly;
211
+ }
212
+ /**
213
+ * @param options.client - Defaults to the AppKit execution-context client.
214
+ * @param options.readOnly - When omitted, {@link init} probes write access.
215
+ * @param options.requireBasePath - When `false` (default), a missing
216
+ * {@link basePath} yields an empty namespace instead of errors.
217
+ */
218
+ constructor(options) {
219
+ super({
220
+ name: "DatabricksWorkspaceFilesystem",
221
+ ...options
222
+ });
223
+ this.id = options.id ?? `databricks-fs-${commonUtils.fnvHash(options.basePath)}`;
224
+ this.client = options.client ?? getExecutionContext().client;
225
+ this.basePath = normalizeDatabricksBasePath(options.basePath);
226
+ this.requireBasePath = options.requireBasePath ?? false;
227
+ this._readOnly = options.readOnly;
228
+ }
229
+ /** Resolve and sandbox a workspace-relative path under {@link basePath}. */
230
+ resolvePath(inputPath) {
231
+ const resolved = resolveDatabricksAbsolutePath(this.basePath, inputPath);
232
+ if (resolved !== this.basePath && !resolved.startsWith(`${this.basePath}/`)) throw new PermissionError(inputPath, "access");
233
+ return resolved;
234
+ }
235
+ /** Map a Databricks absolute path back to the workspace namespace. */
236
+ workspacePath(absolutePath) {
237
+ return toDatabricksWorkspacePath(this.basePath, absolutePath);
238
+ }
239
+ /** Throw when the filesystem is read-only. */
240
+ assertWritable(operation) {
241
+ if (this.readOnly) throw new WorkspaceReadOnlyError(operation);
242
+ }
243
+ /** Map SDK / HTTP errors to Mastra workspace filesystem errors. */
244
+ rethrow(err, inputPath) {
245
+ const workspacePath = inputPath.startsWith("/") ? inputPath : this.workspacePath(this.resolvePath(inputPath));
246
+ if (apiUtils.isNotFoundError(err)) throw new FileNotFoundError(workspacePath);
247
+ if (err instanceof ApiError && err.errorCode === "RESOURCE_ALREADY_EXISTS") throw new FileExistsError(workspacePath);
248
+ if (err instanceof ApiError && err.errorCode === "NOT_DIRECTORY") throw new NotDirectoryError(workspacePath);
249
+ if (err instanceof ApiError && err.errorCode === "NOT_A_FILE") throw new IsDirectoryError(workspacePath);
250
+ const message = commonUtils.errorMessage(err);
251
+ throw new Error(`Databricks filesystem ${workspacePath}: ${message}`);
252
+ }
253
+ /**
254
+ * When {@link requireBasePath} is `false`, probe whether {@link basePath}
255
+ * exists and cache the result on {@link _basePathMissing}.
256
+ */
257
+ async resolveBasePathStatus() {
258
+ if (this._basePathMissing !== void 0) return;
259
+ try {
260
+ await this.assertAbsoluteReadable(this.basePath);
261
+ this._basePathMissing = false;
262
+ } catch (err) {
263
+ if (!this.requireBasePath && apiUtils.isNotFoundError(err)) {
264
+ this._basePathMissing = true;
265
+ return;
266
+ }
267
+ this.rethrow(err, "/");
268
+ }
269
+ }
270
+ /** Delegate to {@link emptyFilesystem} when the optional base path is missing. */
271
+ async emptyFallback() {
272
+ if (this.requireBasePath) return void 0;
273
+ await this.resolveBasePathStatus();
274
+ return this._basePathMissing ? emptyFilesystem() : void 0;
275
+ }
276
+ async init() {
277
+ await this.resolveBasePathStatus();
278
+ if (this._basePathMissing) return;
279
+ if (!this.readOnly) await this.ensureBaseExists();
280
+ if (this._readOnly === void 0) await this.probeReadOnly();
281
+ }
282
+ /**
283
+ * Create the base directory when missing and writes are allowed.
284
+ *
285
+ * Uses raw SDK errors for the not-found branch so {@link apiUtils.isNotFoundError}
286
+ * still matches before {@link rethrow} wraps them as Mastra errors.
287
+ */
288
+ async ensureBaseExists() {
289
+ try {
290
+ await this.assertAbsoluteReadable(this.basePath);
291
+ } catch (err) {
292
+ if (!apiUtils.isNotFoundError(err)) this.rethrow(err, "/");
293
+ await this.mkdirAbsolute(this.basePath);
294
+ }
295
+ }
296
+ /**
297
+ * Write and delete a ephemeral probe file to detect read-only access when
298
+ * {@link DatabricksWorkspaceFilesystemOptions.readOnly} was not set.
299
+ *
300
+ * @returns `true` when the probe write (and cleanup) succeeded.
301
+ */
302
+ async probeReadOnly() {
303
+ const absolutePath = this.resolvePath(`/.__dbx_fs_probe_${commonUtils.id()}`);
304
+ try {
305
+ await this.writeAbsolute(absolutePath, Buffer.from("probe\n"), true);
306
+ this._readOnly = false;
307
+ } catch {
308
+ this._readOnly = true;
309
+ return false;
310
+ }
311
+ try {
312
+ await this.deleteAbsoluteFile(absolutePath);
313
+ } catch {}
314
+ return true;
315
+ }
316
+ async destroy() {}
317
+ /** Probe that `absolutePath` exists (file or directory metadata). */
318
+ async assertAbsoluteReadable(absolutePath) {
319
+ await dispatchFilesBackend(absolutePath, {
320
+ dbfs: () => this.client.dbfs.getStatus({ path: absolutePath }),
321
+ workspace: () => this.client.workspace.getStatus({ path: absolutePath }),
322
+ ucFiles: () => this.client.files.getDirectoryMetadata({ directory_path: absolutePath })
323
+ });
324
+ }
325
+ /** Read the full contents of `absolutePath` from the matching backend. */
326
+ async readAbsolute(absolutePath) {
327
+ return dispatchFilesBackend(absolutePath, {
328
+ dbfs: () => this.readDbfsFile(absolutePath),
329
+ workspace: () => this.readWorkspaceFile(absolutePath),
330
+ ucFiles: async () => {
331
+ return readResponseBody((await this.client.files.download({ file_path: absolutePath })).contents);
332
+ }
333
+ });
334
+ }
335
+ /** Write `buffer` to `absolutePath` on the matching backend. */
336
+ async writeAbsolute(absolutePath, buffer, overwrite) {
337
+ await dispatchFilesBackend(absolutePath, {
338
+ dbfs: () => this.writeDbfsFile(absolutePath, buffer, overwrite),
339
+ workspace: () => this.writeWorkspaceFile(absolutePath, buffer, overwrite),
340
+ ucFiles: () => this.uploadUcFile(absolutePath, buffer, overwrite)
341
+ });
342
+ }
343
+ /** Upload a buffer to a Unity Catalog Files API path. */
344
+ uploadUcFile(absolutePath, buffer, overwrite) {
345
+ return this.client.files.upload({
346
+ file_path: absolutePath,
347
+ contents: bufferToReadableStream(buffer),
348
+ overwrite
349
+ });
350
+ }
351
+ /** Delete a single file at `absolutePath` (non-recursive). */
352
+ async deleteAbsoluteFile(absolutePath) {
353
+ await dispatchFilesBackend(absolutePath, {
354
+ dbfs: () => this.client.dbfs.delete({
355
+ path: absolutePath,
356
+ recursive: false
357
+ }),
358
+ workspace: () => this.client.workspace.delete({
359
+ path: absolutePath,
360
+ recursive: false
361
+ }),
362
+ ucFiles: () => this.client.files.delete({ file_path: absolutePath })
363
+ });
364
+ }
365
+ /**
366
+ * Delete a file or directory at `absolutePath`.
367
+ *
368
+ * DBFS and workspace APIs accept `recursive`; UC volumes recurse manually.
369
+ */
370
+ async deleteAbsolutePath(absolutePath, recursive) {
371
+ await dispatchFilesBackend(absolutePath, {
372
+ dbfs: async () => {
373
+ await this.client.dbfs.delete({
374
+ path: absolutePath,
375
+ recursive
376
+ });
377
+ },
378
+ workspace: async () => {
379
+ await this.client.workspace.delete({
380
+ path: absolutePath,
381
+ recursive
382
+ });
383
+ },
384
+ ucFiles: async () => {
385
+ if (recursive) {
386
+ await this.deleteUcDirectoryRecursive(absolutePath);
387
+ return;
388
+ }
389
+ if ((await this.listAbsoluteDirectory(absolutePath)).length > 0) throw new DirectoryNotEmptyError(this.workspacePath(absolutePath));
390
+ await this.client.files.deleteDirectory({ directory_path: absolutePath });
391
+ }
392
+ });
393
+ }
394
+ /** Create `absolutePath` and any missing parents on the matching backend. */
395
+ async mkdirAbsolute(absolutePath) {
396
+ await dispatchFilesBackend(absolutePath, {
397
+ dbfs: () => this.client.dbfs.mkdirs({ path: absolutePath }),
398
+ workspace: () => this.client.workspace.mkdirs({ path: absolutePath }),
399
+ ucFiles: () => this.client.files.createDirectory({ directory_path: absolutePath })
400
+ });
401
+ }
402
+ async readDbfsFile(absolutePath) {
403
+ const chunks = [];
404
+ let offset = 0;
405
+ while (true) {
406
+ const chunk = decodeDbfsPayload((await this.client.dbfs.read({
407
+ path: absolutePath,
408
+ offset,
409
+ length: DBFS_READ_CHUNK_BYTES
410
+ })).data);
411
+ if (chunk.length === 0) break;
412
+ chunks.push(chunk);
413
+ offset += chunk.length;
414
+ if (chunk.length < DBFS_READ_CHUNK_BYTES) break;
415
+ }
416
+ return Buffer.concat(chunks);
417
+ }
418
+ async readWorkspaceFile(absolutePath) {
419
+ return decodeDbfsPayload((await this.client.workspace.export({
420
+ path: absolutePath,
421
+ format: "AUTO"
422
+ })).content);
423
+ }
424
+ async writeDbfsFile(absolutePath, buffer, overwrite) {
425
+ if (buffer.length <= DBFS_PUT_MAX_BYTES) {
426
+ await this.client.dbfs.put({
427
+ path: absolutePath,
428
+ contents: buffer.toString("base64"),
429
+ overwrite
430
+ });
431
+ return;
432
+ }
433
+ const handle = (await this.client.dbfs.create({
434
+ path: absolutePath,
435
+ overwrite
436
+ })).handle;
437
+ if (handle === void 0) throw new Error(`DBFS create did not return a handle for ${absolutePath}`);
438
+ for (let offset = 0; offset < buffer.length; offset += DBFS_PUT_MAX_BYTES) {
439
+ const slice = buffer.subarray(offset, offset + DBFS_PUT_MAX_BYTES);
440
+ await this.client.dbfs.addBlock({
441
+ handle,
442
+ data: slice.toString("base64")
443
+ });
444
+ }
445
+ await this.client.dbfs.close({ handle });
446
+ }
447
+ async writeWorkspaceFile(absolutePath, buffer, overwrite) {
448
+ await this.client.workspace.import({
449
+ path: absolutePath,
450
+ format: "AUTO",
451
+ content: buffer.toString("base64"),
452
+ overwrite
453
+ });
454
+ }
455
+ getInfo() {
456
+ return {
457
+ id: this.id,
458
+ name: this.name,
459
+ provider: this.provider,
460
+ status: this.status,
461
+ readOnly: this.readOnly,
462
+ metadata: { basePath: this.basePath }
463
+ };
464
+ }
465
+ getInstructions() {
466
+ return [
467
+ `Files live in Databricks under ${this.basePath}.`,
468
+ "Workspace paths are absolute within this root (for example `/notes/report.md`).",
469
+ "Workspace paths use `/Workspace/...`, `/Users/...`, or `/Repos/...` base paths.",
470
+ "Unity Catalog volumes use `/Volumes/<catalog>/<schema>/<volume>/...` base paths."
471
+ ].join(" ");
472
+ }
473
+ /** Map a workspace-relative path to the backing Databricks absolute path. */
474
+ resolveAbsolutePath(inputPath) {
475
+ return this.resolvePath(inputPath);
476
+ }
477
+ /** Read file contents from the workspace namespace. */
478
+ async readFile(inputPath, options) {
479
+ await this.ensureReady();
480
+ const empty = await this.emptyFallback();
481
+ if (empty) return empty.readFile(inputPath, options);
482
+ const absolutePath = this.resolvePath(inputPath);
483
+ try {
484
+ return formatReadResult(await this.readAbsolute(absolutePath), options?.encoding);
485
+ } catch (err) {
486
+ this.rethrow(err, inputPath);
487
+ }
488
+ }
489
+ /** Write file contents into the workspace namespace. */
490
+ async writeFile(inputPath, content, options) {
491
+ await this.ensureReady();
492
+ const empty = await this.emptyFallback();
493
+ if (empty) return empty.writeFile(inputPath, content, options);
494
+ this.assertWritable("writeFile");
495
+ const absolutePath = this.resolvePath(inputPath);
496
+ const buffer = toBuffer(content);
497
+ const overwrite = options?.overwrite ?? true;
498
+ try {
499
+ if (!overwrite && await this.exists(inputPath)) throw new FileExistsError(inputPath);
500
+ await this.writeAbsolute(absolutePath, buffer, overwrite);
501
+ } catch (err) {
502
+ if (err instanceof FileExistsError) throw err;
503
+ this.rethrow(err, inputPath);
504
+ }
505
+ }
506
+ /** Append to an existing file, creating it when missing. */
507
+ async appendFile(inputPath, content) {
508
+ await this.ensureReady();
509
+ const empty = await this.emptyFallback();
510
+ if (empty) return empty.appendFile(inputPath, content);
511
+ this.assertWritable("appendFile");
512
+ const existing = await this.exists(inputPath) ? await this.readFile(inputPath) : Buffer.alloc(0);
513
+ const merged = Buffer.concat([Buffer.isBuffer(existing) ? existing : Buffer.from(existing, "utf8"), toBuffer(content)]);
514
+ await this.writeFile(inputPath, merged, { overwrite: true });
515
+ }
516
+ /** Delete a file in the workspace namespace. */
517
+ async deleteFile(inputPath, options) {
518
+ await this.ensureReady();
519
+ const empty = await this.emptyFallback();
520
+ if (empty) return empty.deleteFile(inputPath, options);
521
+ this.assertWritable("deleteFile");
522
+ const absolutePath = this.resolvePath(inputPath);
523
+ try {
524
+ if (!await this.exists(inputPath)) {
525
+ if (options?.force) return;
526
+ throw new FileNotFoundError(inputPath);
527
+ }
528
+ if ((await this.stat(inputPath)).type === "directory") throw new IsDirectoryError(inputPath);
529
+ await this.deleteAbsoluteFile(absolutePath);
530
+ } catch (err) {
531
+ if (err instanceof FileNotFoundError || err instanceof IsDirectoryError) throw err;
532
+ this.rethrow(err, inputPath);
533
+ }
534
+ }
535
+ /** Copy a file within the workspace namespace. */
536
+ async copyFile(src, dest, options) {
537
+ await this.ensureReady();
538
+ const empty = await this.emptyFallback();
539
+ if (empty) return empty.copyFile(src, dest, options);
540
+ this.assertWritable("copyFile");
541
+ if (!(options?.overwrite ?? true) && await this.exists(dest)) throw new FileExistsError(dest);
542
+ const content = await this.readFile(src);
543
+ await this.writeFile(dest, content, { overwrite: true });
544
+ }
545
+ /** Move or rename a file within the workspace namespace. */
546
+ async moveFile(src, dest, options) {
547
+ await this.ensureReady();
548
+ const empty = await this.emptyFallback();
549
+ if (empty) return empty.moveFile(src, dest, options);
550
+ this.assertWritable("moveFile");
551
+ const srcAbsolute = this.resolvePath(src);
552
+ const destAbsolute = this.resolvePath(dest);
553
+ const overwrite = options?.overwrite ?? true;
554
+ try {
555
+ if (!overwrite && await this.exists(dest)) throw new FileExistsError(dest);
556
+ if (resolveFilesBackend(srcAbsolute) === "dbfs" && resolveFilesBackend(destAbsolute) === "dbfs") {
557
+ await this.client.dbfs.move({
558
+ source_path: srcAbsolute,
559
+ destination_path: destAbsolute
560
+ });
561
+ return;
562
+ }
563
+ await this.copyFile(src, dest, { overwrite: true });
564
+ await this.deleteFile(src, { force: true });
565
+ } catch (err) {
566
+ if (err instanceof FileExistsError) throw err;
567
+ this.rethrow(err, dest);
568
+ }
569
+ }
570
+ /** Create a directory in the workspace namespace. */
571
+ async mkdir(inputPath, options) {
572
+ await this.ensureReady();
573
+ const empty = await this.emptyFallback();
574
+ if (empty) return empty.mkdir(inputPath, options);
575
+ this.assertWritable("mkdir");
576
+ const absolutePath = this.resolvePath(inputPath);
577
+ try {
578
+ await this.mkdirAbsolute(absolutePath);
579
+ if (!options?.recursive) return;
580
+ } catch (err) {
581
+ this.rethrow(err, inputPath);
582
+ }
583
+ }
584
+ /** Remove a directory from the workspace namespace. */
585
+ async rmdir(inputPath, options) {
586
+ await this.ensureReady();
587
+ const empty = await this.emptyFallback();
588
+ if (empty) return empty.rmdir(inputPath, options);
589
+ this.assertWritable("rmdir");
590
+ const absolutePath = this.resolvePath(inputPath);
591
+ try {
592
+ if (!await this.exists(inputPath)) {
593
+ if (options?.force) return;
594
+ throw new DirectoryNotFoundError(inputPath);
595
+ }
596
+ if ((await this.stat(inputPath)).type !== "directory") throw new NotDirectoryError(inputPath);
597
+ await this.deleteAbsolutePath(absolutePath, options?.recursive ?? false);
598
+ } catch (err) {
599
+ if (err instanceof DirectoryNotFoundError || err instanceof NotDirectoryError || err instanceof DirectoryNotEmptyError) throw err;
600
+ this.rethrow(err, inputPath);
601
+ }
602
+ }
603
+ /**
604
+ * Recursively delete a Unity Catalog directory tree.
605
+ *
606
+ * DBFS and workspace trees use native recursive delete via
607
+ * {@link deleteAbsolutePath} instead.
608
+ */
609
+ async deleteUcDirectoryRecursive(absolutePath) {
610
+ for (const child of await this.listAbsoluteDirectory(absolutePath)) {
611
+ const childAbsolute = posix.join(absolutePath, child.name);
612
+ if (child.type === "directory") await this.deleteUcDirectoryRecursive(childAbsolute);
613
+ else await this.deleteAbsoluteFile(childAbsolute);
614
+ }
615
+ await this.client.files.deleteDirectory({ directory_path: absolutePath });
616
+ }
617
+ /** List entries in a workspace directory. */
618
+ async readdir(inputPath, options) {
619
+ await this.ensureReady();
620
+ const empty = await this.emptyFallback();
621
+ if (empty) return empty.readdir(inputPath, options);
622
+ const absolutePath = this.resolvePath(inputPath);
623
+ try {
624
+ const entries = await this.listAbsoluteDirectory(absolutePath);
625
+ const filtered = this.filterEntries(entries, options);
626
+ if (!options?.recursive) return filtered;
627
+ return this.readDirectoryRecursive(absolutePath, options, 0);
628
+ } catch (err) {
629
+ this.rethrow(err, inputPath);
630
+ }
631
+ }
632
+ async readDirectoryRecursive(absolutePath, options, depth, relativePrefix = "") {
633
+ const maxDepth = options.maxDepth ?? Number.POSITIVE_INFINITY;
634
+ const entries = this.filterEntries(await this.listAbsoluteDirectory(absolutePath), options);
635
+ const collected = [];
636
+ for (const entry of entries) {
637
+ const relativeName = relativePrefix ? posix.join(relativePrefix, entry.name) : entry.name;
638
+ collected.push({
639
+ ...entry,
640
+ name: relativeName
641
+ });
642
+ if (entry.type !== "directory" || depth >= maxDepth) continue;
643
+ collected.push(...await this.readDirectoryRecursive(posix.join(absolutePath, entry.name), options, depth + 1, relativeName));
644
+ }
645
+ return collected;
646
+ }
647
+ async listAbsoluteDirectory(absolutePath) {
648
+ return dispatchFilesBackend(absolutePath, {
649
+ dbfs: async () => {
650
+ const entries = [];
651
+ for await (const info of this.client.dbfs.list({ path: absolutePath })) entries.push({
652
+ name: posix.basename(info.path ?? ""),
653
+ type: info.is_dir ? "directory" : "file",
654
+ size: info.file_size
655
+ });
656
+ return entries;
657
+ },
658
+ workspace: async () => {
659
+ const entries = [];
660
+ for await (const info of this.client.workspace.list({ path: absolutePath })) entries.push({
661
+ name: posix.basename(info.path ?? ""),
662
+ type: info.object_type === "DIRECTORY" ? "directory" : "file"
663
+ });
664
+ return entries;
665
+ },
666
+ ucFiles: async () => {
667
+ const entries = [];
668
+ for await (const entry of this.client.files.listDirectoryContents({ directory_path: absolutePath })) entries.push({
669
+ name: entry.name ?? posix.basename(entry.path ?? ""),
670
+ type: entry.is_directory ? "directory" : "file",
671
+ size: entry.file_size
672
+ });
673
+ return entries;
674
+ }
675
+ });
676
+ }
677
+ /** Apply Mastra list filters (`extension`, etc.) to directory entries. */
678
+ filterEntries(entries, options) {
679
+ if (!options?.extension) return entries;
680
+ const normalized = (Array.isArray(options.extension) ? options.extension : [options.extension]).map((ext) => ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`);
681
+ return entries.filter((entry) => {
682
+ if (entry.type !== "file") return true;
683
+ const lower = entry.name.toLowerCase();
684
+ return normalized.some((ext) => lower.endsWith(ext));
685
+ });
686
+ }
687
+ /** Return whether `inputPath` exists in the workspace namespace. */
688
+ async exists(inputPath) {
689
+ await this.ensureReady();
690
+ const empty = await this.emptyFallback();
691
+ if (empty) return empty.exists(inputPath);
692
+ try {
693
+ await this.stat(inputPath);
694
+ return true;
695
+ } catch (err) {
696
+ if (err instanceof FileNotFoundError) return false;
697
+ throw err;
698
+ }
699
+ }
700
+ /** Return file or directory metadata for `inputPath`. */
701
+ async stat(inputPath) {
702
+ await this.ensureReady();
703
+ const empty = await this.emptyFallback();
704
+ if (empty) return empty.stat(inputPath);
705
+ const absolutePath = this.resolvePath(inputPath);
706
+ const workspacePath = this.workspacePath(absolutePath);
707
+ try {
708
+ return await dispatchFilesBackend(absolutePath, {
709
+ dbfs: async () => {
710
+ const info = await this.client.dbfs.getStatus({ path: absolutePath });
711
+ return {
712
+ name: posix.basename(info.path ?? absolutePath),
713
+ path: workspacePath,
714
+ type: info.is_dir ? "directory" : "file",
715
+ size: info.file_size ?? 0,
716
+ createdAt: new Date(info.modification_time ?? 0),
717
+ modifiedAt: new Date(info.modification_time ?? 0)
718
+ };
719
+ },
720
+ workspace: async () => {
721
+ const info = await this.client.workspace.getStatus({ path: absolutePath });
722
+ return {
723
+ name: posix.basename(info.path ?? absolutePath),
724
+ path: workspacePath,
725
+ type: info.object_type === "DIRECTORY" ? "directory" : "file",
726
+ size: 0,
727
+ createdAt: new Date(info.created_at ?? 0),
728
+ modifiedAt: new Date(info.modified_at ?? 0)
729
+ };
730
+ },
731
+ ucFiles: () => this.statUcAbsolute(absolutePath, workspacePath, inputPath)
732
+ });
733
+ } catch (err) {
734
+ this.rethrow(err, inputPath);
735
+ }
736
+ }
737
+ /**
738
+ * Stat a Unity Catalog path by probing file metadata first, then
739
+ * directory metadata.
740
+ */
741
+ async statUcAbsolute(absolutePath, workspacePath, inputPath) {
742
+ try {
743
+ const metadata = await this.client.files.getMetadata({ file_path: absolutePath });
744
+ return {
745
+ name: posix.basename(absolutePath),
746
+ path: workspacePath,
747
+ type: "file",
748
+ size: Number(metadata["content-length"] ?? 0),
749
+ createdAt: parseHttpDate(metadata["last-modified"]),
750
+ modifiedAt: parseHttpDate(metadata["last-modified"]),
751
+ mimeType: metadata["content-type"]
752
+ };
753
+ } catch (fileErr) {
754
+ if (!apiUtils.isNotFoundError(fileErr)) this.rethrow(fileErr, inputPath);
755
+ await this.client.files.getDirectoryMetadata({ directory_path: absolutePath });
756
+ return {
757
+ name: posix.basename(absolutePath),
758
+ path: workspacePath,
759
+ type: "directory",
760
+ size: 0,
761
+ createdAt: /* @__PURE__ */ new Date(0),
762
+ modifiedAt: /* @__PURE__ */ new Date(0)
763
+ };
764
+ }
765
+ }
766
+ };
767
+ /** Normalize paths for the in-memory empty filesystem namespace. */
768
+ function normalizeEmptyFilesystemPath(inputPath) {
769
+ const trimmed = inputPath.trim();
770
+ if (!trimmed || trimmed === ".") return "/";
771
+ const normalized = trimmed.startsWith("/") ? posix.normalize(trimmed) : posix.normalize(`/${trimmed}`);
772
+ return normalized === "." ? "/" : normalized;
773
+ }
774
+ /**
775
+ * Read-only in-memory {@link WorkspaceFilesystem} with a single empty root.
776
+ * Use {@link emptyFilesystem} rather than constructing directly.
777
+ */
778
+ var EmptyFilesystem = class extends MastraFilesystem {
779
+ id = "empty-fs";
780
+ name = "EmptyFilesystem";
781
+ provider = "empty";
782
+ readOnly = true;
783
+ status = "pending";
784
+ constructor() {
785
+ super({ name: "EmptyFilesystem" });
786
+ }
787
+ async init() {}
788
+ async destroy() {}
789
+ getInfo() {
790
+ return {
791
+ id: this.id,
792
+ name: this.name,
793
+ provider: this.provider,
794
+ status: this.status,
795
+ readOnly: this.readOnly
796
+ };
797
+ }
798
+ getInstructions() {
799
+ return "This filesystem is empty and read-only.";
800
+ }
801
+ rootStat() {
802
+ return {
803
+ name: "/",
804
+ path: "/",
805
+ type: "directory",
806
+ size: 0,
807
+ createdAt: EMPTY_FILESYSTEM_EPOCH,
808
+ modifiedAt: EMPTY_FILESYSTEM_EPOCH
809
+ };
810
+ }
811
+ assertWritable(operation) {
812
+ throw new WorkspaceReadOnlyError(operation);
813
+ }
814
+ async readFile(inputPath, _options) {
815
+ await this.ensureReady();
816
+ const normalized = normalizeEmptyFilesystemPath(inputPath);
817
+ if (normalized === "/") throw new IsDirectoryError(normalized);
818
+ throw new FileNotFoundError(normalized);
819
+ }
820
+ async writeFile(_inputPath, _content, _options) {
821
+ await this.ensureReady();
822
+ this.assertWritable("writeFile");
823
+ }
824
+ async appendFile(_inputPath, _content) {
825
+ await this.ensureReady();
826
+ this.assertWritable("appendFile");
827
+ }
828
+ async deleteFile(inputPath, options) {
829
+ await this.ensureReady();
830
+ const normalized = normalizeEmptyFilesystemPath(inputPath);
831
+ if (normalized === "/") throw new IsDirectoryError(normalized);
832
+ if (options?.force) return;
833
+ throw new FileNotFoundError(normalized);
834
+ }
835
+ async copyFile(src, _dest, _options) {
836
+ await this.ensureReady();
837
+ const normalizedSrc = normalizeEmptyFilesystemPath(src);
838
+ if (normalizedSrc !== "/") throw new FileNotFoundError(normalizedSrc);
839
+ throw new IsDirectoryError(normalizedSrc);
840
+ }
841
+ async moveFile(src, dest, options) {
842
+ await this.copyFile(src, dest, options);
843
+ }
844
+ async mkdir(_inputPath, _options) {
845
+ await this.ensureReady();
846
+ this.assertWritable("mkdir");
847
+ }
848
+ async rmdir(inputPath, options) {
849
+ await this.ensureReady();
850
+ const normalized = normalizeEmptyFilesystemPath(inputPath);
851
+ if (normalized === "/") throw new DirectoryNotEmptyError(normalized);
852
+ if (options?.force) return;
853
+ throw new DirectoryNotFoundError(normalized);
854
+ }
855
+ async readdir(inputPath, _options) {
856
+ await this.ensureReady();
857
+ const normalized = normalizeEmptyFilesystemPath(inputPath);
858
+ if (normalized === "/") return [];
859
+ throw new DirectoryNotFoundError(normalized);
860
+ }
861
+ async exists(inputPath) {
862
+ await this.ensureReady();
863
+ return normalizeEmptyFilesystemPath(inputPath) === "/";
864
+ }
865
+ async stat(inputPath) {
866
+ await this.ensureReady();
867
+ const normalized = normalizeEmptyFilesystemPath(inputPath);
868
+ if (normalized === "/") return this.rootStat();
869
+ throw new FileNotFoundError(normalized);
870
+ }
871
+ };
872
+ /** Memoized singleton empty read-only filesystem for no-op mounts. */
873
+ const emptyFilesystem = commonUtils.memoize(() => new EmptyFilesystem());
874
+
875
+ //#endregion
876
+ //#region packages/appkit-mastra/src/workspaces.ts
877
+ /** Shared Assistant skills tree in the workspace namespace. */
878
+ const ASSISTANT_SHARED_SKILLS_PATH = "/Workspace/.assistant/skills";
879
+ /** Composite mount for {@link ASSISTANT_SHARED_SKILLS_PATH}. */
880
+ const ASSISTANT_WORKSPACE_SKILLS_MOUNT = "/workspace_skills";
881
+ /** Composite mount for the caller's `/.assistant/skills` tree. */
882
+ const ASSISTANT_USER_SKILLS_MOUNT = "/workspace_user_skills";
883
+ /** OAuth scopes that gate Databricks workspace file mounts. */
884
+ const WORKSPACE_FILE_SCOPES = ["workspace", "all-apis"];
885
+ /**
886
+ * Create a Mastra {@link Workspace} with per-request Databricks mounts.
887
+ *
888
+ * @example Assistant skills only (default for agents in this plugin)
889
+ * ```ts
890
+ * createWorkspace()
891
+ * ```
892
+ *
893
+ * @example Assistant skills plus a custom mount resolver
894
+ * ```ts
895
+ * createWorkspace({
896
+ * assistantSkills: true,
897
+ * mounts: [
898
+ * async ({ requestContext }) => ({
899
+ * mounts: { "/data": myFilesystem },
900
+ * skillPaths: [],
901
+ * }),
902
+ * ],
903
+ * })
904
+ * ```
905
+ */
906
+ function createWorkspace(options = {}) {
907
+ const { id, name } = resolveWorkspaceIdentity(options);
908
+ const resolvers = buildMountResolvers(options);
909
+ const skills = options.skills ?? (resolvers.length > 0 ? buildWorkspaceSkillsResolver(resolvers) : void 0);
910
+ return new Workspace({
911
+ id,
912
+ name,
913
+ filesystem: (context) => resolveWorkspaceFilesystem(resolvers, context),
914
+ ...skills ? {
915
+ skills,
916
+ checkSkillFileMtime: options.checkSkillFileMtime ?? options.assistantSkills !== false
917
+ } : {},
918
+ ...options.bm25 !== false ? { bm25: true } : {}
919
+ });
920
+ }
921
+ /**
922
+ * Map an OBO user email to their Assistant skills directory in the
923
+ * workspace namespace.
924
+ */
925
+ function userAssistantSkillsPath(userEmail) {
926
+ return `/Users/${userEmail.trim()}/.assistant/skills`;
927
+ }
928
+ /**
929
+ * Return whether the request token carries a scope that allows workspace
930
+ * file API access (`workspace` or `all-apis` on {@link MASTRA_SCOPES_KEY}).
931
+ */
932
+ function hasWorkspaceFileScope(requestContext) {
933
+ return tokenUtils.includesAccessTokenScope(requestContext?.get(MASTRA_SCOPES_KEY), WORKSPACE_FILE_SCOPES);
934
+ }
935
+ /**
936
+ * Built-in mount resolver for Assistant `SKILL.md` trees.
937
+ *
938
+ * Mounts {@link ASSISTANT_SHARED_SKILLS_PATH} when scope checks pass and
939
+ * `/Users/<email>/.assistant/skills` when {@link MASTRA_USER_EMAIL_KEY} is
940
+ * set. Returns empty mounts when the OBO user or client is missing.
941
+ * Mastra owns filesystem initialization.
942
+ */
943
+ function resolveAssistantSkillsMounts(context) {
944
+ const mounts = {};
945
+ const requestContext = context.requestContext;
946
+ if (!shouldMountAssistantSkills(requestContext)) return {
947
+ mounts,
948
+ skillPaths: []
949
+ };
950
+ const client = requestContext.get(MASTRA_USER_KEY)?.executionContext.client;
951
+ if (!client) return {
952
+ mounts,
953
+ skillPaths: []
954
+ };
955
+ mounts[ASSISTANT_WORKSPACE_SKILLS_MOUNT] = readOnlyDatabricksFilesystem(client, ASSISTANT_SHARED_SKILLS_PATH);
956
+ const email = resolveScopedEmail(requestContext);
957
+ if (email) mounts[ASSISTANT_USER_SKILLS_MOUNT] = readOnlyDatabricksFilesystem(client, userAssistantSkillsPath(email));
958
+ return {
959
+ mounts,
960
+ skillPaths: Object.keys(mounts)
961
+ };
962
+ }
963
+ /**
964
+ * Fill in `id` and `name` when either is omitted on {@link CreateWorkspaceOptions}.
965
+ * Slugifies `name` into `id`; tokenizes `id` into a display `name`.
966
+ */
967
+ function resolveWorkspaceIdentity(options) {
968
+ let id = options.id;
969
+ let name = options.name;
970
+ if (!id) id = name ? stringUtils.toSlug(name) : "workspace";
971
+ if (!name) name = Array.from(stringUtils.tokenize(id)).join(" ");
972
+ return {
973
+ id,
974
+ name
975
+ };
976
+ }
977
+ /** Collect built-in and caller-supplied mount resolvers for one workspace. */
978
+ function buildMountResolvers(options) {
979
+ const resolvers = [];
980
+ const { assistantSkills = true, mounts } = options;
981
+ if (assistantSkills) resolvers.push(resolveAssistantSkillsMounts);
982
+ if (mounts?.length) resolvers.push(...mounts);
983
+ return resolvers;
984
+ }
985
+ /**
986
+ * Gate Assistant skill mounts on request context.
987
+ *
988
+ * Always allows mounts in development; in other environments requires
989
+ * {@link hasWorkspaceFileScope}.
990
+ */
991
+ function shouldMountAssistantSkills(requestContext) {
992
+ if (!requestContext) return false;
993
+ if (process.env.NODE_ENV === "development") return true;
994
+ return hasWorkspaceFileScope(requestContext);
995
+ }
996
+ /** Read the trimmed OBO user email stamped on {@link MASTRA_USER_EMAIL_KEY}. */
997
+ function resolveScopedEmail(requestContext) {
998
+ return (requestContext?.get(MASTRA_USER_EMAIL_KEY))?.trim() || void 0;
999
+ }
1000
+ /** Construct a read-only {@link DatabricksWorkspaceFilesystem} for `basePath`. */
1001
+ function readOnlyDatabricksFilesystem(client, basePath) {
1002
+ return new DatabricksWorkspaceFilesystem({
1003
+ client,
1004
+ basePath,
1005
+ readOnly: true
1006
+ });
1007
+ }
1008
+ /**
1009
+ * Run every mount resolver for one request and merge mounts plus skill paths.
1010
+ * Later resolvers overwrite mount keys from earlier ones.
1011
+ */
1012
+ async function resolveWorkspaceContribution(resolvers, context) {
1013
+ const mounts = {};
1014
+ const skillPaths = [];
1015
+ for (const resolver of resolvers) {
1016
+ const contribution = await resolver(context);
1017
+ Object.assign(mounts, contribution.mounts);
1018
+ if (contribution.skillPaths?.length) skillPaths.push(...contribution.skillPaths);
1019
+ }
1020
+ return {
1021
+ mounts,
1022
+ skillPaths
1023
+ };
1024
+ }
1025
+ /**
1026
+ * Dynamic filesystem resolver passed to Mastra {@link Workspace}.
1027
+ *
1028
+ * Returns a {@link CompositeFilesystem} when any mount resolved; otherwise
1029
+ * {@link emptyFilesystem}.
1030
+ */
1031
+ async function resolveWorkspaceFilesystem(resolvers, context) {
1032
+ const { mounts } = await resolveWorkspaceContribution(resolvers, context);
1033
+ if (Object.keys(mounts).length === 0) return emptyFilesystem();
1034
+ return new CompositeFilesystem({ mounts });
1035
+ }
1036
+ /**
1037
+ * Build the dynamic {@link SkillsResolver} that collects `skillPaths` from
1038
+ * every mount resolver on each request.
1039
+ */
1040
+ function buildWorkspaceSkillsResolver(resolvers) {
1041
+ return async (context) => {
1042
+ const { skillPaths } = await resolveWorkspaceContribution(resolvers, context);
1043
+ return skillPaths ?? [];
1044
+ };
1045
+ }
1046
+
79
1047
  //#endregion
80
1048
  //#region packages/appkit-mastra/src/serving-sanitize.ts
81
1049
  /**
@@ -2150,7 +3118,11 @@ function deriveToolId(description) {
2150
3118
  * type inference and to match the AppKit API surface.
2151
3119
  */
2152
3120
  function createAgent(def) {
2153
- return def;
3121
+ const workspace = def.workspace ?? createWorkspace();
3122
+ return {
3123
+ ...def,
3124
+ workspace
3125
+ };
2154
3126
  }
2155
3127
  /** Fallback agent id used when `config.agents` is omitted entirely. */
2156
3128
  const FALLBACK_AGENT_ID = "default";
@@ -2257,6 +3229,7 @@ async function buildAgents(opts) {
2257
3229
  const approvalGatedByAgent = [];
2258
3230
  for (const [id, def] of Object.entries(definitions)) {
2259
3231
  const tools = await resolveTools(def.tools, plugins, ambientTools);
3232
+ const workspace = resolveAgentWorkspace(def.workspace);
2260
3233
  const gated = approvalGatedToolIds(tools);
2261
3234
  if (gated.length > 0) approvalGatedByAgent.push({
2262
3235
  agentId: id,
@@ -2272,6 +3245,7 @@ async function buildAgents(opts) {
2272
3245
  defaultOptions: { maxSteps: config.agentMaxSteps ?? DEFAULT_AGENT_MAX_STEPS },
2273
3246
  tools,
2274
3247
  ...memory ? { memory } : {},
3248
+ ...workspace ? { workspace } : {},
2275
3249
  inputProcessors,
2276
3250
  outputProcessors
2277
3251
  });
@@ -2441,6 +3415,10 @@ async function resolveTools(defTools, plugins, ambientTools) {
2441
3415
  ...resolved
2442
3416
  };
2443
3417
  }
3418
+ function resolveAgentWorkspace(workspace) {
3419
+ if (!workspace) return void 0;
3420
+ return typeof workspace === "function" ? workspace() : workspace;
3421
+ }
2444
3422
  /**
2445
3423
  * Build the {@link MastraPlugins} runtime proxy handed to
2446
3424
  * `tools(plugins)` callbacks.
@@ -3319,6 +4297,7 @@ async function buildObservability(options) {
3319
4297
  * that lets the plugin's custom API routes (e.g. `historyRoute`) work
3320
4298
  * behind an Express mount point.
3321
4299
  */
4300
+ logUtils.logger("mastra/server");
3322
4301
  /**
3323
4302
  * OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
3324
4303
  * `trace.getActiveSpan()` returns a non-recording span with this id
@@ -3346,12 +4325,13 @@ var MastraServer$1 = class extends MastraServer {
3346
4325
  }
3347
4326
  registerAuthMiddleware() {
3348
4327
  super.registerAuthMiddleware();
3349
- this.app.use((req, res, next) => {
4328
+ this.app.use(async (req, res, next) => {
3350
4329
  const requestContext = res.locals.requestContext;
3351
- this.configureRequestContextUser(requestContext);
4330
+ await this.configureRequestContextUser(requestContext);
3352
4331
  this.configureRequestContextThreadId(req, res, requestContext);
3353
4332
  this.configureRequestContextModelOverride(req, requestContext);
3354
4333
  this.configureRequestContextRequestId(req, res, requestContext);
4334
+ this.configureRequestContextScopes(req, requestContext);
3355
4335
  this.configureMlflowTraceId(res);
3356
4336
  this.log.debug("auth:middleware", {
3357
4337
  method: req.method,
@@ -3366,7 +4346,7 @@ var MastraServer$1 = class extends MastraServer {
3366
4346
  next();
3367
4347
  });
3368
4348
  }
3369
- configureRequestContextUser(requestContext) {
4349
+ async configureRequestContextUser(requestContext) {
3370
4350
  if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
3371
4351
  const executionContext = getExecutionContext();
3372
4352
  const user = {
@@ -3375,10 +4355,18 @@ var MastraServer$1 = class extends MastraServer {
3375
4355
  };
3376
4356
  requestContext.set(MASTRA_USER_KEY, user);
3377
4357
  requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
4358
+ let userName;
4359
+ let email;
3378
4360
  if ("isUserContext" in executionContext) {
3379
- if (executionContext.userName) requestContext.set(MASTRA_USER_NAME_KEY, executionContext.userName);
3380
- if (executionContext.userEmail) requestContext.set(MASTRA_USER_EMAIL_KEY, executionContext.userEmail);
4361
+ userName = executionContext.userName;
4362
+ email = executionContext.userEmail;
4363
+ } else if (process.env.NODE_ENV === "development") {
4364
+ const currentUser = await executionContext.client.currentUser.me();
4365
+ userName = currentUser?.userName;
4366
+ email = currentUser?.emails?.filter((email) => email.primary).find((email) => email.value)?.value;
3381
4367
  }
4368
+ if (userName) requestContext.set(MASTRA_USER_NAME_KEY, userName);
4369
+ if (email) requestContext.set(MASTRA_USER_EMAIL_KEY, email);
3382
4370
  }
3383
4371
  /**
3384
4372
  * Stamp a per-request id and echo it on the response so an upstream
@@ -3399,6 +4387,16 @@ var MastraServer$1 = class extends MastraServer {
3399
4387
  res.setHeader("X-Request-Id", requestId);
3400
4388
  }
3401
4389
  /**
4390
+ * Stamp OAuth scopes from the forwarded access token on
4391
+ * {@link MASTRA_SCOPES_KEY} for workspace mount gating.
4392
+ */
4393
+ configureRequestContextScopes(req, requestContext) {
4394
+ const scopes = /* @__PURE__ */ new Set();
4395
+ for (const scope of tokenUtils.getAccessTokenScopes(req)) scopes.add(scope);
4396
+ for (const scope of tokenUtils.getAccessTokenScopes(req, "authorization")) scopes.add(scope);
4397
+ requestContext.set(MASTRA_SCOPES_KEY, [...scopes]);
4398
+ }
4399
+ /**
3402
4400
  * Stamp the turn's MLflow trace id on the response so the chat client
3403
4401
  * can attach thumbs / comment feedback to it later. MLflow derives
3404
4402
  * its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
@@ -4283,4 +5281,4 @@ function parseStatementLimit(raw) {
4283
5281
  const mastra = toPlugin(MastraPlugin);
4284
5282
 
4285
5283
  //#endregion
4286
- export { DEFAULT_AGENT_MAX_STEPS, DEFAULT_GENIE_ALIAS, DEFAULT_STYLE_INSTRUCTIONS, FALLBACK_AGENT_ID, GENIE_INSTRUCTIONS, MASTRA_MODEL_OVERRIDE_KEY, MASTRA_REQUEST_ID_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, MASTRA_USER_NAME_KEY, MastraPlugin, TRACE_REQUEST_CONTEXT_KEYS, approvalGatedToolIds, buildAgents, buildGenieToolkitProvider, buildGenieTools, buildMcpServer, buildRenderDataTool, buildSummarizeTool, chartPlannerRequestSchema, collectSpaceSuggestions, createAgent, createTool, extractModelOverride, fetchChart, mastra, normalizeGenieSpaces, prepareChart, resolveGenieSpaces, summarizeText, tool };
5284
+ export { DEFAULT_AGENT_MAX_STEPS, DEFAULT_GENIE_ALIAS, DEFAULT_STYLE_INSTRUCTIONS, DatabricksWorkspaceFilesystem, FALLBACK_AGENT_ID, GENIE_INSTRUCTIONS, MASTRA_MODEL_OVERRIDE_KEY, MASTRA_REQUEST_ID_KEY, MASTRA_SCOPES_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, MASTRA_USER_NAME_KEY, MastraPlugin, TRACE_REQUEST_CONTEXT_KEYS, approvalGatedToolIds, buildAgents, buildGenieToolkitProvider, buildGenieTools, buildMcpServer, buildRenderDataTool, buildSummarizeTool, chartPlannerRequestSchema, collectSpaceSuggestions, createAgent, createTool, createWorkspace, emptyFilesystem, extractModelOverride, fetchChart, isDbfsPath, isWorkspaceFilesPath, mastra, normalizeDatabricksBasePath, normalizeGenieSpaces, prepareChart, resolveDatabricksAbsolutePath, resolveGenieSpaces, summarizeText, toDatabricksWorkspacePath, tool };