@dbx-tools/appkit-mastra 0.1.94 → 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/README.md +54 -2
- package/dist/index.d.ts +272 -14
- package/dist/index.js +1004 -12
- package/package.json +6 -6
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 {
|
|
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
|
-
|
|
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";
|
|
@@ -3325,6 +4297,7 @@ async function buildObservability(options) {
|
|
|
3325
4297
|
* that lets the plugin's custom API routes (e.g. `historyRoute`) work
|
|
3326
4298
|
* behind an Express mount point.
|
|
3327
4299
|
*/
|
|
4300
|
+
logUtils.logger("mastra/server");
|
|
3328
4301
|
/**
|
|
3329
4302
|
* OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
|
|
3330
4303
|
* `trace.getActiveSpan()` returns a non-recording span with this id
|
|
@@ -3352,12 +4325,13 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3352
4325
|
}
|
|
3353
4326
|
registerAuthMiddleware() {
|
|
3354
4327
|
super.registerAuthMiddleware();
|
|
3355
|
-
this.app.use((req, res, next) => {
|
|
4328
|
+
this.app.use(async (req, res, next) => {
|
|
3356
4329
|
const requestContext = res.locals.requestContext;
|
|
3357
|
-
this.configureRequestContextUser(requestContext);
|
|
4330
|
+
await this.configureRequestContextUser(requestContext);
|
|
3358
4331
|
this.configureRequestContextThreadId(req, res, requestContext);
|
|
3359
4332
|
this.configureRequestContextModelOverride(req, requestContext);
|
|
3360
4333
|
this.configureRequestContextRequestId(req, res, requestContext);
|
|
4334
|
+
this.configureRequestContextScopes(req, requestContext);
|
|
3361
4335
|
this.configureMlflowTraceId(res);
|
|
3362
4336
|
this.log.debug("auth:middleware", {
|
|
3363
4337
|
method: req.method,
|
|
@@ -3372,7 +4346,7 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3372
4346
|
next();
|
|
3373
4347
|
});
|
|
3374
4348
|
}
|
|
3375
|
-
configureRequestContextUser(requestContext) {
|
|
4349
|
+
async configureRequestContextUser(requestContext) {
|
|
3376
4350
|
if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
|
|
3377
4351
|
const executionContext = getExecutionContext();
|
|
3378
4352
|
const user = {
|
|
@@ -3381,10 +4355,18 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3381
4355
|
};
|
|
3382
4356
|
requestContext.set(MASTRA_USER_KEY, user);
|
|
3383
4357
|
requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
|
|
4358
|
+
let userName;
|
|
4359
|
+
let email;
|
|
3384
4360
|
if ("isUserContext" in executionContext) {
|
|
3385
|
-
|
|
3386
|
-
|
|
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;
|
|
3387
4367
|
}
|
|
4368
|
+
if (userName) requestContext.set(MASTRA_USER_NAME_KEY, userName);
|
|
4369
|
+
if (email) requestContext.set(MASTRA_USER_EMAIL_KEY, email);
|
|
3388
4370
|
}
|
|
3389
4371
|
/**
|
|
3390
4372
|
* Stamp a per-request id and echo it on the response so an upstream
|
|
@@ -3405,6 +4387,16 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3405
4387
|
res.setHeader("X-Request-Id", requestId);
|
|
3406
4388
|
}
|
|
3407
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
|
+
/**
|
|
3408
4400
|
* Stamp the turn's MLflow trace id on the response so the chat client
|
|
3409
4401
|
* can attach thumbs / comment feedback to it later. MLflow derives
|
|
3410
4402
|
* its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
|
|
@@ -4289,4 +5281,4 @@ function parseStatementLimit(raw) {
|
|
|
4289
5281
|
const mastra = toPlugin(MastraPlugin);
|
|
4290
5282
|
|
|
4291
5283
|
//#endregion
|
|
4292
|
-
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 };
|