@dbx-tools/appkit-mastra 0.9.95 → 0.9.99
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 +134 -103
- package/index.ts +3 -3
- package/lib/index.d.ts +3 -3
- package/lib/index.js +3 -3
- package/lib/src/agents.d.ts +3 -3
- package/lib/src/agents.js +9 -6
- package/lib/src/monty-sandbox.d.ts +4 -7
- package/lib/src/monty-sandbox.js +17 -28
- package/lib/src/openapi-tool.js +3 -12
- package/lib/src/server.js +1 -1
- package/lib/src/skill-paths.d.ts +1 -1
- package/lib/src/skill-paths.js +2 -2
- package/lib/src/workspaces.d.ts +53 -92
- package/lib/src/workspaces.js +303 -144
- package/package.json +14 -14
- package/src/agents.ts +8 -6
- package/src/monty-sandbox.ts +18 -29
- package/src/openapi-tool.ts +2 -11
- package/src/server.ts +22 -20
- package/src/skill-paths.ts +1 -1
- package/src/workspaces.ts +476 -215
package/src/workspaces.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mastra workspace factory for Databricks Apps.
|
|
3
3
|
*
|
|
4
|
-
* Builds
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* readable / writable policy: a Databricks path mounted through the OBO client
|
|
9
|
-
* on {@link MASTRA_USER_KEY}, or any {@link WorkspaceFilesystem} a consuming
|
|
10
|
-
* library already owns. {@link DEFAULT_SKILL_FOLDERS} supplies the Assistant
|
|
11
|
-
* trees, and `skillFolders` merges over it - same name overrides, `false`
|
|
12
|
-
* disables, a new name adds. Optional mount resolvers contribute further
|
|
13
|
-
* filesystems and skill scan roots on top.
|
|
4
|
+
* Builds native Mastra workspace configuration from Databricks paths. Paths
|
|
5
|
+
* mount at the same path by default, `~` resolves to the current user's
|
|
6
|
+
* Databricks workspace home, and `/tmp[/subpath]` resolves to stable
|
|
7
|
+
* user-scoped storage under the host's ephemeral temp directory.
|
|
14
8
|
*
|
|
15
9
|
* Databricks mounts use `@dbx-tools/databricks` {@link DatabricksFileSystem}
|
|
16
10
|
* wrapped by {@link filesystems}; inaccessible roots are skipped independently.
|
|
@@ -19,6 +13,8 @@
|
|
|
19
13
|
*/
|
|
20
14
|
|
|
21
15
|
import { createHash } from "node:crypto";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
22
18
|
import { ConfigurationError, createWorkspaceClient, getExecutionContext } from "@databricks/appkit";
|
|
23
19
|
import type { WorkspaceClient } from "@databricks/appkit";
|
|
24
20
|
import { pluginRegistry } from "@dbx-tools/appkit";
|
|
@@ -29,17 +25,27 @@ import {
|
|
|
29
25
|
} from "@dbx-tools/appkit/files-cache";
|
|
30
26
|
import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
|
|
31
27
|
import { LocalFileSystem } from "@dbx-tools/fs";
|
|
28
|
+
import { match, type PathMatchInput } from "@dbx-tools/path";
|
|
32
29
|
import { errorUtils, log, object, stringUtils, token } from "@dbx-tools/shared-core";
|
|
33
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
fs as sharedFS,
|
|
32
|
+
posixPath,
|
|
33
|
+
type CacheableFileSystemOperation,
|
|
34
|
+
type FileSystem,
|
|
35
|
+
type FileSystemCache,
|
|
36
|
+
type FileSystemCacheOptions,
|
|
37
|
+
MemoryFileSystem,
|
|
38
|
+
} from "@dbx-tools/shared-fs";
|
|
34
39
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
35
40
|
import {
|
|
36
41
|
WORKSPACE_TOOLS,
|
|
37
42
|
Workspace,
|
|
38
43
|
type SkillsResolver,
|
|
44
|
+
type ToolConfigWithArgsContext,
|
|
45
|
+
type WorkspaceConfig,
|
|
39
46
|
type WorkspaceFilesystem,
|
|
40
47
|
type WorkspaceSandbox,
|
|
41
48
|
type WorkspaceSandboxResolver,
|
|
42
|
-
type ToolConfigWithArgsContext,
|
|
43
49
|
type WorkspaceToolConfig,
|
|
44
50
|
type WorkspaceToolsConfig,
|
|
45
51
|
} from "@mastra/core/workspace";
|
|
@@ -54,7 +60,6 @@ import {
|
|
|
54
60
|
import {
|
|
55
61
|
filesystems,
|
|
56
62
|
MountedCompositeFilesystem,
|
|
57
|
-
scratchFilesystem,
|
|
58
63
|
type MastraFileSystemAdapterOptions,
|
|
59
64
|
} from "./filesystems.ts";
|
|
60
65
|
import { MontySandbox } from "./monty-sandbox.ts";
|
|
@@ -66,35 +71,30 @@ import { ORGANIZATION_ASSISTANT_PATH, personalWorkspacePath } from "./skill-path
|
|
|
66
71
|
/** OAuth scopes that gate Databricks workspace file mounts. */
|
|
67
72
|
const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] as const;
|
|
68
73
|
const SCRATCH_MOUNT = "/tmp";
|
|
74
|
+
const CACHEABLE_FILESYSTEM_OPERATIONS = [
|
|
75
|
+
"exists",
|
|
76
|
+
"readFile",
|
|
77
|
+
"readdir",
|
|
78
|
+
"stat",
|
|
79
|
+
] as const satisfies readonly CacheableFileSystemOperation[];
|
|
69
80
|
|
|
70
81
|
const logger = log.logger("mastra/workspaces");
|
|
82
|
+
let workspaceSourceSequence = 0;
|
|
83
|
+
const databricksWorkspaceOptions = new WeakMap<Workspace, DatabricksWorkspaceOptions>();
|
|
71
84
|
|
|
72
85
|
/* -------------------------------- types -------------------------------- */
|
|
73
86
|
|
|
74
87
|
/** Per-request context for mount and skill-folder resolvers. */
|
|
75
|
-
export interface
|
|
88
|
+
export interface DatabricksWorkspaceContext {
|
|
76
89
|
requestContext?: RequestContext;
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/**
|
|
80
|
-
*
|
|
81
|
-
* A resolver returning `undefined` skips the folder for that request.
|
|
82
|
-
*/
|
|
83
|
-
export type SkillFolderValue<T> =
|
|
84
|
-
T | ((context: WorkspaceMountContext) => T | undefined | Promise<T | undefined>);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* One named skill-folder location and its read / write policy.
|
|
88
|
-
*
|
|
89
|
-
* Give {@link path} for a Databricks workspace tree (mounted through the
|
|
90
|
-
* request's OBO client), or {@link filesystem} for a mount the consumer builds
|
|
91
|
-
* itself. {@link filesystem} wins when both are set.
|
|
93
|
+
* One Databricks path mounted into a Mastra workspace.
|
|
92
94
|
*/
|
|
93
|
-
export interface
|
|
94
|
-
/** Absolute Databricks
|
|
95
|
-
path
|
|
96
|
-
/** Ready-made mount, for locations the OBO client cannot reach. */
|
|
97
|
-
filesystem?: SkillFolderValue<WorkspaceFilesystem>;
|
|
95
|
+
export interface DatabricksWorkspacePathOptions {
|
|
96
|
+
/** Absolute Databricks path or `~` shortcut for the current user's workspace home. */
|
|
97
|
+
path: string;
|
|
98
98
|
/** Human-friendly mount name shown in filesystem listings. */
|
|
99
99
|
displayName?: string;
|
|
100
100
|
/** Mount description shown in filesystem listings and workspace instructions. */
|
|
@@ -124,23 +124,62 @@ export interface SkillFolderOptions {
|
|
|
124
124
|
mount?: string;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/**
|
|
128
|
-
export
|
|
127
|
+
/** String shorthand or full options for one Databricks workspace path. */
|
|
128
|
+
export type DatabricksWorkspacePath = string | DatabricksWorkspacePathOptions;
|
|
129
|
+
|
|
130
|
+
/** Fixed or request-dependent Databricks path entry. */
|
|
131
|
+
export type DatabricksWorkspacePathValue =
|
|
132
|
+
| DatabricksWorkspacePath
|
|
133
|
+
| false
|
|
134
|
+
| ((
|
|
135
|
+
context: DatabricksWorkspaceContext,
|
|
136
|
+
) =>
|
|
137
|
+
| DatabricksWorkspacePath
|
|
138
|
+
| false
|
|
139
|
+
| undefined
|
|
140
|
+
| Promise<DatabricksWorkspacePath | false | undefined>);
|
|
141
|
+
|
|
142
|
+
/** Operations and path globs cached by the Databricks filesystem shortcut. */
|
|
143
|
+
export interface DatabricksWorkspaceCacheFilter {
|
|
144
|
+
/** Cached filesystem operations. Omit for `exists`, `readFile`, `readdir`, and `stat`. */
|
|
145
|
+
operations?: CacheableFileSystemOperation | readonly CacheableFileSystemOperation[];
|
|
146
|
+
/** Absolute or root-relative globs and predicates. `~` expands per request. */
|
|
147
|
+
paths?: PathMatchInput | readonly PathMatchInput[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Fixed or request-dependent filesystem cache selection. */
|
|
151
|
+
export type DatabricksWorkspaceCache =
|
|
152
|
+
| boolean
|
|
153
|
+
| DatabricksWorkspaceCacheFilter
|
|
154
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
155
|
+
| ((
|
|
156
|
+
context: DatabricksWorkspaceContext,
|
|
157
|
+
) =>
|
|
158
|
+
| boolean
|
|
159
|
+
| DatabricksWorkspaceCacheFilter
|
|
160
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
161
|
+
| undefined
|
|
162
|
+
| Promise<
|
|
163
|
+
| boolean
|
|
164
|
+
| DatabricksWorkspaceCacheFilter
|
|
165
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
166
|
+
| undefined
|
|
167
|
+
>);
|
|
168
|
+
|
|
169
|
+
/** Mount map plus optional Mastra skill scan roots resolved for one request. */
|
|
170
|
+
interface WorkspaceMountContribution {
|
|
129
171
|
mounts: Record<string, WorkspaceFilesystem>;
|
|
130
172
|
/** Paths within the composite namespace where `SKILL.md` files are scanned. */
|
|
131
173
|
skillPaths?: string[];
|
|
132
174
|
}
|
|
133
175
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
context:
|
|
176
|
+
/** Internal filesystem contribution resolver. */
|
|
177
|
+
type WorkspaceMountResolver = (
|
|
178
|
+
context: DatabricksWorkspaceContext,
|
|
137
179
|
) => WorkspaceMountContribution | Promise<WorkspaceMountContribution>;
|
|
138
180
|
|
|
139
|
-
/** Names carried by {@link DEFAULT_SKILL_FOLDERS}. */
|
|
140
|
-
export type DefaultSkillFolderName = "organization-skills" | "personal-skills";
|
|
141
|
-
|
|
142
181
|
/**
|
|
143
|
-
* Sandbox selection for {@link
|
|
182
|
+
* Sandbox selection for {@link databricksWorkspace}. Monty is the default;
|
|
144
183
|
* `false` disables command execution, and a Mastra provider or resolver is an
|
|
145
184
|
* explicit replacement.
|
|
146
185
|
*/
|
|
@@ -152,35 +191,26 @@ export type WorkspaceSandboxSelection =
|
|
|
152
191
|
| WorkspaceSandbox
|
|
153
192
|
| WorkspaceSandboxResolver;
|
|
154
193
|
|
|
155
|
-
/**
|
|
156
|
-
export interface
|
|
157
|
-
|
|
158
|
-
id?: string;
|
|
159
|
-
/** Display name; derived from `id` when omitted. */
|
|
160
|
-
name?: string;
|
|
194
|
+
/** Native Mastra workspace options plus Databricks path shortcuts. */
|
|
195
|
+
export interface DatabricksWorkspaceOptions
|
|
196
|
+
extends Omit<WorkspaceConfig, "filesystem" | "mounts" | "sandbox" | "skills"> {
|
|
161
197
|
/**
|
|
162
|
-
* Start from {@link
|
|
163
|
-
* starts from an empty map, leaving only the {@link skillFolders} given here.
|
|
198
|
+
* Start from {@link DEFAULT_DATABRICKS_WORKSPACE_PATHS}. Defaults to `true`.
|
|
164
199
|
*/
|
|
165
|
-
|
|
200
|
+
assistantPaths?: boolean;
|
|
166
201
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* a folder.
|
|
202
|
+
* Databricks paths appended after the defaults. A string is shorthand for
|
|
203
|
+
* `{ path }`; a resolver may return either form per request.
|
|
170
204
|
*/
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
mounts?:
|
|
205
|
+
paths?: readonly DatabricksWorkspacePathValue[];
|
|
206
|
+
/** Native Mastra filesystems composed beside the Databricks paths. */
|
|
207
|
+
mounts?: Record<string, WorkspaceFilesystem>;
|
|
174
208
|
/** Replace the auto-built dynamic skills resolver. */
|
|
175
209
|
skills?: SkillsResolver;
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
/** Additional native Mastra workspace tool configuration and overrides. */
|
|
179
|
-
tools?: WorkspaceToolsConfig;
|
|
210
|
+
/** Filesystem cache filters. Ignored when the AppKit files-cache plugin is absent. */
|
|
211
|
+
cache?: DatabricksWorkspaceCache;
|
|
180
212
|
/** AppKit plugin context used to discover optional sibling capabilities. */
|
|
181
213
|
pluginContext?: pluginRegistry.PluginContextLike;
|
|
182
|
-
/** Enable BM25 keyword search over indexed workspace content. */
|
|
183
|
-
bm25?: boolean;
|
|
184
214
|
/**
|
|
185
215
|
* Command sandbox. Defaults to Monty. Pass `"databricks"` or an options
|
|
186
216
|
* object for Databricks Sandbox, `false` to disable command execution, or an
|
|
@@ -200,97 +230,78 @@ export interface CreateWorkspaceOptions {
|
|
|
200
230
|
/* ------------------------------- defaults ------------------------------- */
|
|
201
231
|
|
|
202
232
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* - `organization-skills` - `/Workspace/.assistant` with `skills`; write
|
|
206
|
-
* attempts require approval and remain subject to Databricks permissions.
|
|
207
|
-
* - `personal-skills` - the requesting user's `/Workspace/Users/<email>` root
|
|
208
|
-
* with `.assistant/skills`, writable and skipped without an email.
|
|
233
|
+
* Default Databricks paths for shared and current-user Agent Skills.
|
|
209
234
|
*/
|
|
210
|
-
export const
|
|
211
|
-
|
|
212
|
-
description: "Organization skills shared with everyone in this Databricks workspace.",
|
|
213
|
-
displayName: "Organization Skills",
|
|
235
|
+
export const DEFAULT_DATABRICKS_WORKSPACE_PATHS: readonly DatabricksWorkspacePathOptions[] = [
|
|
236
|
+
{
|
|
214
237
|
path: ORGANIZATION_ASSISTANT_PATH,
|
|
215
238
|
readable: true,
|
|
216
239
|
skills: ["skills"],
|
|
217
240
|
writable: true,
|
|
218
241
|
createRoot: false,
|
|
219
242
|
},
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
displayName: "Home",
|
|
223
|
-
path: ({ requestContext }) => {
|
|
224
|
-
const email = resolveScopedEmail(requestContext);
|
|
225
|
-
return email ? personalWorkspacePath(email) : undefined;
|
|
226
|
-
},
|
|
243
|
+
{
|
|
244
|
+
path: "~",
|
|
227
245
|
readable: true,
|
|
228
246
|
skills: [".assistant/skills"],
|
|
229
247
|
writable: true,
|
|
230
248
|
createRoot: false,
|
|
231
249
|
},
|
|
232
|
-
|
|
250
|
+
];
|
|
233
251
|
|
|
234
252
|
/**
|
|
235
253
|
* Create a Mastra {@link Workspace} with per-request Databricks mounts.
|
|
236
254
|
*
|
|
237
|
-
* @example Default
|
|
255
|
+
* @example Default Databricks paths
|
|
238
256
|
* ```ts
|
|
239
|
-
*
|
|
257
|
+
* databricksWorkspace()
|
|
240
258
|
* ```
|
|
241
259
|
*
|
|
242
|
-
* @example
|
|
260
|
+
* @example Use only explicit paths
|
|
243
261
|
* ```ts
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* path: "/Workspace/Shared/.assistant",
|
|
248
|
-
* skills: ["skills"],
|
|
249
|
-
* },
|
|
250
|
-
* "personal-skills": false,
|
|
251
|
-
* runbooks: { path: "/Workspace/Shared/runbooks/skills", writable: true },
|
|
252
|
-
* volume: { filesystem: myVolumeFilesystem },
|
|
253
|
-
* },
|
|
262
|
+
* databricksWorkspace({
|
|
263
|
+
* assistantPaths: false,
|
|
264
|
+
* paths: ["~/project", "/tmp/project", "/Volumes/main/default/data"],
|
|
254
265
|
* })
|
|
255
266
|
* ```
|
|
256
267
|
*
|
|
257
|
-
* @example
|
|
268
|
+
* @example Build a standard Mastra workspace yourself
|
|
258
269
|
* ```ts
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* mounts: { "/data": myFilesystem },
|
|
263
|
-
* skillPaths: [],
|
|
264
|
-
* }),
|
|
265
|
-
* ],
|
|
270
|
+
* new Workspace({
|
|
271
|
+
* ...databricksWorkspaceConfig({ paths: ["~/project"] }),
|
|
272
|
+
* sandbox: mySandbox,
|
|
266
273
|
* })
|
|
267
274
|
* ```
|
|
268
275
|
*/
|
|
269
|
-
export function
|
|
276
|
+
export function databricksWorkspaceConfig(
|
|
277
|
+
options: DatabricksWorkspaceOptions = {},
|
|
278
|
+
): WorkspaceConfig {
|
|
270
279
|
const { id, name } = resolveWorkspaceIdentity(options);
|
|
271
|
-
const
|
|
272
|
-
const
|
|
280
|
+
const filesystemSourceKey = `${id}:${++workspaceSourceSequence}`;
|
|
281
|
+
const paths = resolveDatabricksWorkspacePaths(options);
|
|
282
|
+
const configuredPaths = paths.length;
|
|
273
283
|
const extraSkillPaths = options.extraSkillPaths ?? [];
|
|
274
284
|
const filesCache = pluginRegistry.instance(options.pluginContext, filesCachePlugin)?.exports();
|
|
285
|
+
const configuredCache = options.cache;
|
|
286
|
+
const mountFilesCache = configuredCache === false ? undefined : filesCache;
|
|
275
287
|
const retainFilesystemSource =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
(folder) =>
|
|
280
|
-
typeof folder.filesystem !== "function" &&
|
|
281
|
-
!(typeof folder.path === "function" && folder.mount !== undefined),
|
|
282
|
-
);
|
|
288
|
+
mountFilesCache !== undefined &&
|
|
289
|
+
typeof configuredCache !== "function" &&
|
|
290
|
+
paths.every((path) => typeof path !== "function");
|
|
283
291
|
const resolvers = buildMountResolvers(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
292
|
+
paths,
|
|
293
|
+
options.mounts,
|
|
294
|
+
localSkillMountResolvers(extraSkillPaths),
|
|
295
|
+
mountFilesCache,
|
|
296
|
+
configuredCache,
|
|
287
297
|
);
|
|
288
298
|
const resolveContribution = contributionResolver(resolvers);
|
|
289
|
-
const resolveFilesystem = (context:
|
|
299
|
+
const resolveFilesystem = (context: DatabricksWorkspaceContext) =>
|
|
290
300
|
resolveWorkspaceFilesystem(
|
|
291
301
|
resolveContribution,
|
|
292
302
|
context,
|
|
293
|
-
retainFilesystemSource ?
|
|
303
|
+
retainFilesystemSource ? mountFilesCache : undefined,
|
|
304
|
+
filesystemSourceKey,
|
|
294
305
|
);
|
|
295
306
|
const skills: SkillsResolver =
|
|
296
307
|
options.skills ??
|
|
@@ -303,15 +314,15 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
303
314
|
);
|
|
304
315
|
});
|
|
305
316
|
const checkSkillFileMtime = options.checkSkillFileMtime ?? false;
|
|
306
|
-
const bm25 = options.bm25
|
|
317
|
+
const bm25 = options.bm25 ?? true;
|
|
307
318
|
const sandbox = resolveWorkspaceSandbox(options.sandbox, id, name);
|
|
308
319
|
const tools = workspaceTools(options.tools);
|
|
309
320
|
logger.debug("workspace:create", {
|
|
310
321
|
id,
|
|
311
322
|
name,
|
|
312
323
|
resolverCount: resolvers.length,
|
|
313
|
-
|
|
314
|
-
|
|
324
|
+
configuredPaths,
|
|
325
|
+
customMounts: Object.keys(options.mounts ?? {}).length,
|
|
315
326
|
customSkillsResolver: Boolean(options.skills),
|
|
316
327
|
checkSkillFileMtime,
|
|
317
328
|
bm25,
|
|
@@ -319,7 +330,19 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
319
330
|
sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
|
|
320
331
|
});
|
|
321
332
|
|
|
322
|
-
const
|
|
333
|
+
const {
|
|
334
|
+
assistantPaths: _assistantPaths,
|
|
335
|
+
paths: _paths,
|
|
336
|
+
mounts: _mounts,
|
|
337
|
+
skills: _skills,
|
|
338
|
+
cache: _cache,
|
|
339
|
+
pluginContext: _pluginContext,
|
|
340
|
+
sandbox: _sandbox,
|
|
341
|
+
extraSkillPaths: _extraSkillPaths,
|
|
342
|
+
...workspaceOptions
|
|
343
|
+
} = options;
|
|
344
|
+
return {
|
|
345
|
+
...workspaceOptions,
|
|
323
346
|
id,
|
|
324
347
|
name,
|
|
325
348
|
filesystem: resolveFilesystem,
|
|
@@ -337,29 +360,38 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
337
360
|
: {}),
|
|
338
361
|
bm25,
|
|
339
362
|
tools,
|
|
340
|
-
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Create a native Mastra {@link Workspace} from {@link databricksWorkspaceConfig}. */
|
|
367
|
+
export function databricksWorkspace(options: DatabricksWorkspaceOptions = {}): Workspace {
|
|
368
|
+
const workspace = new Workspace(databricksWorkspaceConfig(options));
|
|
369
|
+
databricksWorkspaceOptions.set(workspace, options);
|
|
341
370
|
return workspace;
|
|
342
371
|
}
|
|
343
372
|
|
|
373
|
+
/** @internal Bind a caller-created Databricks workspace to its AppKit plugin context. */
|
|
374
|
+
export function bindDatabricksWorkspaceContext(
|
|
375
|
+
workspace: Workspace,
|
|
376
|
+
pluginContext: pluginRegistry.PluginContextLike | undefined,
|
|
377
|
+
): Workspace {
|
|
378
|
+
const options = databricksWorkspaceOptions.get(workspace);
|
|
379
|
+
if (!options || options.pluginContext !== undefined || pluginContext === undefined) {
|
|
380
|
+
return workspace;
|
|
381
|
+
}
|
|
382
|
+
return databricksWorkspace({ ...options, pluginContext });
|
|
383
|
+
}
|
|
384
|
+
|
|
344
385
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* `assistantSkills: false` drops the defaults, and a `false` value removes one
|
|
348
|
-
* entry by name.
|
|
386
|
+
* Resolve default and caller-provided Databricks paths in priority order.
|
|
349
387
|
*/
|
|
350
|
-
export function
|
|
351
|
-
options: Pick<
|
|
352
|
-
):
|
|
353
|
-
|
|
354
|
-
options.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
delete merged[name];
|
|
358
|
-
} else {
|
|
359
|
-
merged[name] = folder;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return merged;
|
|
388
|
+
export function resolveDatabricksWorkspacePaths(
|
|
389
|
+
options: Pick<DatabricksWorkspaceOptions, "assistantPaths" | "paths"> = {},
|
|
390
|
+
): DatabricksWorkspacePathValue[] {
|
|
391
|
+
return [
|
|
392
|
+
...(options.assistantPaths === false ? [] : DEFAULT_DATABRICKS_WORKSPACE_PATHS),
|
|
393
|
+
...(options.paths ?? []),
|
|
394
|
+
];
|
|
363
395
|
}
|
|
364
396
|
|
|
365
397
|
/* ---------------------------- private helpers ---------------------------- */
|
|
@@ -484,7 +516,7 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
484
516
|
}
|
|
485
517
|
|
|
486
518
|
/** Resolve the request client and authorization-safe files-cache scope. */
|
|
487
|
-
async function resolveFilesCacheScope(context:
|
|
519
|
+
async function resolveFilesCacheScope(context: DatabricksWorkspaceContext): Promise<
|
|
488
520
|
| {
|
|
489
521
|
client: WorkspaceClient;
|
|
490
522
|
scope: FilesCacheScope;
|
|
@@ -532,38 +564,54 @@ function isWorkspaceRoot(root: string): boolean {
|
|
|
532
564
|
* folder whose location resolves for this request.
|
|
533
565
|
*/
|
|
534
566
|
async function resolveSkillFolderMounts(
|
|
535
|
-
|
|
536
|
-
context:
|
|
567
|
+
paths: readonly DatabricksWorkspacePathValue[],
|
|
568
|
+
context: DatabricksWorkspaceContext,
|
|
537
569
|
filesCache: FilesCacheExports | undefined,
|
|
570
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
538
571
|
): Promise<WorkspaceMountContribution> {
|
|
539
572
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
540
573
|
const skillPaths: string[] = [];
|
|
541
574
|
const requestContext = context.requestContext;
|
|
542
575
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
reason: !requestContext ? "no-request-context" : "missing-workspace-scope",
|
|
546
|
-
nodeEnv: process.env.NODE_ENV,
|
|
547
|
-
scopes: requestContext?.get(MASTRA_SCOPES_KEY),
|
|
548
|
-
});
|
|
549
|
-
return { mounts, skillPaths };
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const scoped = await resolveFilesCacheScope(context);
|
|
576
|
+
const canMountDatabricks = Boolean(requestContext && shouldMountSkillFolders(requestContext));
|
|
577
|
+
const scoped = canMountDatabricks ? await resolveFilesCacheScope(context) : undefined;
|
|
553
578
|
const client = scoped?.client;
|
|
554
579
|
const fileSystemCache =
|
|
555
580
|
filesCache && scoped ? await filesCache.forScope(scoped.scope) : undefined;
|
|
556
581
|
|
|
557
|
-
for (const [
|
|
558
|
-
const
|
|
582
|
+
for (const [index, path] of paths.entries()) {
|
|
583
|
+
const configured = await resolveDatabricksWorkspacePath(path, context);
|
|
584
|
+
if (!configured) continue;
|
|
585
|
+
if (posixPath.isWithinRoot(SCRATCH_MOUNT, configured.path)) {
|
|
586
|
+
const mount = configured.mount ?? configured.path;
|
|
587
|
+
mounts[mount] = userTempFilesystem(context, configured.path);
|
|
588
|
+
if (configured.readable !== false) {
|
|
589
|
+
skillPaths.push(
|
|
590
|
+
...(configured.skills ?? []).map((path) => mountedSkillPath(mount, path)),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (!canMountDatabricks) {
|
|
596
|
+
logger.debug("workspace-path:skipped", {
|
|
597
|
+
path: configured.path,
|
|
598
|
+
reason: !requestContext ? "no-request-context" : "missing-workspace-scope",
|
|
599
|
+
});
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const resolved = await resolveSkillFolderFilesystem(configured, context, cacheConfig, {
|
|
559
603
|
client,
|
|
560
604
|
fileSystemCache,
|
|
561
605
|
});
|
|
562
606
|
if (!resolved) continue;
|
|
563
|
-
const mount =
|
|
607
|
+
const mount = configured.mount ?? resolved.root;
|
|
608
|
+
if (!mount) {
|
|
609
|
+
logger.debug("workspace-path:skipped", { index, reason: "missing-mount" });
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
564
612
|
mounts[mount] = resolved.filesystem;
|
|
565
|
-
if (
|
|
566
|
-
skillPaths.push(...(
|
|
613
|
+
if (configured.readable !== false) {
|
|
614
|
+
skillPaths.push(...(configured.skills ?? ["."]).map((path) => mountedSkillPath(mount, path)));
|
|
567
615
|
}
|
|
568
616
|
}
|
|
569
617
|
|
|
@@ -578,14 +626,43 @@ async function resolveSkillFolderMounts(
|
|
|
578
626
|
};
|
|
579
627
|
}
|
|
580
628
|
|
|
629
|
+
interface ResolvedDatabricksWorkspacePath {
|
|
630
|
+
path: string;
|
|
631
|
+
displayName?: string;
|
|
632
|
+
description?: string;
|
|
633
|
+
readable?: boolean;
|
|
634
|
+
skills?: readonly string[];
|
|
635
|
+
writable?: boolean;
|
|
636
|
+
createRoot?: boolean;
|
|
637
|
+
mount?: string;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function resolveDatabricksWorkspacePath(
|
|
641
|
+
value: DatabricksWorkspacePathValue,
|
|
642
|
+
context: DatabricksWorkspaceContext,
|
|
643
|
+
): Promise<ResolvedDatabricksWorkspacePath | undefined> {
|
|
644
|
+
const selected = typeof value === "function" ? await value(context) : value;
|
|
645
|
+
if (selected === false || selected === undefined) return undefined;
|
|
646
|
+
const configured = typeof selected === "string" ? { path: selected } : selected;
|
|
647
|
+
const requestContext = requestContextValues(context.requestContext);
|
|
648
|
+
const resolvedPath = resolveHomePath(configured.path, requestContext);
|
|
649
|
+
if (!resolvedPath) return undefined;
|
|
650
|
+
const resolvedMount = resolveHomePath(configured.mount, requestContext);
|
|
651
|
+
return {
|
|
652
|
+
...configured,
|
|
653
|
+
path: resolvedPath,
|
|
654
|
+
...(resolvedMount ? { mount: resolvedMount } : {}),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
581
658
|
/**
|
|
582
659
|
* Resolve one skill folder to a Mastra filesystem, or `undefined` to skip it
|
|
583
660
|
* for this request.
|
|
584
661
|
*/
|
|
585
662
|
async function resolveSkillFolderFilesystem(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
663
|
+
folder: ResolvedDatabricksWorkspacePath,
|
|
664
|
+
context: DatabricksWorkspaceContext,
|
|
665
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
589
666
|
cache: {
|
|
590
667
|
client: WorkspaceClient | undefined;
|
|
591
668
|
fileSystemCache: FileSystemCache | undefined;
|
|
@@ -597,32 +674,32 @@ async function resolveSkillFolderFilesystem(
|
|
|
597
674
|
}
|
|
598
675
|
| undefined
|
|
599
676
|
> {
|
|
600
|
-
if (folder.filesystem !== undefined) {
|
|
601
|
-
const filesystem = await resolveSkillFolderValue(folder.filesystem, context);
|
|
602
|
-
return filesystem
|
|
603
|
-
? {
|
|
604
|
-
filesystem,
|
|
605
|
-
}
|
|
606
|
-
: undefined;
|
|
607
|
-
}
|
|
608
677
|
// A path mount needs the request's OBO client to reach the workspace.
|
|
609
|
-
if (
|
|
678
|
+
if (!cache.client) {
|
|
610
679
|
logger.debug("skill-folder:skipped", {
|
|
611
|
-
|
|
612
|
-
reason:
|
|
680
|
+
path: folder.path,
|
|
681
|
+
reason: "missing-obo-client",
|
|
613
682
|
});
|
|
614
683
|
return undefined;
|
|
615
684
|
}
|
|
616
|
-
const root = stringUtils.trimToNull(
|
|
685
|
+
const root = stringUtils.trimToNull(folder.path);
|
|
617
686
|
if (!root) return undefined;
|
|
618
687
|
const writable = folder.writable ?? isWorkspaceRoot(root);
|
|
688
|
+
const cacheOptions = cache.fileSystemCache
|
|
689
|
+
? await resolveFileCacheOptions(
|
|
690
|
+
cacheConfig,
|
|
691
|
+
context,
|
|
692
|
+
root,
|
|
693
|
+
folder.readable === false ? [] : (folder.skills ?? ["."]),
|
|
694
|
+
)
|
|
695
|
+
: undefined;
|
|
619
696
|
const resolved = await databricksFilesystem(
|
|
620
697
|
cache.client,
|
|
621
698
|
root,
|
|
622
699
|
!writable,
|
|
623
700
|
folder.createRoot ?? folder.writable === true,
|
|
624
701
|
cache.fileSystemCache,
|
|
625
|
-
|
|
702
|
+
cacheOptions,
|
|
626
703
|
{
|
|
627
704
|
...(folder.description ? { description: folder.description } : {}),
|
|
628
705
|
...(folder.displayName ? { displayName: folder.displayName } : {}),
|
|
@@ -631,21 +708,116 @@ async function resolveSkillFolderFilesystem(
|
|
|
631
708
|
return resolved ? { ...resolved, root } : undefined;
|
|
632
709
|
}
|
|
633
710
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
711
|
+
async function resolveCacheConfig(
|
|
712
|
+
value: DatabricksWorkspaceCache | undefined,
|
|
713
|
+
context: DatabricksWorkspaceContext,
|
|
714
|
+
): Promise<
|
|
715
|
+
| boolean
|
|
716
|
+
| DatabricksWorkspaceCacheFilter
|
|
717
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
718
|
+
| undefined
|
|
719
|
+
> {
|
|
639
720
|
return typeof value === "function"
|
|
640
|
-
?
|
|
721
|
+
? value(context)
|
|
641
722
|
: value;
|
|
642
723
|
}
|
|
643
724
|
|
|
725
|
+
function requestContextValues(requestContext: RequestContext | undefined): Record<string, unknown> {
|
|
726
|
+
return requestContext?.toJSON() ?? {};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function resolveHomePath(
|
|
730
|
+
input: string | undefined,
|
|
731
|
+
requestContext: Record<string, unknown>,
|
|
732
|
+
): string | undefined {
|
|
733
|
+
const value = stringUtils.trimToNull(input);
|
|
734
|
+
if (!value || (value !== "~" && !value.startsWith("~/"))) return value ?? undefined;
|
|
735
|
+
const emailValue = requestContext[MASTRA_USER_EMAIL_KEY];
|
|
736
|
+
const email = typeof emailValue === "string" ? stringUtils.trimToNull(emailValue) : undefined;
|
|
737
|
+
if (!email) return undefined;
|
|
738
|
+
const home = personalWorkspacePath(email);
|
|
739
|
+
return value === "~" ? home : `${home}/${value.slice(2)}`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function toPolicyArray<T>(value: T | readonly T[] | undefined): readonly T[] {
|
|
743
|
+
if (value === undefined) return [];
|
|
744
|
+
return Array.isArray(value) ? (value as readonly T[]) : [value as T];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function cacheFilterMatches(
|
|
748
|
+
policy: DatabricksWorkspaceCacheFilter,
|
|
749
|
+
operation: CacheableFileSystemOperation,
|
|
750
|
+
path: string,
|
|
751
|
+
requestContext: Record<string, unknown>,
|
|
752
|
+
root?: string,
|
|
753
|
+
): boolean {
|
|
754
|
+
const operations = toPolicyArray(policy.operations ?? CACHEABLE_FILESYSTEM_OPERATIONS);
|
|
755
|
+
if (!operations.includes(operation)) return false;
|
|
756
|
+
if (policy.paths === undefined) return true;
|
|
757
|
+
const inputs: PathMatchInput[] = [];
|
|
758
|
+
for (const input of toPolicyArray<PathMatchInput>(policy.paths)) {
|
|
759
|
+
if (typeof input !== "string") {
|
|
760
|
+
inputs.push(input);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const expanded = resolveHomePath(input, requestContext);
|
|
764
|
+
if (!expanded) continue;
|
|
765
|
+
inputs.push(
|
|
766
|
+
!root || posixPath.isAbsolute(expanded) || expanded.startsWith("**")
|
|
767
|
+
? expanded
|
|
768
|
+
: `${posixPath.normalizeRoot(root)}/${expanded.replace(/^\.\//, "")}`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
return inputs.length > 0 && match.toPathMatcher(...inputs)(path);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function resolveFileCacheOptions(
|
|
775
|
+
configured: DatabricksWorkspaceCache | undefined,
|
|
776
|
+
context: DatabricksWorkspaceContext,
|
|
777
|
+
root: string,
|
|
778
|
+
skillPaths: readonly string[],
|
|
779
|
+
): Promise<FileSystemCacheOptions | undefined> {
|
|
780
|
+
const requestContext = requestContextValues(context.requestContext);
|
|
781
|
+
const resolved = await resolveCacheConfig(configured, context);
|
|
782
|
+
if (resolved === false) return undefined;
|
|
783
|
+
if (resolved === undefined || resolved === true) {
|
|
784
|
+
const roots = skillPaths.map((path) => mountedSkillPath(root, path));
|
|
785
|
+
return {
|
|
786
|
+
operations: CACHEABLE_FILESYSTEM_OPERATIONS,
|
|
787
|
+
filter: (operation, path) =>
|
|
788
|
+
operation !== "readFile" ||
|
|
789
|
+
roots.some((skillRoot) => posixPath.isWithinRoot(skillRoot, path)),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
const policies = toPolicyArray(resolved);
|
|
793
|
+
if (policies.length === 0) return undefined;
|
|
794
|
+
const operations = [
|
|
795
|
+
...new Set(
|
|
796
|
+
policies.flatMap((policy) =>
|
|
797
|
+
toPolicyArray(policy.operations ?? CACHEABLE_FILESYSTEM_OPERATIONS),
|
|
798
|
+
),
|
|
799
|
+
),
|
|
800
|
+
];
|
|
801
|
+
return {
|
|
802
|
+
operations,
|
|
803
|
+
filter: (operation, path) =>
|
|
804
|
+
policies.some((policy) =>
|
|
805
|
+
cacheFilterMatches(
|
|
806
|
+
policy,
|
|
807
|
+
operation,
|
|
808
|
+
path,
|
|
809
|
+
requestContext,
|
|
810
|
+
root,
|
|
811
|
+
),
|
|
812
|
+
),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
644
816
|
/**
|
|
645
|
-
* Fill in `id` and `name` when either is omitted on {@link
|
|
817
|
+
* Fill in `id` and `name` when either is omitted on {@link DatabricksWorkspaceOptions}.
|
|
646
818
|
* Slugifies `name` into `id`; tokenizes `id` into a display `name`.
|
|
647
819
|
*/
|
|
648
|
-
function resolveWorkspaceIdentity(options:
|
|
820
|
+
function resolveWorkspaceIdentity(options: DatabricksWorkspaceOptions): {
|
|
649
821
|
id: string;
|
|
650
822
|
name: string;
|
|
651
823
|
} {
|
|
@@ -662,30 +834,51 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
|
|
|
662
834
|
|
|
663
835
|
/** Collect the skill-folder resolver and any caller-supplied ones. */
|
|
664
836
|
function buildMountResolvers(
|
|
665
|
-
|
|
666
|
-
mounts:
|
|
837
|
+
paths: readonly DatabricksWorkspacePathValue[],
|
|
838
|
+
mounts: Record<string, WorkspaceFilesystem> | undefined,
|
|
839
|
+
localMounts: WorkspaceMountResolver[],
|
|
667
840
|
filesCache: FilesCacheExports | undefined,
|
|
841
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
668
842
|
): WorkspaceMountResolver[] {
|
|
669
|
-
const resolvers: WorkspaceMountResolver[] = [
|
|
670
|
-
const
|
|
671
|
-
if (
|
|
672
|
-
resolvers.push((context) =>
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
resolvers.push(...mounts);
|
|
843
|
+
const resolvers: WorkspaceMountResolver[] = [];
|
|
844
|
+
const pathCount = paths.length;
|
|
845
|
+
if (pathCount > 0) {
|
|
846
|
+
resolvers.push((context) =>
|
|
847
|
+
resolveSkillFolderMounts(paths, context, filesCache, cacheConfig),
|
|
848
|
+
);
|
|
676
849
|
}
|
|
850
|
+
if (mounts && Object.keys(mounts).length > 0) resolvers.push(() => ({ mounts }));
|
|
851
|
+
resolvers.push(...localMounts);
|
|
677
852
|
logger.debug("mounts:resolvers", {
|
|
678
|
-
|
|
679
|
-
|
|
853
|
+
pathCount,
|
|
854
|
+
customMountCount: Object.keys(mounts ?? {}).length,
|
|
855
|
+
localMountCount: localMounts.length,
|
|
680
856
|
totalResolverCount: resolvers.length,
|
|
681
857
|
});
|
|
682
858
|
return resolvers;
|
|
683
859
|
}
|
|
684
860
|
|
|
685
|
-
/**
|
|
686
|
-
function
|
|
687
|
-
|
|
688
|
-
|
|
861
|
+
/** Stable user-scoped local storage under the host operating system's temp directory. */
|
|
862
|
+
function userTempFilesystem(
|
|
863
|
+
context: DatabricksWorkspaceContext,
|
|
864
|
+
workspacePath: string,
|
|
865
|
+
): WorkspaceFilesystem {
|
|
866
|
+
const userKey = resolveUserKey(context.requestContext);
|
|
867
|
+
const digest = createHash("sha256")
|
|
868
|
+
.update(object.toStableKey({ package: "@dbx-tools/appkit-mastra", userKey }))
|
|
869
|
+
.digest("hex")
|
|
870
|
+
.slice(0, 16);
|
|
871
|
+
const normalized = posixPath.normalizeRoot(workspacePath);
|
|
872
|
+
const relative = normalized === SCRATCH_MOUNT ? "" : normalized.slice(`${SCRATCH_MOUNT}/`.length);
|
|
873
|
+
const source = new LocalFileSystem({
|
|
874
|
+
root: join(tmpdir(), "dbx-tools", "mastra", digest, relative),
|
|
875
|
+
readOnly: false,
|
|
876
|
+
createRoot: true,
|
|
877
|
+
});
|
|
878
|
+
return filesystems(source, {
|
|
879
|
+
displayName: "Temporary Files",
|
|
880
|
+
description:
|
|
881
|
+
"User-scoped temporary files retained while the host keeps its ephemeral temp storage.",
|
|
689
882
|
});
|
|
690
883
|
}
|
|
691
884
|
|
|
@@ -717,15 +910,10 @@ function shouldMountSkillFolders(requestContext: RequestContext): boolean {
|
|
|
717
910
|
return hasWorkspaceFileScope(requestContext);
|
|
718
911
|
}
|
|
719
912
|
|
|
720
|
-
/** Read the trimmed OBO user email stamped on {@link MASTRA_USER_EMAIL_KEY}. */
|
|
721
|
-
function resolveScopedEmail(requestContext: RequestContext | undefined): string | undefined {
|
|
722
|
-
return stringUtils.trimToNull(requestContext?.get(MASTRA_USER_EMAIL_KEY)) ?? undefined;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
913
|
/**
|
|
726
914
|
* Wrap a {@link DatabricksFileSystem} as a Mastra filesystem. Missing or
|
|
727
|
-
* inaccessible roots return `undefined` so
|
|
728
|
-
*
|
|
915
|
+
* inaccessible roots return `undefined` so other accessible roots remain
|
|
916
|
+
* usable.
|
|
729
917
|
*/
|
|
730
918
|
async function databricksFilesystem(
|
|
731
919
|
client: WorkspaceClient,
|
|
@@ -733,7 +921,7 @@ async function databricksFilesystem(
|
|
|
733
921
|
readOnly: boolean = true,
|
|
734
922
|
createRoot: boolean = !readOnly,
|
|
735
923
|
fileSystemCache?: FileSystemCache,
|
|
736
|
-
|
|
924
|
+
cacheOptions?: FileSystemCacheOptions,
|
|
737
925
|
adapterOptions: Pick<MastraFileSystemAdapterOptions, "description" | "displayName"> = {},
|
|
738
926
|
): Promise<
|
|
739
927
|
| {
|
|
@@ -754,9 +942,9 @@ async function databricksFilesystem(
|
|
|
754
942
|
readOnly,
|
|
755
943
|
createRoot,
|
|
756
944
|
});
|
|
757
|
-
const
|
|
758
|
-
? sharedFS.cache(fs, fileSystemCache,
|
|
759
|
-
|
|
945
|
+
const cached =
|
|
946
|
+
fileSystemCache && cacheOptions ? sharedFS.cache(fs, fileSystemCache, cacheOptions) : fs;
|
|
947
|
+
const source = verifyFirstFileSystemAccess(cached, () => fs.stat("."), root);
|
|
760
948
|
const filesystem = filesystems(source, { ...adapterOptions, readOnly });
|
|
761
949
|
try {
|
|
762
950
|
await filesystem.init();
|
|
@@ -773,13 +961,83 @@ async function databricksFilesystem(
|
|
|
773
961
|
return undefined;
|
|
774
962
|
}
|
|
775
963
|
|
|
964
|
+
function verifyFirstFileSystemAccess<TFileSystem extends FileSystem>(
|
|
965
|
+
filesystem: TFileSystem,
|
|
966
|
+
readProbe: () => Promise<unknown>,
|
|
967
|
+
root: string,
|
|
968
|
+
): TFileSystem {
|
|
969
|
+
let readCheck: Promise<void> | undefined;
|
|
970
|
+
let writeVerified = false;
|
|
971
|
+
const boundMethods = new Map<PropertyKey, unknown>();
|
|
972
|
+
const readOperations = new Set<PropertyKey>(["exists", "readFile", "readdir", "stat"]);
|
|
973
|
+
const writeOperations = new Set<PropertyKey>([
|
|
974
|
+
"appendFile",
|
|
975
|
+
"copyFile",
|
|
976
|
+
"deleteFile",
|
|
977
|
+
"mkdir",
|
|
978
|
+
"moveFile",
|
|
979
|
+
"rmdir",
|
|
980
|
+
"writeFile",
|
|
981
|
+
]);
|
|
982
|
+
const verifyRead = async (): Promise<void> => {
|
|
983
|
+
readCheck ??= Promise.resolve(readProbe()).then(() => undefined);
|
|
984
|
+
try {
|
|
985
|
+
await readCheck;
|
|
986
|
+
} catch (error) {
|
|
987
|
+
readCheck = undefined;
|
|
988
|
+
logger.debug("databricks-mount:read-check-failed", {
|
|
989
|
+
root,
|
|
990
|
+
error: errorUtils.errorMessage(error),
|
|
991
|
+
});
|
|
992
|
+
throw error;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
return new Proxy(filesystem, {
|
|
997
|
+
get(target, property) {
|
|
998
|
+
const value = Reflect.get(target, property, target);
|
|
999
|
+
if (typeof value !== "function") return value;
|
|
1000
|
+
if (boundMethods.has(property)) return boundMethods.get(property);
|
|
1001
|
+
const delegated = value.bind(target) as (...args: unknown[]) => unknown;
|
|
1002
|
+
let method = delegated;
|
|
1003
|
+
if (readOperations.has(property)) {
|
|
1004
|
+
method = async (...args: unknown[]) => {
|
|
1005
|
+
await verifyRead();
|
|
1006
|
+
return delegated(...args);
|
|
1007
|
+
};
|
|
1008
|
+
} else if (writeOperations.has(property)) {
|
|
1009
|
+
method = async (...args: unknown[]) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const result = await delegated(...args);
|
|
1012
|
+
if (!writeVerified) {
|
|
1013
|
+
writeVerified = true;
|
|
1014
|
+
logger.debug("databricks-mount:write-check-passed", { root });
|
|
1015
|
+
}
|
|
1016
|
+
return result;
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
if (!writeVerified) {
|
|
1019
|
+
logger.debug("databricks-mount:write-check-failed", {
|
|
1020
|
+
root,
|
|
1021
|
+
error: errorUtils.errorMessage(error),
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
throw error;
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
boundMethods.set(property, method);
|
|
1029
|
+
return method;
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
776
1034
|
/**
|
|
777
1035
|
* Run every mount resolver for one request and merge mounts plus skill paths.
|
|
778
1036
|
* Later resolvers overwrite mount keys from earlier ones.
|
|
779
1037
|
*/
|
|
780
1038
|
async function resolveWorkspaceContribution(
|
|
781
1039
|
resolvers: WorkspaceMountResolver[],
|
|
782
|
-
context:
|
|
1040
|
+
context: DatabricksWorkspaceContext,
|
|
783
1041
|
): Promise<WorkspaceMountContribution> {
|
|
784
1042
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
785
1043
|
const skillPaths: string[] = [];
|
|
@@ -811,34 +1069,37 @@ async function resolveWorkspaceContribution(
|
|
|
811
1069
|
/**
|
|
812
1070
|
* Dynamic filesystem resolver passed to Mastra {@link Workspace}.
|
|
813
1071
|
*
|
|
814
|
-
* Returns a {@link CompositeFilesystem}
|
|
815
|
-
*
|
|
1072
|
+
* Returns a {@link CompositeFilesystem} over every path that resolved for the
|
|
1073
|
+
* request. An empty composite exposes no implicit local storage.
|
|
816
1074
|
*/
|
|
817
1075
|
async function resolveWorkspaceFilesystem(
|
|
818
|
-
resolveContribution: (context:
|
|
819
|
-
context:
|
|
1076
|
+
resolveContribution: (context: DatabricksWorkspaceContext) => Promise<WorkspaceMountContribution>,
|
|
1077
|
+
context: DatabricksWorkspaceContext,
|
|
820
1078
|
filesCache?: FilesCacheExports,
|
|
1079
|
+
sourceKey?: string,
|
|
821
1080
|
): Promise<WorkspaceFilesystem> {
|
|
822
1081
|
const { mounts } = await resolveContribution(context);
|
|
823
1082
|
const mountKeys = Object.keys(mounts);
|
|
824
1083
|
if (mountKeys.length === 0) {
|
|
825
|
-
logger.debug("filesystem:
|
|
1084
|
+
logger.debug("filesystem:empty", {
|
|
826
1085
|
hasRequestContext: Boolean(context.requestContext),
|
|
827
1086
|
});
|
|
828
|
-
return
|
|
1087
|
+
return filesystems(new MemoryFileSystem({ root: "/", readOnly: true }), {
|
|
1088
|
+
readOnly: true,
|
|
1089
|
+
});
|
|
829
1090
|
}
|
|
830
1091
|
logger.debug("filesystem:composite", { mountKeys });
|
|
831
1092
|
const load = () => new MountedCompositeFilesystem({ mounts });
|
|
832
1093
|
if (!filesCache) return load();
|
|
833
1094
|
const scoped = await resolveFilesCacheScope(context);
|
|
834
1095
|
if (!scoped) return load();
|
|
835
|
-
return filesCache.forFileSystem(scoped.scope, { paths: mountKeys }, load);
|
|
1096
|
+
return filesCache.forFileSystem(scoped.scope, { paths: mountKeys, key: sourceKey }, load);
|
|
836
1097
|
}
|
|
837
1098
|
|
|
838
1099
|
/**
|
|
839
1100
|
* Expand skill roots into concrete skill directories and keep the first
|
|
840
1101
|
* same-named directory. Mastra still owns parsing, indexing, and refreshes,
|
|
841
|
-
* while
|
|
1102
|
+
* while earlier configured roots retain precedence over later roots.
|
|
842
1103
|
*/
|
|
843
1104
|
async function resolveDistinctSkillPaths(
|
|
844
1105
|
filesystem: WorkspaceFilesystem,
|
|
@@ -870,7 +1131,7 @@ async function resolveDistinctSkillPaths(
|
|
|
870
1131
|
/** Memoize one complete mount contribution for each request context. */
|
|
871
1132
|
function contributionResolver(
|
|
872
1133
|
resolvers: WorkspaceMountResolver[],
|
|
873
|
-
): (context:
|
|
1134
|
+
): (context: DatabricksWorkspaceContext) => Promise<WorkspaceMountContribution> {
|
|
874
1135
|
const scoped = new WeakMap<RequestContext, Promise<WorkspaceMountContribution>>();
|
|
875
1136
|
let unscoped: Promise<WorkspaceMountContribution> | undefined;
|
|
876
1137
|
return (context) => {
|