@dbx-tools/appkit-lakebox 0.0.0-stage → 0.9.131
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/LICENSE +202 -0
- package/README.md +104 -2
- package/index.ts +32 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.js +26 -0
- package/lib/src/_package.d.ts +2 -0
- package/lib/src/_package.js +11 -0
- package/lib/src/_resource-registry.d.ts +41 -0
- package/lib/src/_resource-registry.js +128 -0
- package/lib/src/_service-snapshots.d.ts +55 -0
- package/lib/src/_service-snapshots.js +65 -0
- package/lib/src/config.d.ts +40 -0
- package/lib/src/config.js +86 -0
- package/lib/src/desktop.d.ts +6 -0
- package/lib/src/desktop.js +88 -0
- package/lib/src/metrics.d.ts +59 -0
- package/lib/src/metrics.js +151 -0
- package/lib/src/plugin.d.ts +259 -0
- package/lib/src/plugin.js +329 -0
- package/lib/src/routes.d.ts +75 -0
- package/lib/src/routes.js +441 -0
- package/lib/src/runtime.d.ts +154 -0
- package/lib/src/runtime.js +760 -0
- package/lib/src/services.d.ts +101 -0
- package/lib/src/services.js +188 -0
- package/lib/src/skills.d.ts +5 -0
- package/lib/src/skills.js +21 -0
- package/lib/src/user.d.ts +46 -0
- package/lib/src/user.js +76 -0
- package/lib/src/workspace.d.ts +43 -0
- package/lib/src/workspace.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +167 -4
- package/scripts/desktop-prerequisites.sh +66 -0
- package/scripts/desktop-session.sh +307 -0
- package/scripts/lakebox-admin.sh +234 -0
- package/scripts/lakebox-service.py +449 -0
- package/skills/html-page/SKILL.md +40 -0
- package/skills/jupyter-lab/SKILL.md +32 -0
- package/skills/lakebox-showcase/SKILL.md +66 -0
- package/skills/network-speed-test/SKILL.md +29 -0
- package/skills/realtime-whiteboard/SKILL.md +28 -0
- package/src/_package.ts +11 -0
- package/src/_resource-registry.ts +160 -0
- package/src/_service-snapshots.ts +84 -0
- package/src/config.ts +128 -0
- package/src/desktop.ts +97 -0
- package/src/metrics.ts +178 -0
- package/src/plugin.ts +446 -0
- package/src/routes.ts +524 -0
- package/src/runtime.ts +952 -0
- package/src/services.ts +239 -0
- package/src/skills.ts +23 -0
- package/src/user.ts +88 -0
- package/src/workspace.ts +185 -0
package/src/services.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLakeboxServiceRequestSchema,
|
|
3
|
+
lakeboxServiceKindSchema,
|
|
4
|
+
lakeboxServiceStateSchema,
|
|
5
|
+
type CreateLakeboxServiceRequest,
|
|
6
|
+
type LakeboxBuiltInServiceName,
|
|
7
|
+
type LakeboxService,
|
|
8
|
+
type LakeboxUser,
|
|
9
|
+
} from "@dbx-tools/shared-lakebox";
|
|
10
|
+
import { lakeboxServicePath } from "@dbx-tools/shared-lakebox/client";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { desktopEnvironment, desktopHtmlHead, desktopOpenUrl } from "./desktop.ts";
|
|
13
|
+
import { LAKEBOX_RUNTIME_ROOT, lakeboxServicePort } from "./user.ts";
|
|
14
|
+
|
|
15
|
+
const TTYD_CLIENT_OPTIONS = [
|
|
16
|
+
"term=xterm-256color",
|
|
17
|
+
"rendererType=webgl",
|
|
18
|
+
"cursorStyle=bar",
|
|
19
|
+
"copyOnSelect=true",
|
|
20
|
+
"rightClickSelectsWord=true",
|
|
21
|
+
"disableLeaveAlert=true",
|
|
22
|
+
"disableResizeOverlay=true",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const LakeboxControllerDefinitionSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
name: z.string().describe("Service name."),
|
|
28
|
+
kind: lakeboxServiceKindSchema,
|
|
29
|
+
image: z.string().optional().describe("OCI image for image services."),
|
|
30
|
+
pull: z.enum(["missing", "always"]).default("missing").describe("Image pull policy."),
|
|
31
|
+
command: z
|
|
32
|
+
.array(z.string())
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Argv; omitted to use the image entrypoint and command."),
|
|
35
|
+
env: z
|
|
36
|
+
.record(z.string(), z.string())
|
|
37
|
+
.describe("Container environment, including injected values."),
|
|
38
|
+
workingDirectory: z.string().optional().describe("Container working directory."),
|
|
39
|
+
mounts: z
|
|
40
|
+
.array(
|
|
41
|
+
z
|
|
42
|
+
.object({
|
|
43
|
+
source: z
|
|
44
|
+
.string()
|
|
45
|
+
.startsWith("/")
|
|
46
|
+
.describe("Absolute host path beneath the user's home."),
|
|
47
|
+
target: z.string().startsWith("/").describe("Container path."),
|
|
48
|
+
readOnly: z.boolean().describe("Read-only bind."),
|
|
49
|
+
})
|
|
50
|
+
.strict(),
|
|
51
|
+
)
|
|
52
|
+
.describe("Bind mounts."),
|
|
53
|
+
port: z.number().int().describe("Derived loopback port."),
|
|
54
|
+
readiness: z.enum(["tcp", "none"]).describe("Readiness check."),
|
|
55
|
+
readySeconds: z.number().int().positive().describe("Maximum readiness wait."),
|
|
56
|
+
autostart: z.boolean().describe("Whether the service restarts when the Lakebox starts."),
|
|
57
|
+
route: z.boolean().describe("Whether AppKit routes the service."),
|
|
58
|
+
stripPrefix: z.boolean().describe("Whether the route prefix is removed before forwarding."),
|
|
59
|
+
openPath: z.string().optional().describe("Entry path appended to the route."),
|
|
60
|
+
})
|
|
61
|
+
.strict()
|
|
62
|
+
.describe("Service definition persisted in the user's Lakebox home and read by the controller.");
|
|
63
|
+
/** Service definition persisted in the user's Lakebox home. */
|
|
64
|
+
export type LakeboxControllerDefinition = z.output<typeof LakeboxControllerDefinitionSchema>;
|
|
65
|
+
|
|
66
|
+
export const LakeboxControllerServiceSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
name: z.string(),
|
|
69
|
+
state: lakeboxServiceStateSchema,
|
|
70
|
+
definition: LakeboxControllerDefinitionSchema,
|
|
71
|
+
error: z.string().optional(),
|
|
72
|
+
log: z.string().optional(),
|
|
73
|
+
})
|
|
74
|
+
.passthrough()
|
|
75
|
+
.describe("One service as reported by the remote controller.");
|
|
76
|
+
/** One service as reported by the remote controller. */
|
|
77
|
+
export type LakeboxControllerService = z.infer<typeof LakeboxControllerServiceSchema>;
|
|
78
|
+
|
|
79
|
+
/** Routing behavior derived from a saved definition. */
|
|
80
|
+
export interface LakeboxServiceRoute {
|
|
81
|
+
port: number;
|
|
82
|
+
stripPrefix: boolean;
|
|
83
|
+
redirectToSlash: boolean;
|
|
84
|
+
htmlHead?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Builds the package-owned definition for a built-in service. */
|
|
88
|
+
export function builtInDefinition(
|
|
89
|
+
name: LakeboxBuiltInServiceName,
|
|
90
|
+
user: LakeboxUser,
|
|
91
|
+
): LakeboxControllerDefinition {
|
|
92
|
+
const port = lakeboxServicePort(user.hash, name);
|
|
93
|
+
const common = {
|
|
94
|
+
name,
|
|
95
|
+
kind: name,
|
|
96
|
+
pull: "missing" as const,
|
|
97
|
+
mounts: [],
|
|
98
|
+
port,
|
|
99
|
+
readiness: "tcp" as const,
|
|
100
|
+
autostart: false,
|
|
101
|
+
route: true,
|
|
102
|
+
stripPrefix: true,
|
|
103
|
+
};
|
|
104
|
+
if (name === "terminal") {
|
|
105
|
+
return {
|
|
106
|
+
...common,
|
|
107
|
+
readySeconds: 60,
|
|
108
|
+
env: injectedEnvironment(user, name, port),
|
|
109
|
+
command: [
|
|
110
|
+
`${LAKEBOX_RUNTIME_ROOT}/bin/ttyd`,
|
|
111
|
+
"--interface",
|
|
112
|
+
"127.0.0.1",
|
|
113
|
+
"--port",
|
|
114
|
+
String(port),
|
|
115
|
+
"--writable",
|
|
116
|
+
...TTYD_CLIENT_OPTIONS.flatMap((option) => ["--client-option", option]),
|
|
117
|
+
"/bin/bash",
|
|
118
|
+
"-l",
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...common,
|
|
124
|
+
readySeconds: 180,
|
|
125
|
+
env: { ...desktopEnvironment(), ...injectedEnvironment(user, name, port) },
|
|
126
|
+
command: ["/bin/sh", `${LAKEBOX_RUNTIME_ROOT}/bin/desktop-session`],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validates an agent or REST service request and resolves it to a persisted
|
|
132
|
+
* definition with injected variables and home-relative mounts.
|
|
133
|
+
*/
|
|
134
|
+
export function imageDefinition(
|
|
135
|
+
input: CreateLakeboxServiceRequest,
|
|
136
|
+
user: LakeboxUser,
|
|
137
|
+
): LakeboxControllerDefinition {
|
|
138
|
+
const request = createLakeboxServiceRequestSchema.parse(input);
|
|
139
|
+
const port = lakeboxServicePort(user.hash, request.name);
|
|
140
|
+
return {
|
|
141
|
+
name: request.name,
|
|
142
|
+
kind: "image",
|
|
143
|
+
image: request.image,
|
|
144
|
+
pull: request.pull,
|
|
145
|
+
...(request.command ? { command: request.command } : {}),
|
|
146
|
+
env: { ...request.env, ...injectedEnvironment(user, request.name, port, request.route) },
|
|
147
|
+
...(request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}),
|
|
148
|
+
mounts: request.mounts.map((mount) => ({
|
|
149
|
+
source: `${user.home}/${mount.source.replace(/^\.\/+|\/+$/g, "")}`,
|
|
150
|
+
target: mount.target,
|
|
151
|
+
readOnly: mount.readOnly,
|
|
152
|
+
})),
|
|
153
|
+
port,
|
|
154
|
+
readiness: request.route ? "tcp" : "none",
|
|
155
|
+
readySeconds: 300,
|
|
156
|
+
autostart: true,
|
|
157
|
+
route: request.route,
|
|
158
|
+
stripPrefix: request.stripPrefix,
|
|
159
|
+
...(request.openPath ? { openPath: request.openPath.replace(/^\/+/, "") } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Returns route behavior for a saved definition. */
|
|
164
|
+
export function serviceRoute(definition: LakeboxControllerDefinition): LakeboxServiceRoute {
|
|
165
|
+
return {
|
|
166
|
+
port: definition.port,
|
|
167
|
+
stripPrefix: definition.stripPrefix,
|
|
168
|
+
redirectToSlash: true,
|
|
169
|
+
...(definition.kind === "desktop" ? { htmlHead: desktopHtmlHead } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Maps a controller report to the shared service shape. */
|
|
174
|
+
export function toLakeboxService(
|
|
175
|
+
service: LakeboxControllerService,
|
|
176
|
+
user: LakeboxUser,
|
|
177
|
+
routePrefix: string,
|
|
178
|
+
): LakeboxService {
|
|
179
|
+
const { definition } = service;
|
|
180
|
+
const path = lakeboxServicePath(routePrefix, user.hash, definition.name);
|
|
181
|
+
return {
|
|
182
|
+
name: definition.name,
|
|
183
|
+
kind: definition.kind,
|
|
184
|
+
...(definition.image ? { image: definition.image } : {}),
|
|
185
|
+
...(definition.openPath ? { openPath: definition.openPath } : {}),
|
|
186
|
+
state: service.state,
|
|
187
|
+
port: definition.port,
|
|
188
|
+
route: definition.route,
|
|
189
|
+
path,
|
|
190
|
+
...(definition.route ? { openUrl: openUrl(definition, path) } : {}),
|
|
191
|
+
...(service.error ? { error: service.error } : {}),
|
|
192
|
+
...(service.log !== undefined ? { log: service.log } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Resolve a routed service path against the browser app origin for agent output. */
|
|
197
|
+
export function withServiceOrigin(service: LakeboxService, origin?: string): LakeboxService {
|
|
198
|
+
if (!service.openUrl || !origin) return service;
|
|
199
|
+
try {
|
|
200
|
+
const base = new URL(origin);
|
|
201
|
+
if (base.protocol !== "http:" && base.protocol !== "https:") return service;
|
|
202
|
+
return { ...service, openUrl: new URL(service.openUrl, base).href };
|
|
203
|
+
} catch {
|
|
204
|
+
return service;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Returns the stopped placeholder for a built-in service that has never started. */
|
|
209
|
+
export function idleBuiltInService(
|
|
210
|
+
name: LakeboxBuiltInServiceName,
|
|
211
|
+
user: LakeboxUser,
|
|
212
|
+
routePrefix: string,
|
|
213
|
+
): LakeboxService {
|
|
214
|
+
return toLakeboxService(
|
|
215
|
+
{ name, state: "stopped", definition: builtInDefinition(name, user) },
|
|
216
|
+
user,
|
|
217
|
+
routePrefix,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function openUrl(definition: LakeboxControllerDefinition, path: string): string {
|
|
222
|
+
if (definition.kind === "desktop") return desktopOpenUrl(path);
|
|
223
|
+
return `${path}/${definition.openPath ?? ""}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function injectedEnvironment(
|
|
227
|
+
user: LakeboxUser,
|
|
228
|
+
name: string,
|
|
229
|
+
port: number,
|
|
230
|
+
route = true,
|
|
231
|
+
): Record<string, string> {
|
|
232
|
+
const value = String(port);
|
|
233
|
+
return {
|
|
234
|
+
SERVICE_NAME: name,
|
|
235
|
+
SERVICE_PATH: route ? `${user.routePrefix}/${name}` : "",
|
|
236
|
+
SERVICE_PORT: value,
|
|
237
|
+
PORT: value,
|
|
238
|
+
};
|
|
239
|
+
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { LocalSkillDirectoryMount } from "@dbx-tools/appkit-mastra";
|
|
5
|
+
|
|
6
|
+
/** Default agent-workspace path for packaged Lakebox service recipes. */
|
|
7
|
+
export const LAKEBOX_SKILLS_MOUNT = "/.dbx-tools/skills";
|
|
8
|
+
|
|
9
|
+
/** Returns the packaged Lakebox recipe tree as a Mastra local skill mount. */
|
|
10
|
+
export function lakeboxSkillMounts(): LocalSkillDirectoryMount[] {
|
|
11
|
+
return [{ root: lakeboxSkillsRoot(), mountPath: LAKEBOX_SKILLS_MOUNT }];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function lakeboxSkillsRoot(): string {
|
|
15
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const candidates = [
|
|
17
|
+
resolve(moduleDirectory, "../skills"),
|
|
18
|
+
resolve(moduleDirectory, "../../skills"),
|
|
19
|
+
];
|
|
20
|
+
const root = candidates.find((candidate) => existsSync(candidate));
|
|
21
|
+
if (!root) throw new Error("Packaged Lakebox skills are unavailable");
|
|
22
|
+
return root;
|
|
23
|
+
}
|
package/src/user.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Lakebox user and service-port derivation.
|
|
3
|
+
*
|
|
4
|
+
* One FNV-1a hash of the authenticated identity is the only routing key. It
|
|
5
|
+
* selects the Linux account, UID, persistent home, AppKit route segment, and,
|
|
6
|
+
* with a service name, the loopback service port. Nothing here is persisted:
|
|
7
|
+
* every caller recomputes the same values, and collisions fail closed where
|
|
8
|
+
* they are observed.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hash } from "@dbx-tools/shared-core";
|
|
14
|
+
import type { LakeboxUser } from "@dbx-tools/shared-lakebox";
|
|
15
|
+
|
|
16
|
+
/** Platform account that owns the persistent volume and the SSH session. */
|
|
17
|
+
export const LAKEBOX_AGENT_USER = "sandbox-agent";
|
|
18
|
+
/** Persistent home mounted by the platform. Only this path survives a restart. */
|
|
19
|
+
export const LAKEBOX_AGENT_HOME = `/home/${LAKEBOX_AGENT_USER}`;
|
|
20
|
+
/** Root of package-owned persistent state. */
|
|
21
|
+
export const LAKEBOX_STATE_ROOT = `${LAKEBOX_AGENT_HOME}/.dbx-tools`;
|
|
22
|
+
/** Persistent backing directories for Lakebox user homes. */
|
|
23
|
+
export const LAKEBOX_USERS_ROOT = `${LAKEBOX_STATE_ROOT}/users`;
|
|
24
|
+
/** Shared read-only runtime binaries, such as the container bundle and ttyd. */
|
|
25
|
+
export const LAKEBOX_RUNTIME_ROOT = `${LAKEBOX_STATE_ROOT}/runtime`;
|
|
26
|
+
/** Per-boot runtime state root; reset with the root filesystem. */
|
|
27
|
+
export const LAKEBOX_RUN_ROOT = "/run/lakebox";
|
|
28
|
+
|
|
29
|
+
/** First derived Linux UID. The persistent idmapped volume stores UIDs below 65536. */
|
|
30
|
+
export const LAKEBOX_UID_MIN = 20_000;
|
|
31
|
+
/** Number of derivable UIDs. */
|
|
32
|
+
export const LAKEBOX_UID_COUNT = 40_000;
|
|
33
|
+
/** First derived service port. Platform listeners stay below this value. */
|
|
34
|
+
export const LAKEBOX_PORT_MIN = 20_000;
|
|
35
|
+
/** Number of derivable service ports; the kernel ephemeral range starts at 32768. */
|
|
36
|
+
export const LAKEBOX_PORT_COUNT = 12_000;
|
|
37
|
+
|
|
38
|
+
const USER_PREFIX = "lakebox-";
|
|
39
|
+
const HASH_ALPHABET = "0123456789abcdefghijklmnopqrstuv";
|
|
40
|
+
|
|
41
|
+
/** Normalizes an authenticated identity before hashing. */
|
|
42
|
+
export function normalizeLakeboxIdentity(identity: string): string {
|
|
43
|
+
const normalized = identity.trim().toLowerCase();
|
|
44
|
+
if (!normalized) throw new Error("Lakebox users require an authenticated identity");
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Derives the Lakebox account for one authenticated identity.
|
|
50
|
+
*
|
|
51
|
+
* @param routePrefix - AppKit root for user service routes.
|
|
52
|
+
*/
|
|
53
|
+
export function lakeboxUser(identity: string, routePrefix: string): LakeboxUser {
|
|
54
|
+
const userHash = digestText("lakebox-user", normalizeLakeboxIdentity(identity));
|
|
55
|
+
const name = `${USER_PREFIX}${userHash}`;
|
|
56
|
+
return {
|
|
57
|
+
hash: userHash,
|
|
58
|
+
name,
|
|
59
|
+
uid: LAKEBOX_UID_MIN + (digestValue("lakebox-uid", userHash) % LAKEBOX_UID_COUNT),
|
|
60
|
+
home: `/home/${name}`,
|
|
61
|
+
routePrefix: `${routePrefix.replace(/\/+$/, "")}/${userHash}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Returns the persistent backing directory for a user hash. */
|
|
66
|
+
export function lakeboxPersistentHome(userHash: string): string {
|
|
67
|
+
return `${LAKEBOX_USERS_ROOT}/${userHash}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Derives the loopback port for one user's service. */
|
|
71
|
+
export function lakeboxServicePort(userHash: string, serviceName: string): number {
|
|
72
|
+
return (
|
|
73
|
+
LAKEBOX_PORT_MIN + (digestValue("lakebox-port", userHash, serviceName) % LAKEBOX_PORT_COUNT)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns whether a route segment has the shape of a user hash. */
|
|
78
|
+
export function isLakeboxUserHash(value: string): boolean {
|
|
79
|
+
return /^[0-9a-v]{7}$/.test(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function digestText(...values: string[]): string {
|
|
83
|
+
return hash.fnvHashWithOptions({ length: 7, alphabet: HASH_ALPHABET }, ...values);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function digestValue(...values: string[]): number {
|
|
87
|
+
return Number.parseInt(digestText(...values), 32);
|
|
88
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ConfigurationError } from "@databricks/appkit";
|
|
2
|
+
import { MASTRA_USER_KEY, type User } from "@dbx-tools/appkit-mastra";
|
|
3
|
+
import {
|
|
4
|
+
MastraSandbox,
|
|
5
|
+
type CommandResult,
|
|
6
|
+
type ExecuteCommandOptions,
|
|
7
|
+
type SandboxInfo,
|
|
8
|
+
type SandboxStartOptions,
|
|
9
|
+
type SandboxStartResult,
|
|
10
|
+
type WorkspaceSandbox,
|
|
11
|
+
type WorkspaceSandboxResolver,
|
|
12
|
+
} from "@mastra/core/workspace";
|
|
13
|
+
import type { LakeboxRuntime } from "./runtime.ts";
|
|
14
|
+
|
|
15
|
+
const INSTRUCTIONS = `Commands run on the app's shared Databricks Lakebox as your own locked, non-sudo Linux account with a persistent home. Other users have separate accounts and cannot reach your files or service ports. The Lakebox is created and started on first use and stops after its idle timeout; files and saved services persist.
|
|
16
|
+
|
|
17
|
+
Run long-lived servers as services, not background commands. Call create_lakebox_service with an OCI image; it starts the service detached, waits until it is ready, and returns openUrl. The runtime injects SERVICE_NAME, SERVICE_PATH, SERVICE_PORT, and PORT. Routed services must listen on 127.0.0.1:$SERVICE_PORT. There is no Docker CLI, daemon, port publishing, privileged mode, or device access. Bind mounts come from your home through the mounts field. Services restart from their saved definition when the Lakebox starts; process memory does not resume.
|
|
18
|
+
|
|
19
|
+
For a minimal routed Python service:
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
"name": "hello",
|
|
23
|
+
"image": "docker.io/library/python:3.12-alpine",
|
|
24
|
+
"command": ["python", "-c", "import http.server, os; name=os.environ['SERVICE_NAME']; H=type('H',(http.server.BaseHTTPRequestHandler,),{'do_GET':lambda s:(s.send_response(200),s.send_header('Content-Type','text/plain'),s.end_headers(),s.wfile.write(f'hello from {name}\\\\n'.encode())),'log_message':lambda *a:None}); http.server.ThreadingHTTPServer(('127.0.0.1',int(os.environ['SERVICE_PORT'])),H).serve_forever()"]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Terminal and desktop are built-in services: call start_lakebox_service with name "terminal" or "desktop" and return the openUrl. Graphical applications belong on the desktop; never start, stop, or reconfigure the desktop display yourself.
|
|
28
|
+
|
|
29
|
+
With execute_lakebox_command, pass command "sh" and put the script in args ["-lc", "..."]; never pass an executable and its arguments together as command. Commands never receive Databricks credentials. Never return localhost, a loopback port, or a direct Lakebox URL; use the returned openUrl. Services must use relative asset and API URLs beneath their route. Derive ws:// or wss:// from the browser origin.`;
|
|
30
|
+
|
|
31
|
+
/** Options for the Lakebox-backed Mastra workspace sandbox. */
|
|
32
|
+
export interface LakeboxWorkspaceSandboxOptions {
|
|
33
|
+
/** Optional display name presented by Mastra instead of Lakebox. */
|
|
34
|
+
displayName?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Mastra workspace sandbox backed by the app Lakebox.
|
|
39
|
+
*
|
|
40
|
+
* Commands run as the requesting user's Lakebox account through the shared
|
|
41
|
+
* {@link LakeboxRuntime}, so identity, credential scrubbing, and transport
|
|
42
|
+
* policy match the AppKit plugin tools and routes.
|
|
43
|
+
*/
|
|
44
|
+
export class LakeboxWorkspaceSandbox extends MastraSandbox {
|
|
45
|
+
readonly provider = "appkit-lakebox";
|
|
46
|
+
status: WorkspaceSandbox["status"] = "pending";
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
readonly id: string,
|
|
50
|
+
readonly name: string,
|
|
51
|
+
private readonly runtime: LakeboxRuntime,
|
|
52
|
+
private readonly identity: string,
|
|
53
|
+
) {
|
|
54
|
+
super({ name });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Creates or starts the app Lakebox. */
|
|
58
|
+
async start(options?: SandboxStartOptions): Promise<SandboxStartResult> {
|
|
59
|
+
await this.runtime.start(options?.abortSignal);
|
|
60
|
+
this.status = "running";
|
|
61
|
+
return { outcome: "connected" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Leaves the shared Lakebox running; its platform idle timeout stops it. */
|
|
65
|
+
async stop(): Promise<void> {
|
|
66
|
+
this.status = "stopped";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Releases this handle without changing the shared Lakebox. */
|
|
70
|
+
async destroy(): Promise<void> {
|
|
71
|
+
this.status = "destroyed";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Mastra snapshot hook. Lakeboxes do not expose snapshot operations. */
|
|
75
|
+
async snapshot(): Promise<void> {}
|
|
76
|
+
|
|
77
|
+
/** Runs a command as the current user's Lakebox account. */
|
|
78
|
+
override async executeCommand(
|
|
79
|
+
command: string,
|
|
80
|
+
args: string[] = [],
|
|
81
|
+
options: ExecuteCommandOptions = {},
|
|
82
|
+
): Promise<CommandResult> {
|
|
83
|
+
const execution = this.runtime.execute(
|
|
84
|
+
this.identity,
|
|
85
|
+
{
|
|
86
|
+
command,
|
|
87
|
+
args,
|
|
88
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
89
|
+
...(options.env
|
|
90
|
+
? {
|
|
91
|
+
env: Object.fromEntries(
|
|
92
|
+
Object.entries(options.env).filter(
|
|
93
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
...(options.timeout ? { timeoutMs: options.timeout } : {}),
|
|
99
|
+
},
|
|
100
|
+
options.abortSignal,
|
|
101
|
+
);
|
|
102
|
+
if (options.onStdout) execution.stdout.on("data", (chunk: string) => options.onStdout?.(chunk));
|
|
103
|
+
if (options.onStderr) execution.stderr.on("data", (chunk: string) => options.onStderr?.(chunk));
|
|
104
|
+
const result = await execution.result;
|
|
105
|
+
const stdout = retainCommandOutput(result.stdout, options.maxRetainedBytes);
|
|
106
|
+
const stderr = retainCommandOutput(result.stderr, options.maxRetainedBytes);
|
|
107
|
+
return {
|
|
108
|
+
...result,
|
|
109
|
+
stdout: stdout.value,
|
|
110
|
+
stderr: stderr.value,
|
|
111
|
+
...(stdout.droppedBytes > 0
|
|
112
|
+
? { stdoutTruncated: true, stdoutDroppedBytes: stdout.droppedBytes }
|
|
113
|
+
: {}),
|
|
114
|
+
...(stderr.droppedBytes > 0
|
|
115
|
+
? { stderrTruncated: true, stderrDroppedBytes: stderr.droppedBytes }
|
|
116
|
+
: {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Returns the current Lakebox status in Mastra's sandbox shape. */
|
|
121
|
+
async getInfo(): Promise<SandboxInfo> {
|
|
122
|
+
const summary = await this.runtime.summary(this.identity);
|
|
123
|
+
this.status =
|
|
124
|
+
summary.status === "missing" || summary.status === "unknown" ? "pending" : summary.status;
|
|
125
|
+
return {
|
|
126
|
+
id: this.id,
|
|
127
|
+
name: this.name,
|
|
128
|
+
provider: this.provider,
|
|
129
|
+
status: this.status,
|
|
130
|
+
createdAt: new Date(0),
|
|
131
|
+
metadata: { lakeboxId: summary.id, user: summary.user.name, services: summary.services },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Returns agent instructions for services, routes, and command execution. */
|
|
136
|
+
getInstructions(): string {
|
|
137
|
+
return INSTRUCTIONS;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Creates a request-scoped Mastra workspace resolver for the app Lakebox.
|
|
143
|
+
*
|
|
144
|
+
* Resolution never creates compute; the first command or service does.
|
|
145
|
+
*/
|
|
146
|
+
export function lakeboxWorkspaceSandbox(
|
|
147
|
+
runtime: LakeboxRuntime,
|
|
148
|
+
options: LakeboxWorkspaceSandboxOptions = {},
|
|
149
|
+
): WorkspaceSandboxResolver {
|
|
150
|
+
return async ({ requestContext }) => {
|
|
151
|
+
const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
|
|
152
|
+
if (!user) {
|
|
153
|
+
throw ConfigurationError.resourceNotFound(
|
|
154
|
+
"Lakebox user context",
|
|
155
|
+
"Invoke Code Mode from an agent turn served by the Mastra plugin.",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return new LakeboxWorkspaceSandbox(
|
|
159
|
+
runtime.config.application,
|
|
160
|
+
options.displayName ?? "Lakebox",
|
|
161
|
+
runtime,
|
|
162
|
+
user.id,
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function retainCommandOutput(
|
|
168
|
+
output: string,
|
|
169
|
+
maxRetainedBytes: number | undefined,
|
|
170
|
+
): { value: string; droppedBytes: number } {
|
|
171
|
+
if (maxRetainedBytes === undefined || maxRetainedBytes === Infinity) {
|
|
172
|
+
return { value: output, droppedBytes: 0 };
|
|
173
|
+
}
|
|
174
|
+
const limit = Number.isFinite(maxRetainedBytes) ? Math.max(0, Math.floor(maxRetainedBytes)) : 0;
|
|
175
|
+
const bytes = Buffer.from(output);
|
|
176
|
+
if (bytes.length <= limit) return { value: output, droppedBytes: 0 };
|
|
177
|
+
let retainedStart = bytes.length - limit;
|
|
178
|
+
while (retainedStart < bytes.length && (bytes[retainedStart]! & 0xc0) === 0x80) {
|
|
179
|
+
retainedStart++;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
value: bytes.subarray(retainedStart).toString("utf8"),
|
|
183
|
+
droppedBytes: retainedStart,
|
|
184
|
+
};
|
|
185
|
+
}
|