@dbx-tools/appkit-mastra 0.9.79 → 0.9.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -48
- package/index.ts +4 -12
- package/lib/index.d.ts +4 -12
- package/lib/index.js +4 -9
- package/lib/src/agents.js +11 -23
- 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 -3
- package/lib/src/monty-sandbox.js +54 -21
- 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 +8 -4
- package/lib/src/skill-paths.js +12 -6
- package/lib/src/workspaces.d.ts +39 -33
- package/lib/src/workspaces.js +208 -168
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -38
- package/src/agents.ts +11 -24
- package/src/chat.ts +24 -0
- package/src/config.ts +15 -17
- package/src/filesystems.ts +43 -0
- package/src/monty-sandbox.ts +61 -22
- package/src/plugin.ts +2 -0
- package/src/remote-skills.ts +5 -15
- package/src/server.ts +33 -1
- package/src/skill-paths.ts +13 -5
- package/src/workspaces.ts +285 -210
- package/lib/src/monty-native.d.ts +0 -40
- package/lib/src/monty-native.js +0 -100
- 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/monty-native.ts +0 -149
- package/src/skill-cache.ts +0 -347
- package/src/skill-search.ts +0 -203
package/src/workspaces.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mastra workspace factory for Databricks Apps.
|
|
3
3
|
*
|
|
4
|
-
* Builds a
|
|
5
|
-
*
|
|
4
|
+
* Builds a {@link Workspace} with Databricks Sandbox command execution, an
|
|
5
|
+
* isolated `/tmp` scratch mount, and a {@link CompositeFilesystem} over the
|
|
6
|
+
* named skill folders
|
|
6
7
|
* resolved for that request. A skill folder maps a name to a location plus its
|
|
7
8
|
* readable / writable policy: a Databricks path mounted through the OBO client
|
|
8
9
|
* on {@link MASTRA_USER_KEY}, or any {@link WorkspaceFilesystem} a consuming
|
|
@@ -12,28 +13,35 @@
|
|
|
12
13
|
* filesystems and skill scan roots on top.
|
|
13
14
|
*
|
|
14
15
|
* Databricks mounts use `@dbx-tools/databricks` {@link DatabricksFileSystem}
|
|
15
|
-
* wrapped by {@link filesystems};
|
|
16
|
-
* {@link scratchFilesystem}.
|
|
16
|
+
* wrapped by {@link filesystems}; inaccessible roots are skipped independently.
|
|
17
17
|
*
|
|
18
18
|
* @module
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
|
-
import { ConfigurationError, createWorkspaceClient } from "@databricks/appkit";
|
|
22
|
+
import { ConfigurationError, createWorkspaceClient, getExecutionContext } from "@databricks/appkit";
|
|
23
23
|
import type { WorkspaceClient } from "@databricks/appkit";
|
|
24
|
+
import { pluginRegistry } from "@dbx-tools/appkit";
|
|
25
|
+
import {
|
|
26
|
+
filesCache as filesCachePlugin,
|
|
27
|
+
type FilesCacheExports,
|
|
28
|
+
type FilesCacheScope,
|
|
29
|
+
} from "@dbx-tools/appkit/files-cache";
|
|
24
30
|
import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
|
|
25
31
|
import { LocalFileSystem } from "@dbx-tools/fs";
|
|
26
32
|
import { errorUtils, log, object, stringUtils, token } from "@dbx-tools/shared-core";
|
|
27
|
-
import type
|
|
28
|
-
import { LRUCache } from "lru-cache";
|
|
33
|
+
import { fs as sharedFS, posixPath, type FileSystemCache } from "@dbx-tools/shared-fs";
|
|
29
34
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
30
35
|
import {
|
|
31
|
-
|
|
36
|
+
WORKSPACE_TOOLS,
|
|
32
37
|
Workspace,
|
|
33
38
|
type SkillsResolver,
|
|
34
39
|
type WorkspaceFilesystem,
|
|
35
40
|
type WorkspaceSandbox,
|
|
36
41
|
type WorkspaceSandboxResolver,
|
|
42
|
+
type ToolConfigWithArgsContext,
|
|
43
|
+
type WorkspaceToolConfig,
|
|
44
|
+
type WorkspaceToolsConfig,
|
|
37
45
|
} from "@mastra/core/workspace";
|
|
38
46
|
|
|
39
47
|
import {
|
|
@@ -43,28 +51,23 @@ import {
|
|
|
43
51
|
resolveUserKey,
|
|
44
52
|
type User,
|
|
45
53
|
} from "./config.ts";
|
|
46
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
filesystems,
|
|
56
|
+
MountedCompositeFilesystem,
|
|
57
|
+
scratchFilesystem,
|
|
58
|
+
type MastraFileSystemAdapterOptions,
|
|
59
|
+
} from "./filesystems.ts";
|
|
47
60
|
import { MontySandbox } from "./monty-sandbox.ts";
|
|
48
61
|
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";
|
|
62
|
+
import { ORGANIZATION_ASSISTANT_PATH, personalWorkspacePath } from "./skill-paths.ts";
|
|
56
63
|
|
|
57
64
|
/* ------------------------------ constants ------------------------------ */
|
|
58
65
|
|
|
59
66
|
/** OAuth scopes that gate Databricks workspace file mounts. */
|
|
60
67
|
const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] as const;
|
|
68
|
+
const SCRATCH_MOUNT = "/tmp";
|
|
61
69
|
|
|
62
70
|
const logger = log.logger("mastra/workspaces");
|
|
63
|
-
const MAX_SCOPED_WORKSPACE_FILESYSTEMS = 16;
|
|
64
|
-
const scopedWorkspaceFilesystems = new LRUCache<string, WorkspaceFilesystem>({
|
|
65
|
-
max: MAX_SCOPED_WORKSPACE_FILESYSTEMS,
|
|
66
|
-
});
|
|
67
|
-
const workspaceCatalogueResolvers = new WeakMap<Workspace, SkillCatalogueResolver>();
|
|
68
71
|
|
|
69
72
|
/* -------------------------------- types -------------------------------- */
|
|
70
73
|
|
|
@@ -88,37 +91,44 @@ export type SkillFolderValue<T> =
|
|
|
88
91
|
* itself. {@link filesystem} wins when both are set.
|
|
89
92
|
*/
|
|
90
93
|
export interface SkillFolderOptions {
|
|
91
|
-
/** Absolute Databricks workspace
|
|
94
|
+
/** Absolute Databricks workspace root mounted at its actual path. */
|
|
92
95
|
path?: SkillFolderValue<string>;
|
|
93
96
|
/** Ready-made mount, for locations the OBO client cannot reach. */
|
|
94
97
|
filesystem?: SkillFolderValue<WorkspaceFilesystem>;
|
|
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
127
|
/** Mount map plus optional Mastra skill scan roots for one resolver. */
|
|
111
128
|
export interface WorkspaceMountContribution {
|
|
112
129
|
mounts: Record<string, WorkspaceFilesystem>;
|
|
113
|
-
/** Ordered source roots included in catalogue discovery. */
|
|
114
|
-
skillSources?: SkillCatalogueSource[];
|
|
115
130
|
/** Paths within the composite namespace where `SKILL.md` files are scanned. */
|
|
116
131
|
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
132
|
}
|
|
123
133
|
|
|
124
134
|
/** Contributes filesystem mounts (and optional skill paths) for one request. */
|
|
@@ -127,7 +137,7 @@ export type WorkspaceMountResolver = (
|
|
|
127
137
|
) => WorkspaceMountContribution | Promise<WorkspaceMountContribution>;
|
|
128
138
|
|
|
129
139
|
/** Names carried by {@link DEFAULT_SKILL_FOLDERS}. */
|
|
130
|
-
export type DefaultSkillFolderName = "
|
|
140
|
+
export type DefaultSkillFolderName = "organization-skills" | "personal-skills";
|
|
131
141
|
|
|
132
142
|
/**
|
|
133
143
|
* Sandbox selection for {@link createWorkspace}. Databricks is the default;
|
|
@@ -165,11 +175,10 @@ export interface CreateWorkspaceOptions {
|
|
|
165
175
|
skills?: SkillsResolver;
|
|
166
176
|
/** Forwarded to Mastra when skill discovery is enabled. */
|
|
167
177
|
checkSkillFileMtime?: boolean;
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
workspaceSkillRefreshTtlMs?: number;
|
|
178
|
+
/** Additional native Mastra workspace tool configuration and overrides. */
|
|
179
|
+
tools?: WorkspaceToolsConfig;
|
|
180
|
+
/** AppKit plugin context used to discover optional sibling capabilities. */
|
|
181
|
+
pluginContext?: pluginRegistry.PluginContextLike;
|
|
173
182
|
/** Enable BM25 keyword search over indexed workspace content. */
|
|
174
183
|
bm25?: boolean;
|
|
175
184
|
/**
|
|
@@ -193,25 +202,32 @@ export interface CreateWorkspaceOptions {
|
|
|
193
202
|
/**
|
|
194
203
|
* The skill folders every workspace starts with.
|
|
195
204
|
*
|
|
196
|
-
* - `
|
|
197
|
-
*
|
|
198
|
-
* - `
|
|
199
|
-
*
|
|
200
|
-
* user email.
|
|
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.
|
|
201
209
|
*/
|
|
202
210
|
export const DEFAULT_SKILL_FOLDERS: Readonly<Record<DefaultSkillFolderName, SkillFolderOptions>> = {
|
|
203
|
-
"
|
|
204
|
-
|
|
211
|
+
"organization-skills": {
|
|
212
|
+
description: "Organization skills shared with everyone in this Databricks workspace.",
|
|
213
|
+
displayName: "Organization Skills",
|
|
214
|
+
path: ORGANIZATION_ASSISTANT_PATH,
|
|
205
215
|
readable: true,
|
|
206
|
-
|
|
216
|
+
skills: ["skills"],
|
|
217
|
+
writable: true,
|
|
218
|
+
createRoot: false,
|
|
207
219
|
},
|
|
208
|
-
"
|
|
220
|
+
"personal-skills": {
|
|
221
|
+
description: "Your Databricks workspace home directory for personal files and skills.",
|
|
222
|
+
displayName: "Home",
|
|
209
223
|
path: ({ requestContext }) => {
|
|
210
224
|
const email = resolveScopedEmail(requestContext);
|
|
211
|
-
return email ?
|
|
225
|
+
return email ? personalWorkspacePath(email) : undefined;
|
|
212
226
|
},
|
|
213
227
|
readable: true,
|
|
228
|
+
skills: [".assistant/skills"],
|
|
214
229
|
writable: true,
|
|
230
|
+
createRoot: false,
|
|
215
231
|
},
|
|
216
232
|
};
|
|
217
233
|
|
|
@@ -227,8 +243,11 @@ export const DEFAULT_SKILL_FOLDERS: Readonly<Record<DefaultSkillFolderName, Skil
|
|
|
227
243
|
* ```ts
|
|
228
244
|
* createWorkspace({
|
|
229
245
|
* skillFolders: {
|
|
230
|
-
* "
|
|
231
|
-
*
|
|
246
|
+
* "organization-skills": {
|
|
247
|
+
* path: "/Workspace/Shared/.assistant",
|
|
248
|
+
* skills: ["skills"],
|
|
249
|
+
* },
|
|
250
|
+
* "personal-skills": false,
|
|
232
251
|
* runbooks: { path: "/Workspace/Shared/runbooks/skills", writable: true },
|
|
233
252
|
* volume: { filesystem: myVolumeFilesystem },
|
|
234
253
|
* },
|
|
@@ -252,15 +271,41 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
252
271
|
const skillFolders = resolveSkillFolders(options);
|
|
253
272
|
const folderNames = Object.keys(skillFolders);
|
|
254
273
|
const extraSkillPaths = options.extraSkillPaths ?? [];
|
|
274
|
+
const filesCache = pluginRegistry.instance(options.pluginContext, filesCachePlugin)?.exports();
|
|
275
|
+
const retainFilesystemSource =
|
|
276
|
+
filesCache !== undefined &&
|
|
277
|
+
(options.mounts?.length ?? 0) === 0 &&
|
|
278
|
+
Object.values(skillFolders).every(
|
|
279
|
+
(folder) =>
|
|
280
|
+
typeof folder.filesystem !== "function" &&
|
|
281
|
+
!(typeof folder.path === "function" && folder.mount !== undefined),
|
|
282
|
+
);
|
|
255
283
|
const resolvers = buildMountResolvers(
|
|
256
284
|
skillFolders,
|
|
257
285
|
[...(options.mounts ?? []), ...localSkillMountResolvers(extraSkillPaths)],
|
|
286
|
+
filesCache,
|
|
258
287
|
);
|
|
259
288
|
const resolveContribution = contributionResolver(resolvers);
|
|
260
|
-
const
|
|
289
|
+
const resolveFilesystem = (context: WorkspaceMountContext) =>
|
|
290
|
+
resolveWorkspaceFilesystem(
|
|
291
|
+
resolveContribution,
|
|
292
|
+
context,
|
|
293
|
+
retainFilesystemSource ? filesCache : undefined,
|
|
294
|
+
);
|
|
295
|
+
const skills: SkillsResolver =
|
|
296
|
+
options.skills ??
|
|
297
|
+
(async ({ requestContext }) => {
|
|
298
|
+
const context = { requestContext };
|
|
299
|
+
const contribution = await resolveContribution(context);
|
|
300
|
+
return resolveDistinctSkillPaths(
|
|
301
|
+
await resolveFilesystem(context),
|
|
302
|
+
contribution.skillPaths ?? [],
|
|
303
|
+
);
|
|
304
|
+
});
|
|
261
305
|
const checkSkillFileMtime = options.checkSkillFileMtime ?? false;
|
|
262
306
|
const bm25 = options.bm25 !== false;
|
|
263
307
|
const sandbox = resolveWorkspaceSandbox(options.sandbox, id, name);
|
|
308
|
+
const tools = workspaceTools(options.tools);
|
|
264
309
|
logger.debug("workspace:create", {
|
|
265
310
|
id,
|
|
266
311
|
name,
|
|
@@ -269,8 +314,6 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
269
314
|
customMountResolvers: options.mounts?.length ?? 0,
|
|
270
315
|
customSkillsResolver: Boolean(options.skills),
|
|
271
316
|
checkSkillFileMtime,
|
|
272
|
-
workspaceSkillRefreshTtlMs:
|
|
273
|
-
options.workspaceSkillRefreshTtlMs ?? DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
|
|
274
317
|
bm25,
|
|
275
318
|
extraSkillPaths: extraSkillPaths.length,
|
|
276
319
|
sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
|
|
@@ -279,8 +322,8 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
279
322
|
const workspace = new Workspace({
|
|
280
323
|
id,
|
|
281
324
|
name,
|
|
282
|
-
filesystem:
|
|
283
|
-
...(skills
|
|
325
|
+
filesystem: resolveFilesystem,
|
|
326
|
+
...(resolvers.length > 0 || options.skills
|
|
284
327
|
? {
|
|
285
328
|
skills,
|
|
286
329
|
checkSkillFileMtime,
|
|
@@ -293,23 +336,11 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
293
336
|
}
|
|
294
337
|
: {}),
|
|
295
338
|
bm25,
|
|
339
|
+
tools,
|
|
296
340
|
});
|
|
297
|
-
if (!options.skills && resolvers.length > 0) {
|
|
298
|
-
workspaceCatalogueResolvers.set(
|
|
299
|
-
workspace,
|
|
300
|
-
buildSkillCatalogueResolver(resolveContribution, options.workspaceSkillRefreshTtlMs),
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
341
|
return workspace;
|
|
304
342
|
}
|
|
305
343
|
|
|
306
|
-
/** Return the dbx-tools catalogue resolver attached to one workspace. */
|
|
307
|
-
export function workspaceSkillCatalogueResolver(
|
|
308
|
-
workspace: Workspace | undefined,
|
|
309
|
-
): SkillCatalogueResolver | undefined {
|
|
310
|
-
return workspace ? workspaceCatalogueResolvers.get(workspace) : undefined;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
344
|
/**
|
|
314
345
|
* Merge the configured skill folders over {@link DEFAULT_SKILL_FOLDERS}.
|
|
315
346
|
*
|
|
@@ -333,6 +364,48 @@ export function resolveSkillFolders(
|
|
|
333
364
|
|
|
334
365
|
/* ---------------------------- private helpers ---------------------------- */
|
|
335
366
|
|
|
367
|
+
/** Require approval unless a filesystem mutation stays inside a user-owned writable root. */
|
|
368
|
+
function requireApprovalOutsideWritableRoots({
|
|
369
|
+
args,
|
|
370
|
+
requestContext,
|
|
371
|
+
}: ToolConfigWithArgsContext): boolean {
|
|
372
|
+
const inputPath = args.path;
|
|
373
|
+
if (typeof inputPath !== "string" || !posixPath.isAbsolute(inputPath)) return true;
|
|
374
|
+
const normalized = posixPath.normalize(inputPath);
|
|
375
|
+
if (!normalized.ok) return true;
|
|
376
|
+
if (posixPath.isWithinRoot(SCRATCH_MOUNT, normalized.path)) return false;
|
|
377
|
+
const emailValue = requestContext[MASTRA_USER_EMAIL_KEY];
|
|
378
|
+
const email = typeof emailValue === "string" ? stringUtils.trimToNull(emailValue) : undefined;
|
|
379
|
+
if (!email) return true;
|
|
380
|
+
return !posixPath.isWithinRoot(personalWorkspacePath(email), normalized.path);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function workspaceTools(configured: WorkspaceToolsConfig | undefined): WorkspaceToolsConfig {
|
|
384
|
+
const requireApproval: WorkspaceToolConfig =
|
|
385
|
+
configured?.requireApproval === undefined
|
|
386
|
+
? { requireApproval: requireApprovalOutsideWritableRoots }
|
|
387
|
+
: {};
|
|
388
|
+
const defaults = {
|
|
389
|
+
[WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE]: requireApproval,
|
|
390
|
+
[WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE]: requireApproval,
|
|
391
|
+
[WORKSPACE_TOOLS.FILESYSTEM.AST_EDIT]: requireApproval,
|
|
392
|
+
[WORKSPACE_TOOLS.FILESYSTEM.DELETE]: requireApproval,
|
|
393
|
+
[WORKSPACE_TOOLS.FILESYSTEM.MKDIR]: requireApproval,
|
|
394
|
+
} satisfies WorkspaceToolsConfig;
|
|
395
|
+
const configuredTools = configured as
|
|
396
|
+
Readonly<Record<string, WorkspaceToolConfig | undefined>> | undefined;
|
|
397
|
+
const mergedTools = Object.fromEntries(
|
|
398
|
+
Object.entries(defaults).map(([name, defaultConfig]) => [
|
|
399
|
+
name,
|
|
400
|
+
{ ...defaultConfig, ...configuredTools?.[name] },
|
|
401
|
+
]),
|
|
402
|
+
);
|
|
403
|
+
return {
|
|
404
|
+
...defaults,
|
|
405
|
+
...configured,
|
|
406
|
+
...mergedTools,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
336
409
|
function resolveWorkspaceSandbox(
|
|
337
410
|
selection: WorkspaceSandboxSelection | undefined,
|
|
338
411
|
workspaceId: string,
|
|
@@ -410,6 +483,48 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
410
483
|
);
|
|
411
484
|
}
|
|
412
485
|
|
|
486
|
+
/** Resolve the request client and authorization-safe files-cache scope. */
|
|
487
|
+
async function resolveFilesCacheScope(context: WorkspaceMountContext): Promise<
|
|
488
|
+
| {
|
|
489
|
+
client: WorkspaceClient;
|
|
490
|
+
scope: FilesCacheScope;
|
|
491
|
+
}
|
|
492
|
+
| undefined
|
|
493
|
+
> {
|
|
494
|
+
const requestContext = context.requestContext;
|
|
495
|
+
const user = requestContext?.get(MASTRA_USER_KEY) as User | undefined;
|
|
496
|
+
const client = user?.executionContext.client as WorkspaceClient | undefined;
|
|
497
|
+
if (!requestContext || !user || !client) return undefined;
|
|
498
|
+
return {
|
|
499
|
+
client,
|
|
500
|
+
scope: {
|
|
501
|
+
host: (await client.config.getHost()).toString(),
|
|
502
|
+
userKey: resolveUserKey(requestContext),
|
|
503
|
+
...(process.env.DATABRICKS_WORKSPACE_ID?.trim()
|
|
504
|
+
? { workspaceId: process.env.DATABRICKS_WORKSPACE_ID.trim() }
|
|
505
|
+
: {}),
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Join one relative skill root to its actual composite mount path. */
|
|
511
|
+
function mountedSkillPath(mount: string, skillRoot: string): string {
|
|
512
|
+
if (posixPath.isAbsolute(skillRoot)) {
|
|
513
|
+
throw new TypeError(`Skill path must be relative to its mount: ${skillRoot}`);
|
|
514
|
+
}
|
|
515
|
+
const normalized = posixPath.normalize(skillRoot);
|
|
516
|
+
if (!normalized.ok) {
|
|
517
|
+
throw new TypeError(`Skill path escapes its mount: ${skillRoot}`);
|
|
518
|
+
}
|
|
519
|
+
return posixPath.join(posixPath.normalizeRoot(mount), normalized.path.slice(1));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Return whether a Databricks path belongs to the writable Workspace namespace. */
|
|
523
|
+
function isWorkspaceRoot(root: string): boolean {
|
|
524
|
+
const normalized = posixPath.normalizeRoot(root);
|
|
525
|
+
return normalized === "/Workspace" || normalized.startsWith("/Workspace/");
|
|
526
|
+
}
|
|
527
|
+
|
|
413
528
|
/**
|
|
414
529
|
* Mount resolver for the named skill folders.
|
|
415
530
|
*
|
|
@@ -419,11 +534,10 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
419
534
|
async function resolveSkillFolderMounts(
|
|
420
535
|
skillFolders: Record<string, SkillFolderOptions>,
|
|
421
536
|
context: WorkspaceMountContext,
|
|
537
|
+
filesCache: FilesCacheExports | undefined,
|
|
422
538
|
): Promise<WorkspaceMountContribution> {
|
|
423
539
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
424
|
-
const skillSources: SkillCatalogueSource[] = [];
|
|
425
540
|
const skillPaths: string[] = [];
|
|
426
|
-
const cacheKeys: string[] = [];
|
|
427
541
|
const requestContext = context.requestContext;
|
|
428
542
|
|
|
429
543
|
if (!requestContext || !shouldMountSkillFolders(requestContext)) {
|
|
@@ -435,24 +549,21 @@ async function resolveSkillFolderMounts(
|
|
|
435
549
|
return { mounts, skillPaths };
|
|
436
550
|
}
|
|
437
551
|
|
|
438
|
-
const
|
|
439
|
-
const client =
|
|
440
|
-
const
|
|
441
|
-
|
|
552
|
+
const scoped = await resolveFilesCacheScope(context);
|
|
553
|
+
const client = scoped?.client;
|
|
554
|
+
const fileSystemCache =
|
|
555
|
+
filesCache && scoped ? await filesCache.forScope(scoped.scope) : undefined;
|
|
442
556
|
|
|
443
557
|
for (const [name, folder] of Object.entries(skillFolders)) {
|
|
444
558
|
const resolved = await resolveSkillFolderFilesystem(name, folder, context, {
|
|
445
559
|
client,
|
|
446
|
-
|
|
447
|
-
userKey,
|
|
560
|
+
fileSystemCache,
|
|
448
561
|
});
|
|
449
562
|
if (!resolved) continue;
|
|
450
|
-
const mount = folder.mount ?? `/${name}`;
|
|
563
|
+
const mount = folder.mount ?? resolved.root ?? `/${name}`;
|
|
451
564
|
mounts[mount] = resolved.filesystem;
|
|
452
|
-
if (resolved.cacheKey) cacheKeys.push(resolved.cacheKey);
|
|
453
565
|
if (folder.readable !== false) {
|
|
454
|
-
skillPaths.push(mount);
|
|
455
|
-
skillSources.push({ ...resolved.skillSource, id: object.toStableKey({ mount, source: resolved.skillSource.id }) });
|
|
566
|
+
skillPaths.push(...(folder.skills ?? ["."]).map((path) => mountedSkillPath(mount, path)));
|
|
456
567
|
}
|
|
457
568
|
}
|
|
458
569
|
|
|
@@ -463,11 +574,7 @@ async function resolveSkillFolderMounts(
|
|
|
463
574
|
|
|
464
575
|
return {
|
|
465
576
|
mounts,
|
|
466
|
-
skillSources,
|
|
467
577
|
skillPaths,
|
|
468
|
-
...(cacheKeys.length === Object.keys(mounts).length && cacheKeys.length > 0
|
|
469
|
-
? { cacheKey: object.toStableKey(cacheKeys.sort()) }
|
|
470
|
-
: {}),
|
|
471
578
|
};
|
|
472
579
|
}
|
|
473
580
|
|
|
@@ -481,14 +588,12 @@ async function resolveSkillFolderFilesystem(
|
|
|
481
588
|
context: WorkspaceMountContext,
|
|
482
589
|
cache: {
|
|
483
590
|
client: WorkspaceClient | undefined;
|
|
484
|
-
|
|
485
|
-
userKey: string | undefined;
|
|
591
|
+
fileSystemCache: FileSystemCache | undefined;
|
|
486
592
|
},
|
|
487
593
|
): Promise<
|
|
488
594
|
| {
|
|
489
|
-
cacheKey?: string;
|
|
490
595
|
filesystem: WorkspaceFilesystem;
|
|
491
|
-
|
|
596
|
+
root?: string;
|
|
492
597
|
}
|
|
493
598
|
| undefined
|
|
494
599
|
> {
|
|
@@ -497,19 +602,11 @@ async function resolveSkillFolderFilesystem(
|
|
|
497
602
|
return filesystem
|
|
498
603
|
? {
|
|
499
604
|
filesystem,
|
|
500
|
-
skillSource: mastraSkillSource(
|
|
501
|
-
object.toStableKey({
|
|
502
|
-
id: filesystem.id,
|
|
503
|
-
name,
|
|
504
|
-
provider: filesystem.provider,
|
|
505
|
-
}),
|
|
506
|
-
filesystem,
|
|
507
|
-
),
|
|
508
605
|
}
|
|
509
606
|
: undefined;
|
|
510
607
|
}
|
|
511
608
|
// A path mount needs the request's OBO client to reach the workspace.
|
|
512
|
-
if (folder.path === undefined || !cache.client
|
|
609
|
+
if (folder.path === undefined || !cache.client) {
|
|
513
610
|
logger.debug("skill-folder:skipped", {
|
|
514
611
|
name,
|
|
515
612
|
reason: folder.path === undefined ? "no-location" : "missing-obo-client",
|
|
@@ -518,10 +615,20 @@ async function resolveSkillFolderFilesystem(
|
|
|
518
615
|
}
|
|
519
616
|
const root = stringUtils.trimToNull(await resolveSkillFolderValue(folder.path, context));
|
|
520
617
|
if (!root) return undefined;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
618
|
+
const writable = folder.writable ?? isWorkspaceRoot(root);
|
|
619
|
+
const resolved = await databricksFilesystem(
|
|
620
|
+
cache.client,
|
|
621
|
+
root,
|
|
622
|
+
!writable,
|
|
623
|
+
folder.createRoot ?? folder.writable === true,
|
|
624
|
+
cache.fileSystemCache,
|
|
625
|
+
folder.readable === false ? [] : (folder.skills ?? ["."]),
|
|
626
|
+
{
|
|
627
|
+
...(folder.description ? { description: folder.description } : {}),
|
|
628
|
+
...(folder.displayName ? { displayName: folder.displayName } : {}),
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
return resolved ? { ...resolved, root } : undefined;
|
|
525
632
|
}
|
|
526
633
|
|
|
527
634
|
/** Read a {@link SkillFolderValue}, calling it when it is a per-request resolver. */
|
|
@@ -557,11 +664,12 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
|
|
|
557
664
|
function buildMountResolvers(
|
|
558
665
|
skillFolders: Record<string, SkillFolderOptions>,
|
|
559
666
|
mounts: WorkspaceMountResolver[] | undefined,
|
|
667
|
+
filesCache: FilesCacheExports | undefined,
|
|
560
668
|
): WorkspaceMountResolver[] {
|
|
561
|
-
const resolvers: WorkspaceMountResolver[] = [];
|
|
669
|
+
const resolvers: WorkspaceMountResolver[] = [scratchMountResolver()];
|
|
562
670
|
const folderCount = Object.keys(skillFolders).length;
|
|
563
671
|
if (folderCount > 0) {
|
|
564
|
-
resolvers.push((context) => resolveSkillFolderMounts(skillFolders, context));
|
|
672
|
+
resolvers.push((context) => resolveSkillFolderMounts(skillFolders, context, filesCache));
|
|
565
673
|
}
|
|
566
674
|
if (mounts?.length) {
|
|
567
675
|
resolvers.push(...mounts);
|
|
@@ -574,10 +682,17 @@ function buildMountResolvers(
|
|
|
574
682
|
return resolvers;
|
|
575
683
|
}
|
|
576
684
|
|
|
685
|
+
/** Contribute one isolated local scratch filesystem at `/tmp`. */
|
|
686
|
+
function scratchMountResolver(): WorkspaceMountResolver {
|
|
687
|
+
return () => ({
|
|
688
|
+
mounts: { [SCRATCH_MOUNT]: scratchFilesystem() },
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
577
692
|
/** Mount startup-provisioned local skill roots into Mastra's workspace filesystem. */
|
|
578
693
|
function localSkillMountResolvers(paths: readonly string[]): WorkspaceMountResolver[] {
|
|
579
|
-
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].map((root
|
|
580
|
-
const mount =
|
|
694
|
+
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].map((root) => {
|
|
695
|
+
const mount = posixPath.normalizeRoot(root);
|
|
581
696
|
const source = new LocalFileSystem({
|
|
582
697
|
root,
|
|
583
698
|
readOnly: true,
|
|
@@ -586,51 +701,11 @@ function localSkillMountResolvers(paths: readonly string[]): WorkspaceMountResol
|
|
|
586
701
|
const filesystem = filesystems(source, { readOnly: true });
|
|
587
702
|
return () => ({
|
|
588
703
|
mounts: { [mount]: filesystem },
|
|
589
|
-
skillSources: [sharedSkillSource(object.toStableKey({ mount, root }), source)],
|
|
590
704
|
skillPaths: [mount],
|
|
591
|
-
cacheKey: object.toStableKey({ mount, root }),
|
|
592
705
|
});
|
|
593
706
|
});
|
|
594
707
|
}
|
|
595
708
|
|
|
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
709
|
/**
|
|
635
710
|
* Gate skill-folder mounts on the request's token.
|
|
636
711
|
*
|
|
@@ -648,47 +723,48 @@ function resolveScopedEmail(requestContext: RequestContext | undefined): string
|
|
|
648
723
|
}
|
|
649
724
|
|
|
650
725
|
/**
|
|
651
|
-
* Wrap a {@link DatabricksFileSystem} as a Mastra filesystem.
|
|
652
|
-
*
|
|
653
|
-
*
|
|
726
|
+
* Wrap a {@link DatabricksFileSystem} as a Mastra filesystem. Missing or
|
|
727
|
+
* inaccessible roots return `undefined` so the independent `/tmp` mount and
|
|
728
|
+
* any other accessible roots remain usable.
|
|
654
729
|
*/
|
|
655
730
|
async function databricksFilesystem(
|
|
656
731
|
client: WorkspaceClient,
|
|
657
732
|
root: string,
|
|
658
733
|
readOnly: boolean = true,
|
|
659
|
-
|
|
734
|
+
createRoot: boolean = !readOnly,
|
|
735
|
+
fileSystemCache?: FileSystemCache,
|
|
736
|
+
skillPaths: readonly string[] = [],
|
|
737
|
+
adapterOptions: Pick<MastraFileSystemAdapterOptions, "description" | "displayName"> = {},
|
|
660
738
|
): Promise<
|
|
661
|
-
{
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
skillSource: SkillCatalogueSource;
|
|
665
|
-
}
|
|
739
|
+
| {
|
|
740
|
+
filesystem: WorkspaceFilesystem;
|
|
741
|
+
}
|
|
666
742
|
| undefined
|
|
667
743
|
> {
|
|
744
|
+
const initialClient = workspaceClient.toLegacyWorkspaceClient(client);
|
|
668
745
|
const fs = new DatabricksFileSystem({
|
|
669
|
-
client:
|
|
746
|
+
client: () => {
|
|
747
|
+
try {
|
|
748
|
+
return workspaceClient.toLegacyWorkspaceClient(getExecutionContext().client);
|
|
749
|
+
} catch {
|
|
750
|
+
return initialClient;
|
|
751
|
+
}
|
|
752
|
+
},
|
|
670
753
|
root,
|
|
671
754
|
readOnly,
|
|
672
|
-
createRoot
|
|
755
|
+
createRoot,
|
|
673
756
|
});
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
userKey: cache.userKey,
|
|
679
|
-
});
|
|
680
|
-
const filesystem = filesystems(fs, { readOnly });
|
|
757
|
+
const source = fileSystemCache
|
|
758
|
+
? sharedFS.cache(fs, fileSystemCache, { readFilePaths: skillPaths })
|
|
759
|
+
: fs;
|
|
760
|
+
const filesystem = filesystems(source, { ...adapterOptions, readOnly });
|
|
681
761
|
try {
|
|
682
762
|
await filesystem.init();
|
|
683
763
|
if (await filesystem.exists(".")) {
|
|
684
|
-
return {
|
|
685
|
-
cacheKey,
|
|
686
|
-
filesystem,
|
|
687
|
-
skillSource: sharedSkillSource(cacheKey, fs),
|
|
688
|
-
};
|
|
764
|
+
return { filesystem };
|
|
689
765
|
}
|
|
690
766
|
} catch (err) {
|
|
691
|
-
logger.debug("databricks-mount:
|
|
767
|
+
logger.debug("databricks-mount:skipped", {
|
|
692
768
|
root,
|
|
693
769
|
readOnly,
|
|
694
770
|
error: errorUtils.errorMessage(err),
|
|
@@ -706,10 +782,7 @@ async function resolveWorkspaceContribution(
|
|
|
706
782
|
context: WorkspaceMountContext,
|
|
707
783
|
): Promise<WorkspaceMountContribution> {
|
|
708
784
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
709
|
-
const skillSources: SkillCatalogueSource[] = [];
|
|
710
785
|
const skillPaths: string[] = [];
|
|
711
|
-
const cacheKeys: string[] = [];
|
|
712
|
-
let cacheable = true;
|
|
713
786
|
|
|
714
787
|
for (const [index, resolver] of resolvers.entries()) {
|
|
715
788
|
const contribution = await resolver(context);
|
|
@@ -719,16 +792,9 @@ async function resolveWorkspaceContribution(
|
|
|
719
792
|
skillPaths: contribution.skillPaths ?? [],
|
|
720
793
|
});
|
|
721
794
|
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
795
|
if (contribution.skillPaths?.length) {
|
|
727
796
|
skillPaths.push(...contribution.skillPaths);
|
|
728
797
|
}
|
|
729
|
-
if (contribution.skillSources?.length) {
|
|
730
|
-
skillSources.push(...contribution.skillSources);
|
|
731
|
-
}
|
|
732
798
|
}
|
|
733
799
|
|
|
734
800
|
logger.debug("mounts:merged", {
|
|
@@ -738,11 +804,7 @@ async function resolveWorkspaceContribution(
|
|
|
738
804
|
|
|
739
805
|
return {
|
|
740
806
|
mounts,
|
|
741
|
-
skillSources,
|
|
742
807
|
skillPaths,
|
|
743
|
-
...(cacheable && cacheKeys.length > 0
|
|
744
|
-
? { cacheKey: object.toStableKey({ mounts: Object.keys(mounts), sources: cacheKeys }) }
|
|
745
|
-
: {}),
|
|
746
808
|
};
|
|
747
809
|
}
|
|
748
810
|
|
|
@@ -755,8 +817,9 @@ async function resolveWorkspaceContribution(
|
|
|
755
817
|
async function resolveWorkspaceFilesystem(
|
|
756
818
|
resolveContribution: (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution>,
|
|
757
819
|
context: WorkspaceMountContext,
|
|
820
|
+
filesCache?: FilesCacheExports,
|
|
758
821
|
): Promise<WorkspaceFilesystem> {
|
|
759
|
-
const {
|
|
822
|
+
const { mounts } = await resolveContribution(context);
|
|
760
823
|
const mountKeys = Object.keys(mounts);
|
|
761
824
|
if (mountKeys.length === 0) {
|
|
762
825
|
logger.debug("filesystem:scratch", {
|
|
@@ -765,12 +828,43 @@ async function resolveWorkspaceFilesystem(
|
|
|
765
828
|
return scratchFilesystem();
|
|
766
829
|
}
|
|
767
830
|
logger.debug("filesystem:composite", { mountKeys });
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
831
|
+
const load = () => new MountedCompositeFilesystem({ mounts });
|
|
832
|
+
if (!filesCache) return load();
|
|
833
|
+
const scoped = await resolveFilesCacheScope(context);
|
|
834
|
+
if (!scoped) return load();
|
|
835
|
+
return filesCache.forFileSystem(scoped.scope, { paths: mountKeys }, load);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Expand skill roots into concrete skill directories and keep the first
|
|
840
|
+
* same-named directory. Mastra still owns parsing, indexing, and refreshes,
|
|
841
|
+
* while organization roots retain precedence over personal roots.
|
|
842
|
+
*/
|
|
843
|
+
async function resolveDistinctSkillPaths(
|
|
844
|
+
filesystem: WorkspaceFilesystem,
|
|
845
|
+
roots: readonly string[],
|
|
846
|
+
): Promise<string[]> {
|
|
847
|
+
const selected = new Map<string, string>();
|
|
848
|
+
for (const root of roots) {
|
|
849
|
+
try {
|
|
850
|
+
const entries = await filesystem.readdir(root);
|
|
851
|
+
if (entries.some((entry) => entry.type === "file" && entry.name === "SKILL.md")) {
|
|
852
|
+
const name = posixPath.basename(root);
|
|
853
|
+
if (!selected.has(name)) selected.set(name, root);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
for (const entry of entries) {
|
|
857
|
+
if (entry.type !== "directory" || selected.has(entry.name)) continue;
|
|
858
|
+
selected.set(entry.name, posixPath.join(root, entry.name));
|
|
859
|
+
}
|
|
860
|
+
} catch (error) {
|
|
861
|
+
logger.debug("skill-root:skipped", {
|
|
862
|
+
root,
|
|
863
|
+
error: errorUtils.errorMessage(error),
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return [...selected.values()];
|
|
774
868
|
}
|
|
775
869
|
|
|
776
870
|
/** Memoize one complete mount contribution for each request context. */
|
|
@@ -791,22 +885,3 @@ function contributionResolver(
|
|
|
791
885
|
return pending;
|
|
792
886
|
};
|
|
793
887
|
}
|
|
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
|
-
}
|