@mastra/daytona 0.3.0-alpha.0 → 0.4.0-alpha.0
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/CHANGELOG.md +48 -0
- package/README.md +35 -5
- package/dist/index.cjs +250 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +250 -1
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/mounts/azure.d.ts +40 -0
- package/dist/sandbox/mounts/azure.d.ts.map +1 -0
- package/dist/sandbox/mounts/index.d.ts +1 -0
- package/dist/sandbox/mounts/index.d.ts.map +1 -1
- package/dist/sandbox/mounts/types.d.ts +2 -1
- package/dist/sandbox/mounts/types.d.ts.map +1 -1
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# @mastra/daytona
|
|
2
2
|
|
|
3
|
+
## 0.4.0-alpha.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added Azure Blob sandbox mount support via blobfuse2 in @mastra/e2b and @mastra/daytona. `sandbox.mount(azureBlobFilesystem, '/data')` now works for Azure containers, matching the existing s3fs (S3) and gcsfuse (GCS) integration. Supports authentication via accountKey, sasToken, connectionString, or managed identity/default credentials, and preserves AzureBlobFilesystem prefixes when mounting. ([#15874](https://github.com/mastra-ai/mastra/pull/15874))
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { E2BSandbox } from '@mastra/e2b';
|
|
11
|
+
import { AzureBlobFilesystem } from '@mastra/azure/blob';
|
|
12
|
+
|
|
13
|
+
const azureFs = new AzureBlobFilesystem({ container: 'my-data', connectionString: '...' });
|
|
14
|
+
const sandbox = new E2BSandbox();
|
|
15
|
+
await sandbox.mount(azureFs, '/data');
|
|
16
|
+
// Sandbox processes can now read/write /data/* directly against the Azure container.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [[`512a013`](https://github.com/mastra-ai/mastra/commit/512a013f285aa9c0aa8f08a35b2ce09f9938b017), [`e9becde`](https://github.com/mastra-ai/mastra/commit/e9becdeed9176b9f8392e557bde12b933f99cf7a)]:
|
|
22
|
+
- @mastra/core@1.29.1-alpha.2
|
|
23
|
+
|
|
24
|
+
## 0.3.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- Added S3 prefix (subdirectory) mount support. You can now mount a specific folder within an S3 bucket instead of the entire bucket by setting the `prefix` option on your S3 filesystem. ([#15171](https://github.com/mastra-ai/mastra/pull/15171))
|
|
29
|
+
|
|
30
|
+
**Example:**
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const fs = new S3Filesystem({
|
|
34
|
+
bucket: 'my-bucket',
|
|
35
|
+
region: 'us-east-1',
|
|
36
|
+
prefix: 'workspace/data',
|
|
37
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
38
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
When mounted in a sandbox, only the contents under `workspace/data/` in the bucket will be visible at the mount path. This uses the s3fs `bucket:/path` syntax under the hood.
|
|
43
|
+
|
|
44
|
+
Closes #15147.
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- Updated dependencies [[`f112db1`](https://github.com/mastra-ai/mastra/commit/f112db179557ae9b5a0f1d25dc47f928d7d61cd9), [`21d9706`](https://github.com/mastra-ai/mastra/commit/21d970604d89eee970cbf8013d26d7551aff6ea5), [`0a0aa94`](https://github.com/mastra-ai/mastra/commit/0a0aa94729592e99885af2efb90c56aaada62247), [`ed07df3`](https://github.com/mastra-ai/mastra/commit/ed07df32a9d539c8261e892fc1bade783f5b41a6), [`01a7d51`](https://github.com/mastra-ai/mastra/commit/01a7d513493d21562f677f98550f7ceb165ba78c)]:
|
|
49
|
+
- @mastra/core@1.27.0
|
|
50
|
+
|
|
3
51
|
## 0.3.0-alpha.0
|
|
4
52
|
|
|
5
53
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Daytona cloud sandbox provider for [Mastra](https://mastra.ai) workspaces.
|
|
4
4
|
|
|
5
|
-
Implements the `WorkspaceSandbox` interface using [Daytona](https://www.daytona.io/) sandboxes. Supports multiple runtimes, resource configuration, volumes, snapshots, streaming output, sandbox reconnection, and filesystem mounting (S3, GCS).
|
|
5
|
+
Implements the `WorkspaceSandbox` interface using [Daytona](https://www.daytona.io/) sandboxes. Supports multiple runtimes, resource configuration, volumes, snapshots, streaming output, sandbox reconnection, and filesystem mounting (S3, GCS, Azure Blob).
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Implements the `WorkspaceSandbox` interface using [Daytona](https://www.daytona.
|
|
|
10
10
|
pnpm add @mastra/daytona @mastra/core
|
|
11
11
|
|
|
12
12
|
# For filesystem mounting (optional)
|
|
13
|
-
pnpm add @mastra/s3 @mastra/gcs
|
|
13
|
+
pnpm add @mastra/s3 @mastra/gcs @mastra/azure
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
@@ -101,7 +101,7 @@ console.log(result.stdout); // "session 1"
|
|
|
101
101
|
|
|
102
102
|
### Filesystem mounting
|
|
103
103
|
|
|
104
|
-
Mount S3 or
|
|
104
|
+
Mount S3, GCS, or Azure Blob containers as local directories inside the sandbox.
|
|
105
105
|
|
|
106
106
|
#### Via workspace mounts config
|
|
107
107
|
|
|
@@ -112,6 +112,7 @@ import { Workspace } from '@mastra/core/workspace';
|
|
|
112
112
|
import { DaytonaSandbox } from '@mastra/daytona';
|
|
113
113
|
import { GCSFilesystem } from '@mastra/gcs';
|
|
114
114
|
import { S3Filesystem } from '@mastra/s3';
|
|
115
|
+
import { AzureBlobFilesystem } from '@mastra/azure/blob';
|
|
115
116
|
|
|
116
117
|
const workspace = new Workspace({
|
|
117
118
|
mounts: {
|
|
@@ -127,6 +128,11 @@ const workspace = new Workspace({
|
|
|
127
128
|
projectId: 'my-project-id',
|
|
128
129
|
credentials: JSON.parse(process.env.GCS_SERVICE_ACCOUNT_KEY!),
|
|
129
130
|
}),
|
|
131
|
+
'/azure-data': new AzureBlobFilesystem({
|
|
132
|
+
container: process.env.AZURE_STORAGE_CONTAINER!,
|
|
133
|
+
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
|
134
|
+
prefix: 'workspace/data',
|
|
135
|
+
}),
|
|
130
136
|
},
|
|
131
137
|
sandbox: new DaytonaSandbox({ language: 'python' }),
|
|
132
138
|
});
|
|
@@ -194,6 +200,21 @@ await sandbox.mount(
|
|
|
194
200
|
);
|
|
195
201
|
```
|
|
196
202
|
|
|
203
|
+
#### Azure Blob
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { AzureBlobFilesystem } from '@mastra/azure/blob';
|
|
207
|
+
|
|
208
|
+
await sandbox.mount(
|
|
209
|
+
new AzureBlobFilesystem({
|
|
210
|
+
container: process.env.AZURE_STORAGE_CONTAINER!,
|
|
211
|
+
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
|
212
|
+
prefix: 'workspace/data',
|
|
213
|
+
}),
|
|
214
|
+
'/data',
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
197
218
|
### Network isolation
|
|
198
219
|
|
|
199
220
|
Restrict outbound network access:
|
|
@@ -270,7 +291,7 @@ console.log(response.text);
|
|
|
270
291
|
|
|
271
292
|
## Mount Configuration
|
|
272
293
|
|
|
273
|
-
Pass `S3Filesystem` or `
|
|
294
|
+
Pass `S3Filesystem`, `GCSFilesystem`, or `AzureBlobFilesystem` instances via the workspace `mounts` config or directly to `sandbox.mount()`.
|
|
274
295
|
|
|
275
296
|
### S3 environment variables
|
|
276
297
|
|
|
@@ -289,9 +310,16 @@ Pass `S3Filesystem` or `GCSFilesystem` instances via the workspace `mounts` conf
|
|
|
289
310
|
| `GCS_BUCKET` | Bucket name |
|
|
290
311
|
| `GCS_SERVICE_ACCOUNT_KEY` | Service account key JSON (full JSON string, not a path) |
|
|
291
312
|
|
|
313
|
+
### Azure Blob environment variables
|
|
314
|
+
|
|
315
|
+
| Variable | Description |
|
|
316
|
+
| --------------------------------- | ------------------------- |
|
|
317
|
+
| `AZURE_STORAGE_CONTAINER` | Container name |
|
|
318
|
+
| `AZURE_STORAGE_CONNECTION_STRING` | Storage connection string |
|
|
319
|
+
|
|
292
320
|
### Reducing cold start latency with a snapshot
|
|
293
321
|
|
|
294
|
-
By default, `s3fs` and `
|
|
322
|
+
By default, `s3fs`, `gcsfuse`, and `blobfuse2` are installed at first mount, which adds startup time. To eliminate this, prebake them into a Daytona snapshot and pass the snapshot name via the `snapshot` option.
|
|
295
323
|
|
|
296
324
|
Create the snapshot once:
|
|
297
325
|
|
|
@@ -321,6 +349,8 @@ await daytona.snapshot.create(
|
|
|
321
349
|
);
|
|
322
350
|
```
|
|
323
351
|
|
|
352
|
+
If you use Azure Blob mounts, also pre-install `blobfuse2` in the snapshot using Azure's supported package for your base image. See Azure's [BlobFuse2 installation guide](https://learn.microsoft.com/en-us/azure/storage/blobs/blobfuse2-how-to-deploy) for supported install options.
|
|
353
|
+
|
|
324
354
|
Then use the snapshot name in your sandbox config:
|
|
325
355
|
|
|
326
356
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -268,6 +268,250 @@ Sandbox network response: ${checkOutput}` : "")
|
|
|
268
268
|
throw new Error(`Failed to mount GCS bucket: ${result.stderr || result.stdout}`);
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
+
var SAFE_CONTAINER_NAME = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
|
|
272
|
+
var BLOBFUSE2_GITHUB_DEB = "https://github.com/Azure/azure-storage-fuse/releases/download/blobfuse2-2.5.1/blobfuse2-2.5.1-Ubuntu-22.04.x86_64.deb";
|
|
273
|
+
function validateContainerName(name) {
|
|
274
|
+
if (!SAFE_CONTAINER_NAME.test(name) || name.includes("--")) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Invalid Azure container name: "${name}". Container names must be 3-63 lowercase alphanumeric characters or hyphens, with no consecutive hyphens.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function parseConnectionString(cs) {
|
|
281
|
+
const out = {};
|
|
282
|
+
for (const part of cs.split(";")) {
|
|
283
|
+
const eq = part.indexOf("=");
|
|
284
|
+
if (eq === -1) continue;
|
|
285
|
+
const key = part.slice(0, eq).trim();
|
|
286
|
+
const value = part.slice(eq + 1).trim();
|
|
287
|
+
if (!value) continue;
|
|
288
|
+
if (key === "AccountName") out.accountName = value;
|
|
289
|
+
else if (key === "AccountKey") out.accountKey = value;
|
|
290
|
+
else if (key === "SharedAccessSignature") out.sasToken = value;
|
|
291
|
+
else if (key === "BlobEndpoint") out.endpoint = value;
|
|
292
|
+
else if (key === "EndpointSuffix") out.endpointSuffix = value;
|
|
293
|
+
else if (key === "DefaultEndpointsProtocol") out.protocol = value;
|
|
294
|
+
}
|
|
295
|
+
if (!out.endpoint && out.accountName) {
|
|
296
|
+
out.endpoint = `${out.protocol || "https"}://${out.accountName}.blob.${out.endpointSuffix || "core.windows.net"}`;
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
function yamlString(value) {
|
|
301
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
302
|
+
}
|
|
303
|
+
function parseOsRelease(output) {
|
|
304
|
+
const values = {};
|
|
305
|
+
for (const line of output.split("\n")) {
|
|
306
|
+
const eq = line.indexOf("=");
|
|
307
|
+
if (eq === -1) continue;
|
|
308
|
+
const key = line.slice(0, eq);
|
|
309
|
+
const value = line.slice(eq + 1).trim().replace(/^"|"$/g, "");
|
|
310
|
+
values[key] = value;
|
|
311
|
+
}
|
|
312
|
+
return values;
|
|
313
|
+
}
|
|
314
|
+
function resolveMicrosoftAptRepos(osReleaseOutput) {
|
|
315
|
+
const osRelease = parseOsRelease(osReleaseOutput);
|
|
316
|
+
const distroId = osRelease.ID || "ubuntu";
|
|
317
|
+
const codename = osRelease.VERSION_CODENAME || (distroId === "debian" ? "bookworm" : "jammy");
|
|
318
|
+
const versionId = osRelease.VERSION_ID || (distroId === "debian" ? "12" : "22.04");
|
|
319
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(codename)) {
|
|
320
|
+
throw new Error(`Invalid distro codename for blobfuse2 repo: "${codename}"`);
|
|
321
|
+
}
|
|
322
|
+
if (!/^\d+(?:\.\d+)?$/.test(versionId)) {
|
|
323
|
+
throw new Error(`Invalid distro version for blobfuse2 repo: "${versionId}"`);
|
|
324
|
+
}
|
|
325
|
+
if (distroId === "debian") {
|
|
326
|
+
const repos = [
|
|
327
|
+
{ repoUrl: `https://packages.microsoft.com/debian/${versionId.split(".")[0]}/prod`, suite: codename }
|
|
328
|
+
];
|
|
329
|
+
if (versionId.split(".")[0] !== "12" || codename !== "bookworm") {
|
|
330
|
+
repos.push({ repoUrl: "https://packages.microsoft.com/debian/12/prod", suite: "bookworm" });
|
|
331
|
+
}
|
|
332
|
+
return repos;
|
|
333
|
+
}
|
|
334
|
+
if (distroId === "ubuntu") {
|
|
335
|
+
const repos = [{ repoUrl: `https://packages.microsoft.com/ubuntu/${versionId}/prod`, suite: codename }];
|
|
336
|
+
if (versionId !== "24.04" || codename !== "noble") {
|
|
337
|
+
repos.push({ repoUrl: "https://packages.microsoft.com/ubuntu/24.04/prod", suite: "noble" });
|
|
338
|
+
}
|
|
339
|
+
if (versionId !== "22.04" || codename !== "jammy") {
|
|
340
|
+
repos.push({ repoUrl: "https://packages.microsoft.com/ubuntu/22.04/prod", suite: "jammy" });
|
|
341
|
+
}
|
|
342
|
+
return repos;
|
|
343
|
+
}
|
|
344
|
+
throw new Error(`Unsupported distro for blobfuse2 runtime installation: "${distroId}"`);
|
|
345
|
+
}
|
|
346
|
+
function resolveAuth(config) {
|
|
347
|
+
let accountName = config.accountName;
|
|
348
|
+
let accountKey = config.accountKey;
|
|
349
|
+
let sasToken = config.sasToken;
|
|
350
|
+
let endpoint = config.endpoint;
|
|
351
|
+
if (config.connectionString) {
|
|
352
|
+
const parsed = parseConnectionString(config.connectionString);
|
|
353
|
+
accountName = accountName ?? parsed.accountName;
|
|
354
|
+
accountKey = accountKey ?? parsed.accountKey;
|
|
355
|
+
sasToken = sasToken ?? parsed.sasToken;
|
|
356
|
+
endpoint = endpoint ?? parsed.endpoint;
|
|
357
|
+
}
|
|
358
|
+
let mode;
|
|
359
|
+
if (config.useDefaultCredential) {
|
|
360
|
+
mode = "msi";
|
|
361
|
+
} else if (sasToken) {
|
|
362
|
+
mode = "sas";
|
|
363
|
+
} else if (accountKey) {
|
|
364
|
+
mode = "key";
|
|
365
|
+
} else {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"Azure Blob mount requires credentials: provide connectionString, accountKey + accountName, sasToken + accountName, or useDefaultCredential."
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (!accountName) {
|
|
371
|
+
throw new Error("Azure Blob mount requires an accountName (either explicitly or via connectionString).");
|
|
372
|
+
}
|
|
373
|
+
if (endpoint) {
|
|
374
|
+
validateEndpoint(endpoint);
|
|
375
|
+
}
|
|
376
|
+
return { mode, accountName, accountKey, sasToken, endpoint };
|
|
377
|
+
}
|
|
378
|
+
function buildBlobfuseConfig(container, auth, cachePath, readOnly) {
|
|
379
|
+
const lines = [
|
|
380
|
+
"allow-other: true",
|
|
381
|
+
"foreground: false",
|
|
382
|
+
`read-only: ${readOnly ? "true" : "false"}`,
|
|
383
|
+
"logging:",
|
|
384
|
+
" type: silent",
|
|
385
|
+
"components:",
|
|
386
|
+
" - libfuse",
|
|
387
|
+
" - file_cache",
|
|
388
|
+
" - attr_cache",
|
|
389
|
+
" - azstorage",
|
|
390
|
+
"libfuse:",
|
|
391
|
+
" attribute-expiration-sec: 240",
|
|
392
|
+
" entry-expiration-sec: 240",
|
|
393
|
+
" negative-entry-expiration-sec: 120",
|
|
394
|
+
"file_cache:",
|
|
395
|
+
` path: ${yamlString(cachePath)}`,
|
|
396
|
+
" timeout-sec: 120",
|
|
397
|
+
"attr_cache:",
|
|
398
|
+
" timeout-sec: 7200",
|
|
399
|
+
"azstorage:",
|
|
400
|
+
` mode: ${auth.mode}`,
|
|
401
|
+
` account-name: ${yamlString(auth.accountName)}`,
|
|
402
|
+
` container: ${yamlString(container)}`
|
|
403
|
+
];
|
|
404
|
+
if (auth.mode === "key" && auth.accountKey) {
|
|
405
|
+
lines.push(` account-key: ${yamlString(auth.accountKey)}`);
|
|
406
|
+
} else if (auth.mode === "sas" && auth.sasToken) {
|
|
407
|
+
lines.push(` sas: ${yamlString(auth.sasToken)}`);
|
|
408
|
+
}
|
|
409
|
+
if (auth.endpoint) {
|
|
410
|
+
lines.push(` endpoint: ${yamlString(auth.endpoint.replace(/\/$/, ""))}`);
|
|
411
|
+
}
|
|
412
|
+
return lines.join("\n") + "\n";
|
|
413
|
+
}
|
|
414
|
+
async function mountAzure(mountPath, config, ctx) {
|
|
415
|
+
const { run, writeFile, logger } = ctx;
|
|
416
|
+
validateContainerName(config.container);
|
|
417
|
+
const auth = resolveAuth(config);
|
|
418
|
+
const prefix = config.prefix ? validatePrefix(config.prefix) : void 0;
|
|
419
|
+
const quotedMountPath = shellQuote(mountPath);
|
|
420
|
+
const curlCheck = await run('which curl 2>/dev/null || echo "not found"', 3e4);
|
|
421
|
+
if (curlCheck.stdout.includes("not found")) {
|
|
422
|
+
const curlInstall = await run("sudo apt-get update -qq 2>&1 && sudo apt-get install -y curl 2>&1", 12e4);
|
|
423
|
+
if (curlInstall.exitCode !== 0) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Failed to install curl for Azure Blob reachability check: ${curlInstall.stderr || curlInstall.stdout}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const probeUrl = auth.endpoint ? auth.endpoint.replace(/\/$/, "") : `https://${auth.accountName}.blob.core.windows.net`;
|
|
430
|
+
const connectivityCheck = await run(`curl -sS --max-time 5 ${shellQuote(probeUrl)} 2>&1`, 1e4);
|
|
431
|
+
const checkOutput = connectivityCheck.stdout.trim();
|
|
432
|
+
if (connectivityCheck.exitCode !== 0 || checkOutput.toLowerCase().includes("restricted") || checkOutput.toLowerCase().includes("blocked")) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`Cannot reach ${probeUrl} from this sandbox. Azure Blob mounting requires network access to the storage endpoint, which may be blocked on Daytona's restricted tiers. Upgrade to a tier with unrestricted internet access, or contact Daytona support to remove the network restriction.` + (checkOutput ? `
|
|
435
|
+
|
|
436
|
+
Sandbox network response: ${checkOutput}` : "")
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const checkResult = await run('which blobfuse2 2>/dev/null || echo "not found"', 3e4);
|
|
440
|
+
if (checkResult.stdout.includes("not found")) {
|
|
441
|
+
logger.warn(`${LOG_PREFIX} blobfuse2 not found, attempting runtime installation...`);
|
|
442
|
+
logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install blobfuse2 in your sandbox image`);
|
|
443
|
+
await run("sudo apt-get update -qq 2>&1", 6e4);
|
|
444
|
+
const prepResult = await run("sudo apt-get install -y curl gnupg 2>&1", 12e4);
|
|
445
|
+
if (prepResult.exitCode !== 0) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Failed to install blobfuse2 prerequisites (curl, gnupg): ${prepResult.stderr || prepResult.stdout}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const osReleaseResult = await run("cat /etc/os-release 2>/dev/null || true", 3e4);
|
|
451
|
+
const repos = resolveMicrosoftAptRepos(osReleaseResult.stdout);
|
|
452
|
+
const repoSetup = await run(
|
|
453
|
+
"sudo mkdir -p /etc/apt/keyrings && curl --retry 3 --retry-all-errors --retry-delay 2 -fsSL https://packages.microsoft.com/keys/microsoft.asc -o /tmp/ms-key.asc && sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/microsoft.gpg /tmp/ms-key.asc",
|
|
454
|
+
3e4
|
|
455
|
+
);
|
|
456
|
+
let installResult;
|
|
457
|
+
if (repoSetup.exitCode === 0) {
|
|
458
|
+
for (const { repoUrl, suite } of repos) {
|
|
459
|
+
await run(
|
|
460
|
+
`echo "deb [signed-by=/etc/apt/keyrings/microsoft.gpg] ${repoUrl} ${suite} main" | sudo tee /etc/apt/sources.list.d/microsoft-prod.list`,
|
|
461
|
+
3e4
|
|
462
|
+
);
|
|
463
|
+
await run("sudo apt-get update -qq 2>&1 || true", 6e4);
|
|
464
|
+
installResult = await run("sudo apt-get install -y blobfuse2 fuse3 2>&1", 12e4);
|
|
465
|
+
if (installResult.exitCode === 0) break;
|
|
466
|
+
logger.warn(`${LOG_PREFIX} blobfuse2 install failed for ${repoUrl} ${suite}, trying fallback if available`);
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
logger.warn(`${LOG_PREFIX} Failed to set up Microsoft apt repository, trying GitHub release fallback`);
|
|
470
|
+
}
|
|
471
|
+
let verifyResult = await run("which blobfuse2 && blobfuse2 --version", 3e4);
|
|
472
|
+
if (verifyResult.exitCode !== 0) {
|
|
473
|
+
installResult = await run(
|
|
474
|
+
`sudo apt-get update -qq 2>&1 || true && sudo apt-get install -y fuse3 ca-certificates curl 2>&1 && curl -L --retry 3 --retry-all-errors --retry-delay 2 -fSLo /tmp/blobfuse2.deb ${BLOBFUSE2_GITHUB_DEB} && sudo dpkg -i /tmp/blobfuse2.deb 2>&1 && sudo bash -c 'lib=$(find /usr/lib -name "libfuse3.so.3.*" | head -1); [ -z "$lib" ] || ln -sf "$lib" /usr/lib/x86_64-linux-gnu/libfuse3.so.3'`,
|
|
475
|
+
18e4
|
|
476
|
+
);
|
|
477
|
+
verifyResult = await run("which blobfuse2 && blobfuse2 --version", 3e4);
|
|
478
|
+
}
|
|
479
|
+
if (!installResult || verifyResult.exitCode !== 0) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Failed to install blobfuse2: ${verifyResult.stderr || verifyResult.stdout || installResult?.stderr || installResult?.stdout || "unknown error"}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const idResult = await run("id -u && id -g", 3e4);
|
|
486
|
+
const [uid, gid] = idResult.stdout.trim().split("\n");
|
|
487
|
+
const validUidGid = uid && gid && /^\d+$/.test(uid) && /^\d+$/.test(gid);
|
|
488
|
+
await run(
|
|
489
|
+
`sudo chmod a+rw /dev/fuse 2>/dev/null || true; sudo bash -c 'grep -q "^user_allow_other" /etc/fuse.conf 2>/dev/null || echo "user_allow_other" >> /etc/fuse.conf' 2>/dev/null || true`
|
|
490
|
+
);
|
|
491
|
+
const mountHash = crypto.createHash("md5").update(mountPath).digest("hex").slice(0, 8);
|
|
492
|
+
const configPath = `/tmp/.blobfuse2-config-${mountHash}.yaml`;
|
|
493
|
+
const cachePath = `/tmp/blobfuse2-cache-${mountHash}`;
|
|
494
|
+
const yaml = buildBlobfuseConfig(config.container, auth, cachePath, !!config.readOnly);
|
|
495
|
+
await run(`sudo rm -f ${shellQuote(configPath)}`, 3e4);
|
|
496
|
+
await writeFile(configPath, yaml);
|
|
497
|
+
await run(`chmod 600 ${shellQuote(configPath)}`, 3e4);
|
|
498
|
+
await run(`sudo rm -rf ${shellQuote(cachePath)} && mkdir -p ${shellQuote(cachePath)}`, 3e4);
|
|
499
|
+
if (validUidGid) {
|
|
500
|
+
await run(`sudo chown ${uid}:${gid} ${shellQuote(cachePath)} 2>/dev/null || true`, 3e4);
|
|
501
|
+
}
|
|
502
|
+
const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote(prefix)}` : "";
|
|
503
|
+
const mountCmd = `blobfuse2 mount ${quotedMountPath} --config-file=${shellQuote(configPath)}${prefixFlags}`;
|
|
504
|
+
logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
|
|
505
|
+
const result = await run(mountCmd, 6e4);
|
|
506
|
+
logger.debug(`${LOG_PREFIX} blobfuse2 result:`, {
|
|
507
|
+
exitCode: result.exitCode,
|
|
508
|
+
stdout: result.stdout,
|
|
509
|
+
stderr: result.stderr
|
|
510
|
+
});
|
|
511
|
+
if (result.exitCode !== 0) {
|
|
512
|
+
throw new Error(`Failed to mount Azure Blob container: ${result.stderr || result.stdout}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
271
515
|
var DaytonaProcessHandle = class extends workspace.ProcessHandle {
|
|
272
516
|
pid;
|
|
273
517
|
_cmdId;
|
|
@@ -840,6 +1084,11 @@ var DaytonaSandbox = class _DaytonaSandbox extends workspace.MastraSandbox {
|
|
|
840
1084
|
await mountGCS(mountPath, config, mountCtx);
|
|
841
1085
|
this.logger.debug(`${LOG_PREFIX} Mounted GCS bucket at ${mountPath}`);
|
|
842
1086
|
break;
|
|
1087
|
+
case "azure-blob":
|
|
1088
|
+
this.logger.debug(`${LOG_PREFIX} Mounting Azure Blob at "${mountPath}"...`);
|
|
1089
|
+
await mountAzure(mountPath, config, mountCtx);
|
|
1090
|
+
this.logger.debug(`${LOG_PREFIX} Mounted Azure Blob container at ${mountPath}`);
|
|
1091
|
+
break;
|
|
843
1092
|
default: {
|
|
844
1093
|
const error = `Unsupported mount type: ${config.type}`;
|
|
845
1094
|
this.mounts.set(mountPath, { filesystem, state: "unsupported", config, error });
|
|
@@ -909,7 +1158,7 @@ var DaytonaSandbox = class _DaytonaSandbox extends workspace.MastraSandbox {
|
|
|
909
1158
|
try {
|
|
910
1159
|
const mountsResult = await runCommand(
|
|
911
1160
|
sandbox,
|
|
912
|
-
`grep -E 'fuse\\.(s3fs|gcsfuse)' /proc/mounts | awk '{print $2}'`,
|
|
1161
|
+
`grep -E 'fuse\\.(s3fs|gcsfuse|blobfuse2)' /proc/mounts | awk '{print $2}'`,
|
|
913
1162
|
{ timeout: MOUNT_COMMAND_TIMEOUT_MS }
|
|
914
1163
|
);
|
|
915
1164
|
currentMounts = mountsResult.output.trim().split("\n").filter((p) => p.length > 0);
|