Skip to content

@dbx-tools/appkit-sandbox

AppKit integration for Databricks Sandboxes. It manages assigned machines, executes SSH commands, provisions browser services, and persists isolated Projects in Lakebase.

Terminal window
bun add @dbx-tools/appkit-sandbox

Register AppKit lakebase() before databricksSandbox(). The Sandbox plugin reuses that service-principal pool after plugins initialize. It does not create another pool or request user-scoped Postgres credentials.

Create one SshRuntime and share it with the AppKit plugin and Mastra workspace:

import { lakebase } from "@databricks/appkit";
import { mastra } from "@dbx-tools/appkit-mastra";
import { resolveSshPluginConfig } from "@dbx-tools/appkit-sandbox/config";
import { databricksSandbox } from "@dbx-tools/appkit-sandbox/plugin";
import { SshRuntime } from "@dbx-tools/appkit-sandbox/runtime";
import { databricksSandboxSkillMounts } from "@dbx-tools/appkit-sandbox/skills";
import { sshWorkspaceSandbox } from "@dbx-tools/appkit-sandbox/workspace";
const runtime = new SshRuntime(
resolveSshPluginConfig({
auto: true,
routePrefix: "/sandbox",
sandboxLifetimeSeconds: 86_400,
reaperIntervalSeconds: 300,
}),
);
export const plugins = [
lakebase(),
databricksSandbox({ runtime }),
mastra({
codeMode: true,
sandbox: sshWorkspaceSandbox(runtime),
localSkillMounts: databricksSandboxSkillMounts(),
}),
];

The Mastra resolver uses a Sandbox only for an explicit SSH-backed operation. Other Code Mode work continues to use Mastra’s normal isolated runtime.

Use the add_ssh_project tool instead of creating a proxy route directly:

{
"machineId": "sandbox-id",
"name": "demo",
"openPath": "index.html",
"config": {
"tools": {
"node": "lts"
},
"tasks": {
"start": {
"run": "[ -f server.mjs ] || exit 0\nif [ -f server.pid ] && kill -0 \"$(cat server.pid)\" 2>/dev/null; then exit 0; fi\nnohup node server.mjs > server.log 2>&1 < /dev/null &\necho $! > server.pid\n"
},
"stop": {
"run": "[ -f server.pid ] && kill \"$(cat server.pid)\" 2>/dev/null || true\n"
}
}
}
}

name, path, and port are optional. The runtime generates values that are unique for the machine, creates ~/projects/<path>, writes mise.toml, runs mise trust and mise install, and returns the project directory, public path, loopback port, and openUrl.

Set route: false for a project that does not serve HTTP. Route-less projects still receive a directory, mise environment, and lifecycle tasks, but they do not register an AppKit proxy route or return an openUrl. Declare required built-in services with serviceDependencies, for example serviceDependencies: ["desktop"]. The runtime starts each dependency before project setup and writes hidden sandbox:<kind> dependency tasks into the project’s mise.toml.

Keep hooks.postinstall limited to installation or preparation that belongs to mise install. Put runtime lifecycle behavior in tasks.start and tasks.stop; use a visible tasks.uninstall only when the project has additional teardown beyond stopping its workload. Before invoking any task, call execute_ssh_command from the returned directory with command: "sh" and args: ["-lc", "\"$HOME/.local/bin/mise\" tasks ls --name-only --hidden"]. Evaluate the available tasks and invoke only the task defined for the requested lifecycle action, using the same command and directory with args: ["-lc", "\"$HOME/.local/bin/mise\" run <task>"]. Never concatenate the executable and arguments into command. Start detached servers with nohup and redirected standard streams. The packaged skills under skills/ specify when and where to invoke each task.

The runtime injects these values into [env]:

  • PROJECT_NAME
  • PROJECT_PATH
  • PROJECT_PORT

A matching caller-supplied value is accepted. A conflicting value fails before the route or Lakebase record changes. Run commands from the returned directory with mise exec and bind HTTP services to 127.0.0.1:$PROJECT_PORT.

Pass the returned projectId to update the name, path, open path, port, config, or prefix behavior. An explicit duplicate path or port fails. Omitting a field during an update retains its stored value.

remove_ssh_project runs mise run stop and then mise run uninstall when each task exists. Discovery includes hidden tasks for compatibility, but new uninstall tasks should remain visible. Failures are logged and ignored before the proxy route, Lakebase record, and project directory are removed. Stopping a Sandbox preserves Projects. Deleting or expiring a Sandbox removes its Project records without starting the machine for remote cleanup.

The browser-safe request and response fields are defined by addSshProjectRequestSchema and sshProjectSchema.

The default machineSoftware configuration installs mise in ~/.local/bin. The installer adds mise activation to the Sandbox user’s Bash and Zsh startup files, so entering a trusted Project directory activates its environment.

Add other idempotent setup steps through SshPluginConfig:

resolveSshPluginConfig({
machineSoftware: [
{
name: "mise",
checkCommand: 'test -x "$HOME/.local/bin/mise"',
installCommand: "...",
},
{
name: "custom-tool",
checkCommand: "command -v custom-tool >/dev/null 2>&1",
installCommand: "...",
},
],
});

Pass an empty array to disable automatic machine setup.

Terminal, desktop, and stats are hidden, machine-owned mise projects under ~/projects/.sandbox-services. Their packaged mise.toml files own installation and start/stop lifecycle tasks. Lakebase stores their definitions separately from user projects, so they do not appear in project lists or counts.

Service setup uses a workspace-, machine-, and service-scoped Lakebase advisory lock. Concurrent in-process calls share one operation; after acquiring the distributed lock, the runtime rechecks the persisted definition and skips installation when it is current. Packaged lifecycle tasks recheck remote PID and version state while holding the lock. A service returned as ready has completed its process and endpoint readiness check and has a usable authenticated route.

Terminal and stats start with an explicitly started machine. Desktop remains on demand unless machine creation passes desktop: true or a project declares the desktop service dependency. KasmVNC is downloaded only from its pinned, checksummed official GitHub release.

The AppKit server identity must be able to use the Sandbox REST API and manage SSH keys. The AppKit host must provide OpenSSH on PATH. Direct HTTP and WebSocket forwarding uses ssh2.

The runtime records ownership and absolute expiration in Lakebase. The SshRuntime reaper runs at startup and on the configured interval. It deletes expired assigned Sandboxes, removes records for missing Sandboxes, and leaves unrelated workspace Sandboxes unchanged.

Stopping a machine closes local SSH sessions and forwarding bridges. Persisted Projects are restored when the app restarts or the machine starts again.

Workspace REST calls use the current AppKit user’s OBO credentials when available. fallbackToServicePrincipal can retry a failed REST operation with the app identity. Machine assignment checks still apply to discovery, lifecycle, commands, Projects, Mastra tools, and Code Mode.

Assignments and Projects use the AppKit Lakebase service-principal pool through LakebaseMachineOwnershipStore. The pool remains owned by the Lakebase plugin. Closing the Sandbox plugin does not close it.

This assignment layer limits what the application reveals and operates because the Sandbox API does not expose native per-user Sandbox scopes. It does not replace Databricks workspace authorization.

SshKeyStore creates one Ed25519 key pair for each workspace and AppKit user. Only the public key is registered. Private keys and known-host files remain on the AppKit server under ~/.dbx-tools/appkit-databricks-sandbox by default.

The first successful connection records the gateway host key. Later connections reject a changed key. SshSession passes the stored file through OpenSSH UserKnownHostsFile. Host verification is never disabled.

sshWorkspaceSandbox() provides the request-scoped Mastra workspace. The plugin also registers machine, command, service, and Project tools. Packaged showcase recipes are available through databricksSandboxSkillMounts().

See the complete package API reference.

Open the complete API reference