@mastra/platform-workspace 1.5.0-alpha.4 → 1.5.0-alpha.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mastra/platform-workspace
2
2
 
3
- Mastra Platform workspace provider. Gives agents environment-scoped sandbox execution and bucket-backed filesystem access through the Mastra Platform workspace proxy.
3
+ Mastra Platform workspace provider. It gives agents environment-scoped sandbox execution and bucket-backed filesystem access through the Mastra Platform workspace proxy.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,23 +8,6 @@ Mastra Platform workspace provider. Gives agents environment-scoped sandbox exec
8
8
  npm install @mastra/platform-workspace
9
9
  ```
10
10
 
11
- ## Configuration
12
-
13
- All options can be passed to the constructor or read from environment variables:
14
-
15
- | Option | Env var | Required |
16
- | ----------------- | ------------------------------ | ---------------- |
17
- | `accessToken` | `MASTRA_PLATFORM_ACCESS_TOKEN` | Yes |
18
- | `projectId` | `MASTRA_PROJECT_ID` | Yes |
19
- | `environmentId` | `MASTRA_ENVIRONMENT_ID` | Yes (sandbox) |
20
- | `actingUserId` | — | No (sandbox) |
21
- | `sandboxProvider` | `SANDBOX_PROVIDER` | No (sandbox) |
22
- | `bucketName` | `MASTRA_PLATFORM_BUCKET_NAME` | Yes (filesystem) |
23
-
24
- The sandbox provider resolves from the explicit `sandboxProvider` option, then `SANDBOX_PROVIDER`, then defaults to `e2b`. Set either option to `railway` to use Railway sandboxes. The proxy URL defaults to `https://workspaces.mastra.ai` and can be overridden with the `MASTRA_WORKSPACE_PROXY_URL` env var (useful for staging).
25
-
26
- Requests to the proxy are authenticated with `Authorization: Bearer <accessToken>`. For sandbox requests authenticated with a project access token, set `actingUserId` to the stable opaque user subject from your authentication system. It is sent as `x-acting-user-id` for token partitioning and attribution; it is not an authorization claim.
27
-
28
11
  ## Usage
29
12
 
30
13
  ```typescript
@@ -33,11 +16,8 @@ import { Workspace } from '@mastra/core/workspace';
33
16
  import { PlatformFilesystem, PlatformSandbox } from '@mastra/platform-workspace';
34
17
 
35
18
  const workspace = new Workspace({
36
- filesystem: new PlatformFilesystem({
37
- // accessToken, projectId, bucketName all fall back to env
38
- }),
19
+ filesystem: new PlatformFilesystem({}),
39
20
  sandbox: new PlatformSandbox({
40
- // accessToken, projectId, environmentId all fall back to env
41
21
  idleTimeoutMinutes: 30,
42
22
  networkIsolation: 'ISOLATED',
43
23
  }),
@@ -50,98 +30,24 @@ const agent = new Agent({
50
30
  });
51
31
  ```
52
32
 
53
- ## Filesystem
54
-
55
- `PlatformFilesystem` implements the Mastra filesystem interface against a workspace bucket. Object keys are percent-encoded per segment, so filenames with `?`, `#`, `%`, `&`, `+`, or spaces are preserved end-to-end.
56
-
57
- ```typescript
58
- const fs = new PlatformFilesystem({ bucketName: 'reports' });
59
-
60
- await fs.writeFile('/analyses/repo.md', markdown);
61
- const content = await fs.readFile('/analyses/repo.md');
62
- const entries = await fs.readdir('/analyses');
63
- await fs.moveFile('/analyses/repo.md', '/analyses/repo-final.md');
64
- ```
65
-
66
- Pass `readOnly: true` to mount the bucket read-only. Mutating calls will throw `WorkspaceReadOnlyError`.
67
-
68
- ## Sandbox
69
-
70
- `PlatformSandbox` executes commands inside a provider-backed sandbox tied to a Platform environment. Sessions boot from the configured provider template or checkpoint.
71
-
72
- ```typescript
73
- const sandbox = new PlatformSandbox({ environmentId: 'env_abc' });
74
-
75
- const result = await sandbox.executeCommand('python', ['analyze.py'], {
76
- timeout: 30_000,
77
- env: { INPUT: 'repo' },
78
- });
79
-
80
- console.log(result.stdout);
81
- ```
82
-
83
- Pass an existing `sandboxId` to reattach to a live sandbox instead of creating a new one.
33
+ ## Documentation
84
34
 
85
- ### Reusable templates
35
+ Both providers authenticate through the workspace proxy with `MASTRA_PLATFORM_ACCESS_TOKEN` and `MASTRA_PROJECT_ID`. `PlatformSandbox` also requires `MASTRA_ENVIRONMENT_ID`; `PlatformFilesystem` requires `MASTRA_PLATFORM_BUCKET_NAME`. Constructor values override environment variables, and `MASTRA_WORKSPACE_PROXY_URL` can point requests at a non-production proxy.
86
36
 
87
- Use `Template()` to prebuild a public repository at an immutable commit. `PlatformSandbox` sends the serialized definition to Platform, which content-addresses it and starts or reuses the provider build. Sandbox creation doesn't wait for the build. Platform boots from a prior template in the same family with matching resources when available, otherwise from the provider default, while the requested template builds in the background:
37
+ `PlatformFilesystem` implements the Mastra filesystem interface against a Platform bucket. It supports reading, writing, listing, moving, and deleting files, preserves reserved characters in object names, and can be mounted with `readOnly: true` to reject mutations.
88
38
 
89
- ```typescript
90
- import { PlatformSandbox, Template } from '@mastra/platform-workspace';
91
-
92
- const commitSha = process.env.REPOSITORY_COMMIT_SHA!;
93
- const template = Template()
94
- .cpuCount(4)
95
- .memoryMB(8_192)
96
- .setWorkdir('/workspace/repo')
97
- .setEnvs({ BUILD_CONFIG_MARKER: 'template-v1' })
98
- .aptInstall(['git', 'jq'])
99
- .runCmd('git clone https://github.com/mastra-ai/mastra.git /workspace/repo')
100
- .runCmd(`git checkout ${commitSha}`)
101
- .runCmd('pnpm install --frozen-lockfile');
102
-
103
- const sandbox = new PlatformSandbox({
104
- environmentId: 'env_abc',
105
- sandboxProvider: 'e2b',
106
- template,
107
- });
108
- await sandbox.start();
109
- ```
110
-
111
- Platform serializes the builder and stores build state under a server-derived content hash within the selected environment and provider. Passing the same definition to another sandbox reuses that build. Call `await template.build(options)` to start or reuse the provider build without provisioning a sandbox; it returns `ready`, `pending`, or `failed`. For E2B templates, `cpuCount()` and `memoryMB()` set the resources inherited by sandboxes created from the exact build or a resource-matched stale build. They default to 2 CPUs and 1,024 MB. Effective resource values participate in the template identity, so changing either value creates a distinct template while explicit defaults reuse the omitted-default build. If a pending build falls back to the provider base, that sandbox may use provider-default resources; check `templatePending` to detect this case. Railway currently ignores these two methods because its sandbox template API doesn't expose matching resource settings.
112
-
113
- By default, operation arguments are serialized and sent to Platform. Use `setEnvs(values, { ephemeral: true })` for short-lived build credentials: these values are sent separately, excluded from content identity and persistence, unavailable at runtime, and take precedence over serialized values with the same key. Supply them on every build or fresh provision that may need to build. Railway's provider cache includes transient build variables, so rotating a value may trigger another provider build even though the Platform template ID stays stable.
114
-
115
- ## Errors
39
+ `PlatformSandbox` starts or reconnects to an environment-scoped provider sandbox and implements command execution, lifecycle, and networking operations. The provider defaults to E2B and can be changed to Railway through `sandboxProvider` or `SANDBOX_PROVIDER`. Pass an existing `sandboxId` to reattach to a live sandbox, and `actingUserId` to partition and attribute project-token requests to a stable application user.
116
40
 
117
- Failures from the proxy raise `PlatformApiError`. Structured `{ error: { message, type } }` payloads from the proxy are parsed into `.code` (machine kind) and `.proxyMessage` (human string); the raw response body stays available on `.body`:
118
-
119
- ```typescript
120
- import { PlatformApiError } from '@mastra/platform-workspace';
121
-
122
- try {
123
- await fs.readFile('/missing.txt');
124
- } catch (err) {
125
- if (err instanceof PlatformApiError) {
126
- if (err.code === 'not_found') {
127
- // handle missing file
128
- } else if (err.code === 'authentication_error') {
129
- // refresh token
130
- }
131
- console.error(err.status, err.code, err.proxyMessage, err.body);
132
- }
133
- }
134
- ```
41
+ The exported `Template()` builder creates reusable sandbox images from commands, packages, environment values, repository checkouts, CPU, memory, and working-directory settings. Platform derives a content identity from the serialized template so matching definitions can reuse previous builds. Ephemeral environment values are excluded from that identity and are not persisted into the runtime image.
135
42
 
136
- `code` / `proxyMessage` are `undefined` when the proxy returns a non-JSON body (e.g. an HTML 502 from a load balancer).
43
+ Proxy failures throw `PlatformApiError`, which includes the HTTP status, parsed machine-readable error code, proxy message, and raw response body. Use these fields to distinguish missing resources, authentication failures, and provider errors.
137
44
 
138
- Filesystem-specific errors (`FileNotFoundError`, `FileExistsError`, `WorkspaceReadOnlyError`) are re-exported from `@mastra/core`.
45
+ - [Mastra Platform workspaces](https://mastra.ai/docs/mastra-platform/workspaces)
139
46
 
140
- ### Sandbox exec errors
47
+ ## Changelog
141
48
 
142
- `PlatformSandbox.executeCommand` runs over the direct-exec data plane (a WebSocket straight to the Railway tcp-proxy) and can throw two typed errors on unrecoverable failure:
49
+ See the [package changelog](https://github.com/mastra-ai/mastra/blob/main/workspaces/platform-workspace/CHANGELOG.md) for version history and release notes.
143
50
 
144
- - `SandboxDestroyedError` — the platform returned 410 for `/exec-lease`, meaning the sandbox has been destroyed. The cached sandbox id and lease are cleared, so a reused `PlatformSandbox` instance will re-provision on the next call. Fleet-level code that owns a binding store should catch this, clear the stale sandbox id, and reprovision + replay.
145
- - `SandboxExecTransportError` — both the initial WebSocket attempt and the built-in retry closed without an `exit` frame against a live sandbox. Carries `{ opened, closeCode, closeReason, wsEndpoint }` diagnostics plus `sandboxId`, `command`, and `attempts` so upstream logs / alerts can distinguish "the Railway data plane is broken" from "your command failed".
51
+ ## Support
146
52
 
147
- `PlatformApiError` (with status 404 / 500 / 501 on `/exec-lease`) can also bubble up from `executeCommand` those are configuration or platform errors, not "reprovision me" signals, and are propagated as-is.
53
+ We have an [open community Discord](https://discord.gg/mastra-ai). Come and say hello and let us know if you have any questions or need any help getting things running.
package/dist/index.cjs CHANGED
@@ -1047,9 +1047,9 @@ var SandboxDestroyedError = class extends Error {
1047
1047
  * sandbox.
1048
1048
  */
1049
1049
  function buildCommand(command, args) {
1050
- return args?.length ? `${command} ${args.map(shellQuote).join(" ")}` : command;
1050
+ return args?.length ? `${command} ${args.map(shellQuote$1).join(" ")}` : command;
1051
1051
  }
1052
- function shellQuote(arg) {
1052
+ function shellQuote$1(arg) {
1053
1053
  if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
1054
1054
  return `'${arg.replace(/'/g, `'\\''`)}'`;
1055
1055
  }
@@ -1519,14 +1519,15 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1519
1519
  * sandbox they can't safely retry.
1520
1520
  *
1521
1521
  * Railway requires a caller-supplied recovery `id` before it can have a
1522
- * checkpoint to delete. E2B also permits capture with the automatic id, so
1523
- * destroy releases that named snapshot even when no recovery id was supplied.
1522
+ * checkpoint to delete. On E2B the sandbox itself is the persistent thing
1523
+ * (idle sandboxes pause and resume in place), so destroy only kills the
1524
+ * VM; any snapshot a caller captured on purpose is the caller's to manage.
1524
1525
  */
1525
1526
  async destroy() {
1526
1527
  if (!this._sandboxId) return;
1527
1528
  const destroyedSandboxId = this._sandboxId;
1528
1529
  this._captureInFlight = null;
1529
- if (this._hasRecoveryKey || this._client.sandboxProvider === "e2b") try {
1530
+ if (this._hasRecoveryKey && this._client.sandboxProvider !== "e2b") try {
1530
1531
  await this._request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}/checkpoint`, {
1531
1532
  method: "DELETE",
1532
1533
  headers: { "content-type": "application/json" },
@@ -1989,6 +1990,16 @@ function setupMarkerContent(setupCommand) {
1989
1990
  function setupMarkerCommand(content) {
1990
1991
  return `mkdir -p "$(dirname "${SETUP_MARKER_PATH}")" && printf '%s' '${content}' > "${SETUP_MARKER_PATH}"`;
1991
1992
  }
1993
+ function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
1994
+ return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
1995
+ }
1996
+ /** Per-invocation auth header; `-c` config never reaches `.git/config`. */
1997
+ function gitAuthFlag$1(tokenEnv) {
1998
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
1999
+ }
2000
+ function shellQuote(value) {
2001
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2002
+ }
1992
2003
  //#endregion
1993
2004
  //#region src/repo-template.ts
1994
2005
  const execFileAsync = (0, util.promisify)(child_process.execFile);
@@ -2070,7 +2081,11 @@ function createRepoTemplate(options) {
2070
2081
  if (Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv, { ephemeral: true });
2071
2082
  template = withResources(template, options);
2072
2083
  if (workingDirectory) template = template.runCmd(`mkdir -p "${workingDirectory}"`).setWorkdir(workingDirectory);
2073
- template = template.runCmd(`git ${auth}clone ${cloneUrl} "${repoDir}"`).runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2084
+ template = template.runCmd(repoCloneCommand({
2085
+ cloneUrl,
2086
+ destination: repoDir,
2087
+ ...token ? { tokenEnv: BUILD_TOKEN_ENV } : {}
2088
+ })).runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2074
2089
  for (const command of setupCommands) template = template.runCmd(`cd "${repoDir}" && ${command}`);
2075
2090
  template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands)));
2076
2091
  return template.withFamily(family);