@dbx-tools/appkit-mastra 0.9.93 → 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 +166 -90
- package/index.ts +6 -12
- package/lib/index.d.ts +6 -12
- package/lib/index.js +5 -9
- package/lib/src/agents.d.ts +3 -3
- package/lib/src/agents.js +18 -27
- package/lib/src/chat.js +20 -1
- package/lib/src/config.d.ts +9 -11
- package/lib/src/config.js +7 -7
- package/lib/src/filesystems.d.ts +20 -2
- package/lib/src/filesystems.js +29 -2
- 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/plugin.js +3 -2
- package/lib/src/remote-skills.d.ts +3 -4
- package/lib/src/remote-skills.js +5 -14
- package/lib/src/server.d.ts +7 -0
- package/lib/src/server.js +33 -2
- package/lib/src/skill-paths.d.ts +9 -5
- package/lib/src/skill-paths.js +13 -7
- package/lib/src/workspaces.d.ts +75 -108
- package/lib/src/workspaces.js +459 -260
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -30
- package/src/agents.ts +18 -29
- package/src/chat.ts +24 -0
- package/src/config.ts +15 -17
- package/src/filesystems.ts +43 -0
- package/src/monty-sandbox.ts +18 -29
- package/src/openapi-tool.ts +2 -11
- package/src/plugin.ts +2 -0
- package/src/remote-skills.ts +5 -15
- package/src/server.ts +35 -1
- package/src/skill-paths.ts +14 -6
- package/src/workspaces.ts +702 -366
- package/lib/src/skill-cache.d.ts +0 -94
- package/lib/src/skill-cache.js +0 -251
- package/lib/src/skill-search.d.ts +0 -30
- package/lib/src/skill-search.js +0 -171
- package/src/skill-cache.ts +0 -347
- package/src/skill-search.ts +0 -203
package/src/workspaces.ts
CHANGED
|
@@ -1,39 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mastra workspace factory for Databricks Apps.
|
|
3
3
|
*
|
|
4
|
-
* Builds
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* on {@link MASTRA_USER_KEY}, or any {@link WorkspaceFilesystem} a consuming
|
|
9
|
-
* library already owns. {@link DEFAULT_SKILL_FOLDERS} supplies the Assistant
|
|
10
|
-
* trees, and `skillFolders` merges over it - same name overrides, `false`
|
|
11
|
-
* disables, a new name adds. Optional mount resolvers contribute further
|
|
12
|
-
* 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.
|
|
13
8
|
*
|
|
14
9
|
* Databricks mounts use `@dbx-tools/databricks` {@link DatabricksFileSystem}
|
|
15
|
-
* wrapped by {@link filesystems};
|
|
16
|
-
* {@link scratchFilesystem}.
|
|
10
|
+
* wrapped by {@link filesystems}; inaccessible roots are skipped independently.
|
|
17
11
|
*
|
|
18
12
|
* @module
|
|
19
13
|
*/
|
|
20
14
|
|
|
21
15
|
import { createHash } from "node:crypto";
|
|
22
|
-
import {
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { ConfigurationError, createWorkspaceClient, getExecutionContext } from "@databricks/appkit";
|
|
23
19
|
import type { WorkspaceClient } from "@databricks/appkit";
|
|
20
|
+
import { pluginRegistry } from "@dbx-tools/appkit";
|
|
21
|
+
import {
|
|
22
|
+
filesCache as filesCachePlugin,
|
|
23
|
+
type FilesCacheExports,
|
|
24
|
+
type FilesCacheScope,
|
|
25
|
+
} from "@dbx-tools/appkit/files-cache";
|
|
24
26
|
import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
|
|
25
27
|
import { LocalFileSystem } from "@dbx-tools/fs";
|
|
28
|
+
import { match, type PathMatchInput } from "@dbx-tools/path";
|
|
26
29
|
import { errorUtils, log, object, stringUtils, token } from "@dbx-tools/shared-core";
|
|
27
|
-
import
|
|
28
|
-
|
|
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";
|
|
29
39
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
30
40
|
import {
|
|
31
|
-
|
|
41
|
+
WORKSPACE_TOOLS,
|
|
32
42
|
Workspace,
|
|
33
43
|
type SkillsResolver,
|
|
44
|
+
type ToolConfigWithArgsContext,
|
|
45
|
+
type WorkspaceConfig,
|
|
34
46
|
type WorkspaceFilesystem,
|
|
35
47
|
type WorkspaceSandbox,
|
|
36
48
|
type WorkspaceSandboxResolver,
|
|
49
|
+
type WorkspaceToolConfig,
|
|
50
|
+
type WorkspaceToolsConfig,
|
|
37
51
|
} from "@mastra/core/workspace";
|
|
38
52
|
|
|
39
53
|
import {
|
|
@@ -43,94 +57,129 @@ import {
|
|
|
43
57
|
resolveUserKey,
|
|
44
58
|
type User,
|
|
45
59
|
} from "./config.ts";
|
|
46
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
filesystems,
|
|
62
|
+
MountedCompositeFilesystem,
|
|
63
|
+
type MastraFileSystemAdapterOptions,
|
|
64
|
+
} from "./filesystems.ts";
|
|
47
65
|
import { MontySandbox } from "./monty-sandbox.ts";
|
|
48
66
|
import { DatabricksSandbox, type DatabricksWorkspaceSandboxOptions } from "./sandbox.ts";
|
|
49
|
-
import {
|
|
50
|
-
DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
|
|
51
|
-
workspaceSkillCatalogue,
|
|
52
|
-
type SkillCatalogueResolver,
|
|
53
|
-
type SkillCatalogueSource,
|
|
54
|
-
} from "./skill-cache.ts";
|
|
55
|
-
import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-paths.ts";
|
|
67
|
+
import { ORGANIZATION_ASSISTANT_PATH, personalWorkspacePath } from "./skill-paths.ts";
|
|
56
68
|
|
|
57
69
|
/* ------------------------------ constants ------------------------------ */
|
|
58
70
|
|
|
59
71
|
/** OAuth scopes that gate Databricks workspace file mounts. */
|
|
60
72
|
const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] as const;
|
|
73
|
+
const SCRATCH_MOUNT = "/tmp";
|
|
74
|
+
const CACHEABLE_FILESYSTEM_OPERATIONS = [
|
|
75
|
+
"exists",
|
|
76
|
+
"readFile",
|
|
77
|
+
"readdir",
|
|
78
|
+
"stat",
|
|
79
|
+
] as const satisfies readonly CacheableFileSystemOperation[];
|
|
61
80
|
|
|
62
81
|
const logger = log.logger("mastra/workspaces");
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
max: MAX_SCOPED_WORKSPACE_FILESYSTEMS,
|
|
66
|
-
});
|
|
67
|
-
const workspaceCatalogueResolvers = new WeakMap<Workspace, SkillCatalogueResolver>();
|
|
82
|
+
let workspaceSourceSequence = 0;
|
|
83
|
+
const databricksWorkspaceOptions = new WeakMap<Workspace, DatabricksWorkspaceOptions>();
|
|
68
84
|
|
|
69
85
|
/* -------------------------------- types -------------------------------- */
|
|
70
86
|
|
|
71
87
|
/** Per-request context for mount and skill-folder resolvers. */
|
|
72
|
-
export interface
|
|
88
|
+
export interface DatabricksWorkspaceContext {
|
|
73
89
|
requestContext?: RequestContext;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
/**
|
|
77
|
-
*
|
|
78
|
-
* A resolver returning `undefined` skips the folder for that request.
|
|
93
|
+
* One Databricks path mounted into a Mastra workspace.
|
|
79
94
|
*/
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* request's OBO client), or {@link filesystem} for a mount the consumer builds
|
|
88
|
-
* itself. {@link filesystem} wins when both are set.
|
|
89
|
-
*/
|
|
90
|
-
export interface SkillFolderOptions {
|
|
91
|
-
/** Absolute Databricks workspace path for this folder. */
|
|
92
|
-
path?: SkillFolderValue<string>;
|
|
93
|
-
/** Ready-made mount, for locations the OBO client cannot reach. */
|
|
94
|
-
filesystem?: SkillFolderValue<WorkspaceFilesystem>;
|
|
95
|
+
export interface DatabricksWorkspacePathOptions {
|
|
96
|
+
/** Absolute Databricks path or `~` shortcut for the current user's workspace home. */
|
|
97
|
+
path: string;
|
|
98
|
+
/** Human-friendly mount name shown in filesystem listings. */
|
|
99
|
+
displayName?: string;
|
|
100
|
+
/** Mount description shown in filesystem listings and workspace instructions. */
|
|
101
|
+
description?: string;
|
|
95
102
|
/**
|
|
96
103
|
* Scan this mount for `SKILL.md` files. Defaults to `true`; `false` mounts
|
|
97
104
|
* the location for file tools without adding it to skill discovery.
|
|
98
105
|
*/
|
|
99
106
|
readable?: boolean;
|
|
100
107
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
108
|
+
* Skill roots relative to this filesystem root. Defaults to `["."]`.
|
|
109
|
+
* Each path is joined to the actual mount path before Mastra scans it.
|
|
110
|
+
*/
|
|
111
|
+
skills?: readonly string[];
|
|
112
|
+
/**
|
|
113
|
+
* Allow write attempts to a {@link path} mount. Defaults to `true` for
|
|
114
|
+
* `/Workspace` roots and `false` elsewhere. Databricks permissions still
|
|
115
|
+
* determine whether each mutation succeeds.
|
|
104
116
|
*/
|
|
105
117
|
writable?: boolean;
|
|
106
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Create a writable {@link path} root when missing. Defaults to
|
|
120
|
+
* {@link writable}; set `false` for pre-existing roots with conditional access.
|
|
121
|
+
*/
|
|
122
|
+
createRoot?: boolean;
|
|
123
|
+
/** Mount point in the composite namespace. Databricks paths default to their actual root. */
|
|
107
124
|
mount?: string;
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
/**
|
|
111
|
-
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 {
|
|
112
171
|
mounts: Record<string, WorkspaceFilesystem>;
|
|
113
|
-
/** Ordered source roots included in catalogue discovery. */
|
|
114
|
-
skillSources?: SkillCatalogueSource[];
|
|
115
172
|
/** Paths within the composite namespace where `SKILL.md` files are scanned. */
|
|
116
173
|
skillPaths?: string[];
|
|
117
|
-
/**
|
|
118
|
-
* Stable identity for this contribution. When every non-empty contribution
|
|
119
|
-
* supplies one, Mastra reuses the resolved source across requests.
|
|
120
|
-
*/
|
|
121
|
-
cacheKey?: string;
|
|
122
174
|
}
|
|
123
175
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
context:
|
|
176
|
+
/** Internal filesystem contribution resolver. */
|
|
177
|
+
type WorkspaceMountResolver = (
|
|
178
|
+
context: DatabricksWorkspaceContext,
|
|
127
179
|
) => WorkspaceMountContribution | Promise<WorkspaceMountContribution>;
|
|
128
180
|
|
|
129
|
-
/** Names carried by {@link DEFAULT_SKILL_FOLDERS}. */
|
|
130
|
-
export type DefaultSkillFolderName = "workspace-team" | "workspace-team-app";
|
|
131
|
-
|
|
132
181
|
/**
|
|
133
|
-
* Sandbox selection for {@link
|
|
182
|
+
* Sandbox selection for {@link databricksWorkspace}. Monty is the default;
|
|
134
183
|
* `false` disables command execution, and a Mastra provider or resolver is an
|
|
135
184
|
* explicit replacement.
|
|
136
185
|
*/
|
|
@@ -142,36 +191,26 @@ export type WorkspaceSandboxSelection =
|
|
|
142
191
|
| WorkspaceSandbox
|
|
143
192
|
| WorkspaceSandboxResolver;
|
|
144
193
|
|
|
145
|
-
/**
|
|
146
|
-
export interface
|
|
147
|
-
|
|
148
|
-
id?: string;
|
|
149
|
-
/** Display name; derived from `id` when omitted. */
|
|
150
|
-
name?: string;
|
|
194
|
+
/** Native Mastra workspace options plus Databricks path shortcuts. */
|
|
195
|
+
export interface DatabricksWorkspaceOptions
|
|
196
|
+
extends Omit<WorkspaceConfig, "filesystem" | "mounts" | "sandbox" | "skills"> {
|
|
151
197
|
/**
|
|
152
|
-
* Start from {@link
|
|
153
|
-
* starts from an empty map, leaving only the {@link skillFolders} given here.
|
|
198
|
+
* Start from {@link DEFAULT_DATABRICKS_WORKSPACE_PATHS}. Defaults to `true`.
|
|
154
199
|
*/
|
|
155
|
-
|
|
200
|
+
assistantPaths?: boolean;
|
|
156
201
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* a folder.
|
|
202
|
+
* Databricks paths appended after the defaults. A string is shorthand for
|
|
203
|
+
* `{ path }`; a resolver may return either form per request.
|
|
160
204
|
*/
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
mounts?:
|
|
205
|
+
paths?: readonly DatabricksWorkspacePathValue[];
|
|
206
|
+
/** Native Mastra filesystems composed beside the Databricks paths. */
|
|
207
|
+
mounts?: Record<string, WorkspaceFilesystem>;
|
|
164
208
|
/** Replace the auto-built dynamic skills resolver. */
|
|
165
209
|
skills?: SkillsResolver;
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
* always fresh. Defaults to five minutes.
|
|
171
|
-
*/
|
|
172
|
-
workspaceSkillRefreshTtlMs?: number;
|
|
173
|
-
/** Enable BM25 keyword search over indexed workspace content. */
|
|
174
|
-
bm25?: boolean;
|
|
210
|
+
/** Filesystem cache filters. Ignored when the AppKit files-cache plugin is absent. */
|
|
211
|
+
cache?: DatabricksWorkspaceCache;
|
|
212
|
+
/** AppKit plugin context used to discover optional sibling capabilities. */
|
|
213
|
+
pluginContext?: pluginRegistry.PluginContextLike;
|
|
175
214
|
/**
|
|
176
215
|
* Command sandbox. Defaults to Monty. Pass `"databricks"` or an options
|
|
177
216
|
* object for Databricks Sandbox, `false` to disable command execution, or an
|
|
@@ -191,96 +230,123 @@ export interface CreateWorkspaceOptions {
|
|
|
191
230
|
/* ------------------------------- defaults ------------------------------- */
|
|
192
231
|
|
|
193
232
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* - `workspace-team` - the shared workspace Assistant tree, read-only because
|
|
197
|
-
* writing it is a workspace-admin action.
|
|
198
|
-
* - `workspace-team-app` - the requesting user's own Assistant tree, writable
|
|
199
|
-
* so the app can save skills back to it. Skipped when the request carries no
|
|
200
|
-
* user email.
|
|
233
|
+
* Default Databricks paths for shared and current-user Agent Skills.
|
|
201
234
|
*/
|
|
202
|
-
export const
|
|
203
|
-
|
|
204
|
-
path:
|
|
235
|
+
export const DEFAULT_DATABRICKS_WORKSPACE_PATHS: readonly DatabricksWorkspacePathOptions[] = [
|
|
236
|
+
{
|
|
237
|
+
path: ORGANIZATION_ASSISTANT_PATH,
|
|
205
238
|
readable: true,
|
|
206
|
-
|
|
239
|
+
skills: ["skills"],
|
|
240
|
+
writable: true,
|
|
241
|
+
createRoot: false,
|
|
207
242
|
},
|
|
208
|
-
|
|
209
|
-
path:
|
|
210
|
-
const email = resolveScopedEmail(requestContext);
|
|
211
|
-
return email ? userAssistantSkillsPath(email) : undefined;
|
|
212
|
-
},
|
|
243
|
+
{
|
|
244
|
+
path: "~",
|
|
213
245
|
readable: true,
|
|
246
|
+
skills: [".assistant/skills"],
|
|
214
247
|
writable: true,
|
|
248
|
+
createRoot: false,
|
|
215
249
|
},
|
|
216
|
-
|
|
250
|
+
];
|
|
217
251
|
|
|
218
252
|
/**
|
|
219
253
|
* Create a Mastra {@link Workspace} with per-request Databricks mounts.
|
|
220
254
|
*
|
|
221
|
-
* @example Default
|
|
255
|
+
* @example Default Databricks paths
|
|
222
256
|
* ```ts
|
|
223
|
-
*
|
|
257
|
+
* databricksWorkspace()
|
|
224
258
|
* ```
|
|
225
259
|
*
|
|
226
|
-
* @example
|
|
260
|
+
* @example Use only explicit paths
|
|
227
261
|
* ```ts
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* "workspace-team-app": false,
|
|
232
|
-
* runbooks: { path: "/Workspace/Shared/runbooks/skills", writable: true },
|
|
233
|
-
* volume: { filesystem: myVolumeFilesystem },
|
|
234
|
-
* },
|
|
262
|
+
* databricksWorkspace({
|
|
263
|
+
* assistantPaths: false,
|
|
264
|
+
* paths: ["~/project", "/tmp/project", "/Volumes/main/default/data"],
|
|
235
265
|
* })
|
|
236
266
|
* ```
|
|
237
267
|
*
|
|
238
|
-
* @example
|
|
268
|
+
* @example Build a standard Mastra workspace yourself
|
|
239
269
|
* ```ts
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* mounts: { "/data": myFilesystem },
|
|
244
|
-
* skillPaths: [],
|
|
245
|
-
* }),
|
|
246
|
-
* ],
|
|
270
|
+
* new Workspace({
|
|
271
|
+
* ...databricksWorkspaceConfig({ paths: ["~/project"] }),
|
|
272
|
+
* sandbox: mySandbox,
|
|
247
273
|
* })
|
|
248
274
|
* ```
|
|
249
275
|
*/
|
|
250
|
-
export function
|
|
276
|
+
export function databricksWorkspaceConfig(
|
|
277
|
+
options: DatabricksWorkspaceOptions = {},
|
|
278
|
+
): WorkspaceConfig {
|
|
251
279
|
const { id, name } = resolveWorkspaceIdentity(options);
|
|
252
|
-
const
|
|
253
|
-
const
|
|
280
|
+
const filesystemSourceKey = `${id}:${++workspaceSourceSequence}`;
|
|
281
|
+
const paths = resolveDatabricksWorkspacePaths(options);
|
|
282
|
+
const configuredPaths = paths.length;
|
|
254
283
|
const extraSkillPaths = options.extraSkillPaths ?? [];
|
|
284
|
+
const filesCache = pluginRegistry.instance(options.pluginContext, filesCachePlugin)?.exports();
|
|
285
|
+
const configuredCache = options.cache;
|
|
286
|
+
const mountFilesCache = configuredCache === false ? undefined : filesCache;
|
|
287
|
+
const retainFilesystemSource =
|
|
288
|
+
mountFilesCache !== undefined &&
|
|
289
|
+
typeof configuredCache !== "function" &&
|
|
290
|
+
paths.every((path) => typeof path !== "function");
|
|
255
291
|
const resolvers = buildMountResolvers(
|
|
256
|
-
|
|
257
|
-
|
|
292
|
+
paths,
|
|
293
|
+
options.mounts,
|
|
294
|
+
localSkillMountResolvers(extraSkillPaths),
|
|
295
|
+
mountFilesCache,
|
|
296
|
+
configuredCache,
|
|
258
297
|
);
|
|
259
298
|
const resolveContribution = contributionResolver(resolvers);
|
|
260
|
-
const
|
|
299
|
+
const resolveFilesystem = (context: DatabricksWorkspaceContext) =>
|
|
300
|
+
resolveWorkspaceFilesystem(
|
|
301
|
+
resolveContribution,
|
|
302
|
+
context,
|
|
303
|
+
retainFilesystemSource ? mountFilesCache : undefined,
|
|
304
|
+
filesystemSourceKey,
|
|
305
|
+
);
|
|
306
|
+
const skills: SkillsResolver =
|
|
307
|
+
options.skills ??
|
|
308
|
+
(async ({ requestContext }) => {
|
|
309
|
+
const context = { requestContext };
|
|
310
|
+
const contribution = await resolveContribution(context);
|
|
311
|
+
return resolveDistinctSkillPaths(
|
|
312
|
+
await resolveFilesystem(context),
|
|
313
|
+
contribution.skillPaths ?? [],
|
|
314
|
+
);
|
|
315
|
+
});
|
|
261
316
|
const checkSkillFileMtime = options.checkSkillFileMtime ?? false;
|
|
262
|
-
const bm25 = options.bm25
|
|
317
|
+
const bm25 = options.bm25 ?? true;
|
|
263
318
|
const sandbox = resolveWorkspaceSandbox(options.sandbox, id, name);
|
|
319
|
+
const tools = workspaceTools(options.tools);
|
|
264
320
|
logger.debug("workspace:create", {
|
|
265
321
|
id,
|
|
266
322
|
name,
|
|
267
323
|
resolverCount: resolvers.length,
|
|
268
|
-
|
|
269
|
-
|
|
324
|
+
configuredPaths,
|
|
325
|
+
customMounts: Object.keys(options.mounts ?? {}).length,
|
|
270
326
|
customSkillsResolver: Boolean(options.skills),
|
|
271
327
|
checkSkillFileMtime,
|
|
272
|
-
workspaceSkillRefreshTtlMs:
|
|
273
|
-
options.workspaceSkillRefreshTtlMs ?? DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
|
|
274
328
|
bm25,
|
|
275
329
|
extraSkillPaths: extraSkillPaths.length,
|
|
276
330
|
sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
|
|
277
331
|
});
|
|
278
332
|
|
|
279
|
-
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,
|
|
280
346
|
id,
|
|
281
347
|
name,
|
|
282
|
-
filesystem:
|
|
283
|
-
...(skills
|
|
348
|
+
filesystem: resolveFilesystem,
|
|
349
|
+
...(resolvers.length > 0 || options.skills
|
|
284
350
|
? {
|
|
285
351
|
skills,
|
|
286
352
|
checkSkillFileMtime,
|
|
@@ -293,46 +359,85 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
293
359
|
}
|
|
294
360
|
: {}),
|
|
295
361
|
bm25,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
362
|
+
tools,
|
|
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);
|
|
303
370
|
return workspace;
|
|
304
371
|
}
|
|
305
372
|
|
|
306
|
-
/**
|
|
307
|
-
export function
|
|
308
|
-
workspace: Workspace
|
|
309
|
-
|
|
310
|
-
|
|
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 });
|
|
311
383
|
}
|
|
312
384
|
|
|
313
385
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* `assistantSkills: false` drops the defaults, and a `false` value removes one
|
|
317
|
-
* entry by name.
|
|
386
|
+
* Resolve default and caller-provided Databricks paths in priority order.
|
|
318
387
|
*/
|
|
319
|
-
export function
|
|
320
|
-
options: Pick<
|
|
321
|
-
):
|
|
322
|
-
|
|
323
|
-
options.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
delete merged[name];
|
|
327
|
-
} else {
|
|
328
|
-
merged[name] = folder;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
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
|
+
];
|
|
332
395
|
}
|
|
333
396
|
|
|
334
397
|
/* ---------------------------- private helpers ---------------------------- */
|
|
335
398
|
|
|
399
|
+
/** Require approval unless a filesystem mutation stays inside a user-owned writable root. */
|
|
400
|
+
function requireApprovalOutsideWritableRoots({
|
|
401
|
+
args,
|
|
402
|
+
requestContext,
|
|
403
|
+
}: ToolConfigWithArgsContext): boolean {
|
|
404
|
+
const inputPath = args.path;
|
|
405
|
+
if (typeof inputPath !== "string" || !posixPath.isAbsolute(inputPath)) return true;
|
|
406
|
+
const normalized = posixPath.normalize(inputPath);
|
|
407
|
+
if (!normalized.ok) return true;
|
|
408
|
+
if (posixPath.isWithinRoot(SCRATCH_MOUNT, normalized.path)) return false;
|
|
409
|
+
const emailValue = requestContext[MASTRA_USER_EMAIL_KEY];
|
|
410
|
+
const email = typeof emailValue === "string" ? stringUtils.trimToNull(emailValue) : undefined;
|
|
411
|
+
if (!email) return true;
|
|
412
|
+
return !posixPath.isWithinRoot(personalWorkspacePath(email), normalized.path);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function workspaceTools(configured: WorkspaceToolsConfig | undefined): WorkspaceToolsConfig {
|
|
416
|
+
const requireApproval: WorkspaceToolConfig =
|
|
417
|
+
configured?.requireApproval === undefined
|
|
418
|
+
? { requireApproval: requireApprovalOutsideWritableRoots }
|
|
419
|
+
: {};
|
|
420
|
+
const defaults = {
|
|
421
|
+
[WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE]: requireApproval,
|
|
422
|
+
[WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE]: requireApproval,
|
|
423
|
+
[WORKSPACE_TOOLS.FILESYSTEM.AST_EDIT]: requireApproval,
|
|
424
|
+
[WORKSPACE_TOOLS.FILESYSTEM.DELETE]: requireApproval,
|
|
425
|
+
[WORKSPACE_TOOLS.FILESYSTEM.MKDIR]: requireApproval,
|
|
426
|
+
} satisfies WorkspaceToolsConfig;
|
|
427
|
+
const configuredTools = configured as
|
|
428
|
+
Readonly<Record<string, WorkspaceToolConfig | undefined>> | undefined;
|
|
429
|
+
const mergedTools = Object.fromEntries(
|
|
430
|
+
Object.entries(defaults).map(([name, defaultConfig]) => [
|
|
431
|
+
name,
|
|
432
|
+
{ ...defaultConfig, ...configuredTools?.[name] },
|
|
433
|
+
]),
|
|
434
|
+
);
|
|
435
|
+
return {
|
|
436
|
+
...defaults,
|
|
437
|
+
...configured,
|
|
438
|
+
...mergedTools,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
336
441
|
function resolveWorkspaceSandbox(
|
|
337
442
|
selection: WorkspaceSandboxSelection | undefined,
|
|
338
443
|
workspaceId: string,
|
|
@@ -410,6 +515,48 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
410
515
|
);
|
|
411
516
|
}
|
|
412
517
|
|
|
518
|
+
/** Resolve the request client and authorization-safe files-cache scope. */
|
|
519
|
+
async function resolveFilesCacheScope(context: DatabricksWorkspaceContext): Promise<
|
|
520
|
+
| {
|
|
521
|
+
client: WorkspaceClient;
|
|
522
|
+
scope: FilesCacheScope;
|
|
523
|
+
}
|
|
524
|
+
| undefined
|
|
525
|
+
> {
|
|
526
|
+
const requestContext = context.requestContext;
|
|
527
|
+
const user = requestContext?.get(MASTRA_USER_KEY) as User | undefined;
|
|
528
|
+
const client = user?.executionContext.client as WorkspaceClient | undefined;
|
|
529
|
+
if (!requestContext || !user || !client) return undefined;
|
|
530
|
+
return {
|
|
531
|
+
client,
|
|
532
|
+
scope: {
|
|
533
|
+
host: (await client.config.getHost()).toString(),
|
|
534
|
+
userKey: resolveUserKey(requestContext),
|
|
535
|
+
...(process.env.DATABRICKS_WORKSPACE_ID?.trim()
|
|
536
|
+
? { workspaceId: process.env.DATABRICKS_WORKSPACE_ID.trim() }
|
|
537
|
+
: {}),
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Join one relative skill root to its actual composite mount path. */
|
|
543
|
+
function mountedSkillPath(mount: string, skillRoot: string): string {
|
|
544
|
+
if (posixPath.isAbsolute(skillRoot)) {
|
|
545
|
+
throw new TypeError(`Skill path must be relative to its mount: ${skillRoot}`);
|
|
546
|
+
}
|
|
547
|
+
const normalized = posixPath.normalize(skillRoot);
|
|
548
|
+
if (!normalized.ok) {
|
|
549
|
+
throw new TypeError(`Skill path escapes its mount: ${skillRoot}`);
|
|
550
|
+
}
|
|
551
|
+
return posixPath.join(posixPath.normalizeRoot(mount), normalized.path.slice(1));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Return whether a Databricks path belongs to the writable Workspace namespace. */
|
|
555
|
+
function isWorkspaceRoot(root: string): boolean {
|
|
556
|
+
const normalized = posixPath.normalizeRoot(root);
|
|
557
|
+
return normalized === "/Workspace" || normalized.startsWith("/Workspace/");
|
|
558
|
+
}
|
|
559
|
+
|
|
413
560
|
/**
|
|
414
561
|
* Mount resolver for the named skill folders.
|
|
415
562
|
*
|
|
@@ -417,42 +564,54 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
417
564
|
* folder whose location resolves for this request.
|
|
418
565
|
*/
|
|
419
566
|
async function resolveSkillFolderMounts(
|
|
420
|
-
|
|
421
|
-
context:
|
|
567
|
+
paths: readonly DatabricksWorkspacePathValue[],
|
|
568
|
+
context: DatabricksWorkspaceContext,
|
|
569
|
+
filesCache: FilesCacheExports | undefined,
|
|
570
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
422
571
|
): Promise<WorkspaceMountContribution> {
|
|
423
572
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
424
|
-
const skillSources: SkillCatalogueSource[] = [];
|
|
425
573
|
const skillPaths: string[] = [];
|
|
426
|
-
const cacheKeys: string[] = [];
|
|
427
574
|
const requestContext = context.requestContext;
|
|
428
575
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
435
|
-
return { mounts, skillPaths };
|
|
436
|
-
}
|
|
576
|
+
const canMountDatabricks = Boolean(requestContext && shouldMountSkillFolders(requestContext));
|
|
577
|
+
const scoped = canMountDatabricks ? await resolveFilesCacheScope(context) : undefined;
|
|
578
|
+
const client = scoped?.client;
|
|
579
|
+
const fileSystemCache =
|
|
580
|
+
filesCache && scoped ? await filesCache.forScope(scoped.scope) : undefined;
|
|
437
581
|
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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, {
|
|
445
603
|
client,
|
|
446
|
-
|
|
447
|
-
userKey,
|
|
604
|
+
fileSystemCache,
|
|
448
605
|
});
|
|
449
606
|
if (!resolved) continue;
|
|
450
|
-
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
|
+
}
|
|
451
612
|
mounts[mount] = resolved.filesystem;
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
skillPaths.push(mount);
|
|
455
|
-
skillSources.push({ ...resolved.skillSource, id: object.toStableKey({ mount, source: resolved.skillSource.id }) });
|
|
613
|
+
if (configured.readable !== false) {
|
|
614
|
+
skillPaths.push(...(configured.skills ?? ["."]).map((path) => mountedSkillPath(mount, path)));
|
|
456
615
|
}
|
|
457
616
|
}
|
|
458
617
|
|
|
@@ -463,11 +622,36 @@ async function resolveSkillFolderMounts(
|
|
|
463
622
|
|
|
464
623
|
return {
|
|
465
624
|
mounts,
|
|
466
|
-
skillSources,
|
|
467
625
|
skillPaths,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
626
|
+
};
|
|
627
|
+
}
|
|
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 } : {}),
|
|
471
655
|
};
|
|
472
656
|
}
|
|
473
657
|
|
|
@@ -476,69 +660,164 @@ async function resolveSkillFolderMounts(
|
|
|
476
660
|
* for this request.
|
|
477
661
|
*/
|
|
478
662
|
async function resolveSkillFolderFilesystem(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
663
|
+
folder: ResolvedDatabricksWorkspacePath,
|
|
664
|
+
context: DatabricksWorkspaceContext,
|
|
665
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
482
666
|
cache: {
|
|
483
667
|
client: WorkspaceClient | undefined;
|
|
484
|
-
|
|
485
|
-
userKey: string | undefined;
|
|
668
|
+
fileSystemCache: FileSystemCache | undefined;
|
|
486
669
|
},
|
|
487
670
|
): Promise<
|
|
488
671
|
| {
|
|
489
|
-
cacheKey?: string;
|
|
490
672
|
filesystem: WorkspaceFilesystem;
|
|
491
|
-
|
|
673
|
+
root?: string;
|
|
492
674
|
}
|
|
493
675
|
| undefined
|
|
494
676
|
> {
|
|
495
|
-
if (folder.filesystem !== undefined) {
|
|
496
|
-
const filesystem = await resolveSkillFolderValue(folder.filesystem, context);
|
|
497
|
-
return filesystem
|
|
498
|
-
? {
|
|
499
|
-
filesystem,
|
|
500
|
-
skillSource: mastraSkillSource(
|
|
501
|
-
object.toStableKey({
|
|
502
|
-
id: filesystem.id,
|
|
503
|
-
name,
|
|
504
|
-
provider: filesystem.provider,
|
|
505
|
-
}),
|
|
506
|
-
filesystem,
|
|
507
|
-
),
|
|
508
|
-
}
|
|
509
|
-
: undefined;
|
|
510
|
-
}
|
|
511
677
|
// A path mount needs the request's OBO client to reach the workspace.
|
|
512
|
-
if (
|
|
678
|
+
if (!cache.client) {
|
|
513
679
|
logger.debug("skill-folder:skipped", {
|
|
514
|
-
|
|
515
|
-
reason:
|
|
680
|
+
path: folder.path,
|
|
681
|
+
reason: "missing-obo-client",
|
|
516
682
|
});
|
|
517
683
|
return undefined;
|
|
518
684
|
}
|
|
519
|
-
const root = stringUtils.trimToNull(
|
|
685
|
+
const root = stringUtils.trimToNull(folder.path);
|
|
520
686
|
if (!root) return undefined;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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;
|
|
696
|
+
const resolved = await databricksFilesystem(
|
|
697
|
+
cache.client,
|
|
698
|
+
root,
|
|
699
|
+
!writable,
|
|
700
|
+
folder.createRoot ?? folder.writable === true,
|
|
701
|
+
cache.fileSystemCache,
|
|
702
|
+
cacheOptions,
|
|
703
|
+
{
|
|
704
|
+
...(folder.description ? { description: folder.description } : {}),
|
|
705
|
+
...(folder.displayName ? { displayName: folder.displayName } : {}),
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
return resolved ? { ...resolved, root } : undefined;
|
|
525
709
|
}
|
|
526
710
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
711
|
+
async function resolveCacheConfig(
|
|
712
|
+
value: DatabricksWorkspaceCache | undefined,
|
|
713
|
+
context: DatabricksWorkspaceContext,
|
|
714
|
+
): Promise<
|
|
715
|
+
| boolean
|
|
716
|
+
| DatabricksWorkspaceCacheFilter
|
|
717
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
718
|
+
| undefined
|
|
719
|
+
> {
|
|
532
720
|
return typeof value === "function"
|
|
533
|
-
?
|
|
721
|
+
? value(context)
|
|
534
722
|
: value;
|
|
535
723
|
}
|
|
536
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
|
+
|
|
537
816
|
/**
|
|
538
|
-
* Fill in `id` and `name` when either is omitted on {@link
|
|
817
|
+
* Fill in `id` and `name` when either is omitted on {@link DatabricksWorkspaceOptions}.
|
|
539
818
|
* Slugifies `name` into `id`; tokenizes `id` into a display `name`.
|
|
540
819
|
*/
|
|
541
|
-
function resolveWorkspaceIdentity(options:
|
|
820
|
+
function resolveWorkspaceIdentity(options: DatabricksWorkspaceOptions): {
|
|
542
821
|
id: string;
|
|
543
822
|
name: string;
|
|
544
823
|
} {
|
|
@@ -555,29 +834,58 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
|
|
|
555
834
|
|
|
556
835
|
/** Collect the skill-folder resolver and any caller-supplied ones. */
|
|
557
836
|
function buildMountResolvers(
|
|
558
|
-
|
|
559
|
-
mounts:
|
|
837
|
+
paths: readonly DatabricksWorkspacePathValue[],
|
|
838
|
+
mounts: Record<string, WorkspaceFilesystem> | undefined,
|
|
839
|
+
localMounts: WorkspaceMountResolver[],
|
|
840
|
+
filesCache: FilesCacheExports | undefined,
|
|
841
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
560
842
|
): WorkspaceMountResolver[] {
|
|
561
843
|
const resolvers: WorkspaceMountResolver[] = [];
|
|
562
|
-
const
|
|
563
|
-
if (
|
|
564
|
-
resolvers.push((context) =>
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
resolvers.push(...mounts);
|
|
844
|
+
const pathCount = paths.length;
|
|
845
|
+
if (pathCount > 0) {
|
|
846
|
+
resolvers.push((context) =>
|
|
847
|
+
resolveSkillFolderMounts(paths, context, filesCache, cacheConfig),
|
|
848
|
+
);
|
|
568
849
|
}
|
|
850
|
+
if (mounts && Object.keys(mounts).length > 0) resolvers.push(() => ({ mounts }));
|
|
851
|
+
resolvers.push(...localMounts);
|
|
569
852
|
logger.debug("mounts:resolvers", {
|
|
570
|
-
|
|
571
|
-
|
|
853
|
+
pathCount,
|
|
854
|
+
customMountCount: Object.keys(mounts ?? {}).length,
|
|
855
|
+
localMountCount: localMounts.length,
|
|
572
856
|
totalResolverCount: resolvers.length,
|
|
573
857
|
});
|
|
574
858
|
return resolvers;
|
|
575
859
|
}
|
|
576
860
|
|
|
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.",
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
577
885
|
/** Mount startup-provisioned local skill roots into Mastra's workspace filesystem. */
|
|
578
886
|
function localSkillMountResolvers(paths: readonly string[]): WorkspaceMountResolver[] {
|
|
579
|
-
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].map((root
|
|
580
|
-
const mount =
|
|
887
|
+
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].map((root) => {
|
|
888
|
+
const mount = posixPath.normalizeRoot(root);
|
|
581
889
|
const source = new LocalFileSystem({
|
|
582
890
|
root,
|
|
583
891
|
readOnly: true,
|
|
@@ -586,51 +894,11 @@ function localSkillMountResolvers(paths: readonly string[]): WorkspaceMountResol
|
|
|
586
894
|
const filesystem = filesystems(source, { readOnly: true });
|
|
587
895
|
return () => ({
|
|
588
896
|
mounts: { [mount]: filesystem },
|
|
589
|
-
skillSources: [sharedSkillSource(object.toStableKey({ mount, root }), source)],
|
|
590
897
|
skillPaths: [mount],
|
|
591
|
-
cacheKey: object.toStableKey({ mount, root }),
|
|
592
898
|
});
|
|
593
899
|
});
|
|
594
900
|
}
|
|
595
901
|
|
|
596
|
-
/** Adapt a portable dbx-tools filesystem to catalogue discovery callbacks. */
|
|
597
|
-
function sharedSkillSource(id: string, filesystem: FileSystem): SkillCatalogueSource {
|
|
598
|
-
return {
|
|
599
|
-
id,
|
|
600
|
-
async list(path) {
|
|
601
|
-
await filesystem.init();
|
|
602
|
-
return (await filesystem.readdir(path)).flatMap((entry) =>
|
|
603
|
-
entry.type === "file" || entry.type === "directory"
|
|
604
|
-
? [
|
|
605
|
-
{
|
|
606
|
-
name: entry.name,
|
|
607
|
-
type: entry.type,
|
|
608
|
-
...(entry.size !== undefined ? { size: entry.size } : {}),
|
|
609
|
-
...(entry.metadata ? { metadata: entry.metadata } : {}),
|
|
610
|
-
},
|
|
611
|
-
]
|
|
612
|
-
: [],
|
|
613
|
-
);
|
|
614
|
-
},
|
|
615
|
-
async read(path) {
|
|
616
|
-
await filesystem.init();
|
|
617
|
-
return filesystem.readFile(path, { encoding: "utf8" });
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/** Adapt an arbitrary Mastra filesystem when no portable source is available. */
|
|
623
|
-
function mastraSkillSource(id: string, filesystem: WorkspaceFilesystem): SkillCatalogueSource {
|
|
624
|
-
return {
|
|
625
|
-
id,
|
|
626
|
-
list: (path) => filesystem.readdir(path),
|
|
627
|
-
async read(path) {
|
|
628
|
-
const value = await filesystem.readFile(path, { encoding: "utf8" });
|
|
629
|
-
return typeof value === "string" ? value : value.toString("utf8");
|
|
630
|
-
},
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
902
|
/**
|
|
635
903
|
* Gate skill-folder mounts on the request's token.
|
|
636
904
|
*
|
|
@@ -642,53 +910,49 @@ function shouldMountSkillFolders(requestContext: RequestContext): boolean {
|
|
|
642
910
|
return hasWorkspaceFileScope(requestContext);
|
|
643
911
|
}
|
|
644
912
|
|
|
645
|
-
/** Read the trimmed OBO user email stamped on {@link MASTRA_USER_EMAIL_KEY}. */
|
|
646
|
-
function resolveScopedEmail(requestContext: RequestContext | undefined): string | undefined {
|
|
647
|
-
return stringUtils.trimToNull(requestContext?.get(MASTRA_USER_EMAIL_KEY)) ?? undefined;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
913
|
/**
|
|
651
|
-
* Wrap a {@link DatabricksFileSystem} as a Mastra filesystem.
|
|
652
|
-
*
|
|
653
|
-
*
|
|
914
|
+
* Wrap a {@link DatabricksFileSystem} as a Mastra filesystem. Missing or
|
|
915
|
+
* inaccessible roots return `undefined` so other accessible roots remain
|
|
916
|
+
* usable.
|
|
654
917
|
*/
|
|
655
918
|
async function databricksFilesystem(
|
|
656
919
|
client: WorkspaceClient,
|
|
657
920
|
root: string,
|
|
658
921
|
readOnly: boolean = true,
|
|
659
|
-
|
|
922
|
+
createRoot: boolean = !readOnly,
|
|
923
|
+
fileSystemCache?: FileSystemCache,
|
|
924
|
+
cacheOptions?: FileSystemCacheOptions,
|
|
925
|
+
adapterOptions: Pick<MastraFileSystemAdapterOptions, "description" | "displayName"> = {},
|
|
660
926
|
): Promise<
|
|
661
|
-
{
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
skillSource: SkillCatalogueSource;
|
|
665
|
-
}
|
|
927
|
+
| {
|
|
928
|
+
filesystem: WorkspaceFilesystem;
|
|
929
|
+
}
|
|
666
930
|
| undefined
|
|
667
931
|
> {
|
|
932
|
+
const initialClient = workspaceClient.toLegacyWorkspaceClient(client);
|
|
668
933
|
const fs = new DatabricksFileSystem({
|
|
669
|
-
client:
|
|
934
|
+
client: () => {
|
|
935
|
+
try {
|
|
936
|
+
return workspaceClient.toLegacyWorkspaceClient(getExecutionContext().client);
|
|
937
|
+
} catch {
|
|
938
|
+
return initialClient;
|
|
939
|
+
}
|
|
940
|
+
},
|
|
670
941
|
root,
|
|
671
942
|
readOnly,
|
|
672
|
-
createRoot
|
|
673
|
-
});
|
|
674
|
-
const cacheKey = object.toStableKey({
|
|
675
|
-
host: cache.host,
|
|
676
|
-
readOnly,
|
|
677
|
-
root,
|
|
678
|
-
userKey: cache.userKey,
|
|
943
|
+
createRoot,
|
|
679
944
|
});
|
|
680
|
-
const
|
|
945
|
+
const cached =
|
|
946
|
+
fileSystemCache && cacheOptions ? sharedFS.cache(fs, fileSystemCache, cacheOptions) : fs;
|
|
947
|
+
const source = verifyFirstFileSystemAccess(cached, () => fs.stat("."), root);
|
|
948
|
+
const filesystem = filesystems(source, { ...adapterOptions, readOnly });
|
|
681
949
|
try {
|
|
682
950
|
await filesystem.init();
|
|
683
951
|
if (await filesystem.exists(".")) {
|
|
684
|
-
return {
|
|
685
|
-
cacheKey,
|
|
686
|
-
filesystem,
|
|
687
|
-
skillSource: sharedSkillSource(cacheKey, fs),
|
|
688
|
-
};
|
|
952
|
+
return { filesystem };
|
|
689
953
|
}
|
|
690
954
|
} catch (err) {
|
|
691
|
-
logger.debug("databricks-mount:
|
|
955
|
+
logger.debug("databricks-mount:skipped", {
|
|
692
956
|
root,
|
|
693
957
|
readOnly,
|
|
694
958
|
error: errorUtils.errorMessage(err),
|
|
@@ -697,19 +961,86 @@ async function databricksFilesystem(
|
|
|
697
961
|
return undefined;
|
|
698
962
|
}
|
|
699
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
|
+
|
|
700
1034
|
/**
|
|
701
1035
|
* Run every mount resolver for one request and merge mounts plus skill paths.
|
|
702
1036
|
* Later resolvers overwrite mount keys from earlier ones.
|
|
703
1037
|
*/
|
|
704
1038
|
async function resolveWorkspaceContribution(
|
|
705
1039
|
resolvers: WorkspaceMountResolver[],
|
|
706
|
-
context:
|
|
1040
|
+
context: DatabricksWorkspaceContext,
|
|
707
1041
|
): Promise<WorkspaceMountContribution> {
|
|
708
1042
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
709
|
-
const skillSources: SkillCatalogueSource[] = [];
|
|
710
1043
|
const skillPaths: string[] = [];
|
|
711
|
-
const cacheKeys: string[] = [];
|
|
712
|
-
let cacheable = true;
|
|
713
1044
|
|
|
714
1045
|
for (const [index, resolver] of resolvers.entries()) {
|
|
715
1046
|
const contribution = await resolver(context);
|
|
@@ -719,16 +1050,9 @@ async function resolveWorkspaceContribution(
|
|
|
719
1050
|
skillPaths: contribution.skillPaths ?? [],
|
|
720
1051
|
});
|
|
721
1052
|
Object.assign(mounts, contribution.mounts);
|
|
722
|
-
if (Object.keys(contribution.mounts).length > 0) {
|
|
723
|
-
if (contribution.cacheKey) cacheKeys.push(contribution.cacheKey);
|
|
724
|
-
else cacheable = false;
|
|
725
|
-
}
|
|
726
1053
|
if (contribution.skillPaths?.length) {
|
|
727
1054
|
skillPaths.push(...contribution.skillPaths);
|
|
728
1055
|
}
|
|
729
|
-
if (contribution.skillSources?.length) {
|
|
730
|
-
skillSources.push(...contribution.skillSources);
|
|
731
|
-
}
|
|
732
1056
|
}
|
|
733
1057
|
|
|
734
1058
|
logger.debug("mounts:merged", {
|
|
@@ -738,45 +1062,76 @@ async function resolveWorkspaceContribution(
|
|
|
738
1062
|
|
|
739
1063
|
return {
|
|
740
1064
|
mounts,
|
|
741
|
-
skillSources,
|
|
742
1065
|
skillPaths,
|
|
743
|
-
...(cacheable && cacheKeys.length > 0
|
|
744
|
-
? { cacheKey: object.toStableKey({ mounts: Object.keys(mounts), sources: cacheKeys }) }
|
|
745
|
-
: {}),
|
|
746
1066
|
};
|
|
747
1067
|
}
|
|
748
1068
|
|
|
749
1069
|
/**
|
|
750
1070
|
* Dynamic filesystem resolver passed to Mastra {@link Workspace}.
|
|
751
1071
|
*
|
|
752
|
-
* Returns a {@link CompositeFilesystem}
|
|
753
|
-
*
|
|
1072
|
+
* Returns a {@link CompositeFilesystem} over every path that resolved for the
|
|
1073
|
+
* request. An empty composite exposes no implicit local storage.
|
|
754
1074
|
*/
|
|
755
1075
|
async function resolveWorkspaceFilesystem(
|
|
756
|
-
resolveContribution: (context:
|
|
757
|
-
context:
|
|
1076
|
+
resolveContribution: (context: DatabricksWorkspaceContext) => Promise<WorkspaceMountContribution>,
|
|
1077
|
+
context: DatabricksWorkspaceContext,
|
|
1078
|
+
filesCache?: FilesCacheExports,
|
|
1079
|
+
sourceKey?: string,
|
|
758
1080
|
): Promise<WorkspaceFilesystem> {
|
|
759
|
-
const {
|
|
1081
|
+
const { mounts } = await resolveContribution(context);
|
|
760
1082
|
const mountKeys = Object.keys(mounts);
|
|
761
1083
|
if (mountKeys.length === 0) {
|
|
762
|
-
logger.debug("filesystem:
|
|
1084
|
+
logger.debug("filesystem:empty", {
|
|
763
1085
|
hasRequestContext: Boolean(context.requestContext),
|
|
764
1086
|
});
|
|
765
|
-
return
|
|
1087
|
+
return filesystems(new MemoryFileSystem({ root: "/", readOnly: true }), {
|
|
1088
|
+
readOnly: true,
|
|
1089
|
+
});
|
|
766
1090
|
}
|
|
767
1091
|
logger.debug("filesystem:composite", { mountKeys });
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1092
|
+
const load = () => new MountedCompositeFilesystem({ mounts });
|
|
1093
|
+
if (!filesCache) return load();
|
|
1094
|
+
const scoped = await resolveFilesCacheScope(context);
|
|
1095
|
+
if (!scoped) return load();
|
|
1096
|
+
return filesCache.forFileSystem(scoped.scope, { paths: mountKeys, key: sourceKey }, load);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Expand skill roots into concrete skill directories and keep the first
|
|
1101
|
+
* same-named directory. Mastra still owns parsing, indexing, and refreshes,
|
|
1102
|
+
* while earlier configured roots retain precedence over later roots.
|
|
1103
|
+
*/
|
|
1104
|
+
async function resolveDistinctSkillPaths(
|
|
1105
|
+
filesystem: WorkspaceFilesystem,
|
|
1106
|
+
roots: readonly string[],
|
|
1107
|
+
): Promise<string[]> {
|
|
1108
|
+
const selected = new Map<string, string>();
|
|
1109
|
+
for (const root of roots) {
|
|
1110
|
+
try {
|
|
1111
|
+
const entries = await filesystem.readdir(root);
|
|
1112
|
+
if (entries.some((entry) => entry.type === "file" && entry.name === "SKILL.md")) {
|
|
1113
|
+
const name = posixPath.basename(root);
|
|
1114
|
+
if (!selected.has(name)) selected.set(name, root);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
for (const entry of entries) {
|
|
1118
|
+
if (entry.type !== "directory" || selected.has(entry.name)) continue;
|
|
1119
|
+
selected.set(entry.name, posixPath.join(root, entry.name));
|
|
1120
|
+
}
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
logger.debug("skill-root:skipped", {
|
|
1123
|
+
root,
|
|
1124
|
+
error: errorUtils.errorMessage(error),
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return [...selected.values()];
|
|
774
1129
|
}
|
|
775
1130
|
|
|
776
1131
|
/** Memoize one complete mount contribution for each request context. */
|
|
777
1132
|
function contributionResolver(
|
|
778
1133
|
resolvers: WorkspaceMountResolver[],
|
|
779
|
-
): (context:
|
|
1134
|
+
): (context: DatabricksWorkspaceContext) => Promise<WorkspaceMountContribution> {
|
|
780
1135
|
const scoped = new WeakMap<RequestContext, Promise<WorkspaceMountContribution>>();
|
|
781
1136
|
let unscoped: Promise<WorkspaceMountContribution> | undefined;
|
|
782
1137
|
return (context) => {
|
|
@@ -791,22 +1146,3 @@ function contributionResolver(
|
|
|
791
1146
|
return pending;
|
|
792
1147
|
};
|
|
793
1148
|
}
|
|
794
|
-
|
|
795
|
-
/** Resolve one semantic catalogue owner from a request's ordered skill roots. */
|
|
796
|
-
function buildSkillCatalogueResolver(
|
|
797
|
-
resolveContribution: (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution>,
|
|
798
|
-
ttlMs: number | undefined,
|
|
799
|
-
): SkillCatalogueResolver {
|
|
800
|
-
return async ({ requestContext }) => {
|
|
801
|
-
const contribution = await resolveContribution({ requestContext });
|
|
802
|
-
const user = requestContext?.get(MASTRA_USER_KEY) as User | undefined;
|
|
803
|
-
const client = user?.executionContext.client as WorkspaceClient | undefined;
|
|
804
|
-
const host = client ? (await client.config.getHost()).toString() : "local";
|
|
805
|
-
return workspaceSkillCatalogue({
|
|
806
|
-
host,
|
|
807
|
-
sources: contribution.skillSources ?? [],
|
|
808
|
-
ttlMs,
|
|
809
|
-
userKey: user?.id ?? "service",
|
|
810
|
-
});
|
|
811
|
-
};
|
|
812
|
-
}
|