@mastra/platform-workspace 1.0.0 → 1.1.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 +76 -0
- package/dist/address-registry.d.ts +47 -0
- package/dist/address-registry.d.ts.map +1 -0
- package/dist/index.cjs +314 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +312 -3
- package/dist/index.js.map +1 -1
- package/dist/private-net-exec.d.ts +114 -0
- package/dist/private-net-exec.d.ts.map +1 -0
- package/dist/sandbox.d.ts +96 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# @mastra/platform
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `PlatformSandbox.executeCommand` can now dial the sandbox directly over Railway's private network instead of going through the platform's public exec proxy. On paths where the direct route is available, per-exec latency drops from ~400 ms p50 to ~16 ms p50, and the exec stops touching the platform control plane. This flows through to every filesystem call (`SandboxFilesystem.readFile`, `writeFile`, `readdir`, `mkdir`, `stat`, `exists`, `copyFile`, `moveFile`, `deleteFile`), which is where most agent tool time was going. ([#20664](https://github.com/mastra-ai/mastra/pull/20664))
|
|
8
|
+
|
|
9
|
+
Direct-path availability is a runtime property, not a configuration knob. When it's not available — no address registry wired up, the workspace-proxy hasn't discovered the sandbox address yet, or a direct dial fails — `executeCommand` transparently falls back to the existing exec-lease path with no behavior change. Timed-out execs are never retried on the fallback path (they're returned to the caller as-is), so this is safe for non-idempotent commands.
|
|
10
|
+
|
|
11
|
+
### Enabling the direct path
|
|
12
|
+
|
|
13
|
+
Wire a `SandboxAddressRegistry` into `PlatformSandbox`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { PlatformSandbox, InProcessSandboxAddressRegistry } from '@mastra/platform-workspace';
|
|
17
|
+
|
|
18
|
+
const registry = new InProcessSandboxAddressRegistry();
|
|
19
|
+
|
|
20
|
+
const sandbox = new PlatformSandbox({
|
|
21
|
+
accessToken: process.env.MASTRA_PLATFORM_ACCESS_TOKEN,
|
|
22
|
+
projectId: process.env.MASTRA_PLATFORM_PROJECT_ID,
|
|
23
|
+
environmentId: process.env.MASTRA_PLATFORM_ENVIRONMENT_ID,
|
|
24
|
+
addressRegistry: registry,
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`PlatformSandbox.start()` populates the registry from the workspace-proxy's response; `executeCommand` reads it, tries the direct path first, evicts on transport failure. `destroy()` also evicts. `clone()` shares the same registry — each child sandbox looks up its own id.
|
|
29
|
+
|
|
30
|
+
### New public exports
|
|
31
|
+
- `SandboxAddressRegistry` — the `{ get, set, delete }` interface `PlatformSandbox` sees. Callers can implement their own (e.g. shared across a worker pool) or use the default.
|
|
32
|
+
- `InProcessSandboxAddressRegistry` — the default `Map`-backed implementation.
|
|
33
|
+
- `PlatformSandboxOptions.addressRegistry?` — DI seam. Optional; omitted keeps pre-existing behavior.
|
|
34
|
+
- `execViaPrivateNetwork`, `PrivateNetExecHttpError`, `PrivateNetExecOptions`, `PrivateNetExecResult`, `PrivateNetFetch` — standalone transport for callers that want to talk to a sandbox directly without going through `PlatformSandbox`.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [[`8d2399b`](https://github.com/mastra-ai/mastra/commit/8d2399b638f8e0945cf2cda0187dbea8dcf0b784), [`c8002da`](https://github.com/mastra-ai/mastra/commit/c8002da7775c468e2965b6ff5f82045450fa8cb9), [`92be47f`](https://github.com/mastra-ai/mastra/commit/92be47fbd26ffccec0e2131ef7c1d9e70dd5ef4a), [`89200ba`](https://github.com/mastra-ai/mastra/commit/89200bafa05444bb7949b363ce7b743e29867561), [`c950138`](https://github.com/mastra-ai/mastra/commit/c950138e72e4f317a40187e3800588731ab790ce), [`810c7e7`](https://github.com/mastra-ai/mastra/commit/810c7e74929989d8b8b5db52cd3af22cd0998af4), [`063c8b2`](https://github.com/mastra-ai/mastra/commit/063c8b2eb14e4e5ca021779bc33e8c3c031c8604), [`f9f9884`](https://github.com/mastra-ai/mastra/commit/f9f98848ee194dc71a787a709ec430b065cdc41b), [`e0904dc`](https://github.com/mastra-ai/mastra/commit/e0904dc538792e54e1806b70172e5900ac49bff4), [`9672fab`](https://github.com/mastra-ai/mastra/commit/9672fabfbcadb961a35c22a2d6722e077f7b24b9), [`f4e964c`](https://github.com/mastra-ai/mastra/commit/f4e964cad57057301d6bed5c55bcdd730175b941), [`1f7bbd7`](https://github.com/mastra-ai/mastra/commit/1f7bbd7785a8d230aad02454ecabeb4a0b2cc96f), [`e47ff36`](https://github.com/mastra-ai/mastra/commit/e47ff36945720f4ee4caa09f6e83514d7d188608), [`64d6781`](https://github.com/mastra-ai/mastra/commit/64d67814bccddd314f7e09643243821e57cb87b6), [`fb9a6ac`](https://github.com/mastra-ai/mastra/commit/fb9a6ac11c9560518742ece60b49d6b062845fd3), [`aa2cec8`](https://github.com/mastra-ai/mastra/commit/aa2cec8501f634d51c2f3ebfb3dd3aa7af8d2ca2), [`c848e65`](https://github.com/mastra-ai/mastra/commit/c848e655a64ff10331a8ceafafe7f18e70a0f092), [`2adf8eb`](https://github.com/mastra-ai/mastra/commit/2adf8eb4a70ed2b6cff2dd39281496ea0e025fac), [`0494489`](https://github.com/mastra-ai/mastra/commit/049448906e4c3d2d615bbe865b073a0d890ddb7c), [`8d1aeb8`](https://github.com/mastra-ai/mastra/commit/8d1aeb8acf7c20c4bb8e4d8e4bdc6569c83ac561), [`8264611`](https://github.com/mastra-ai/mastra/commit/8264611510e421b818bc7395dc2ae4d9c2d518b2), [`d8fa243`](https://github.com/mastra-ai/mastra/commit/d8fa2430d21113e330c4e676ac65e1235cf44f81), [`44fc98b`](https://github.com/mastra-ai/mastra/commit/44fc98b9d1242aa87a3ab44bdce9e9f12c44d8c9), [`f933ba3`](https://github.com/mastra-ai/mastra/commit/f933ba32700e1d0bf143311c1a08f88300b840b6), [`83065bf`](https://github.com/mastra-ai/mastra/commit/83065bfee9e47c3c6f09132a9034501f6cfb69cf), [`0f2ef41`](https://github.com/mastra-ai/mastra/commit/0f2ef4118da022e4f30dac4e9856cc3a8c97671c), [`01b162f`](https://github.com/mastra-ai/mastra/commit/01b162fe435295881aa7ea55f1759407ad5175ad)]:
|
|
39
|
+
- @mastra/core@1.57.0
|
|
40
|
+
|
|
41
|
+
## 1.1.0-alpha.0
|
|
42
|
+
|
|
43
|
+
### Minor Changes
|
|
44
|
+
|
|
45
|
+
- `PlatformSandbox.executeCommand` can now dial the sandbox directly over Railway's private network instead of going through the platform's public exec proxy. On paths where the direct route is available, per-exec latency drops from ~400 ms p50 to ~16 ms p50, and the exec stops touching the platform control plane. This flows through to every filesystem call (`SandboxFilesystem.readFile`, `writeFile`, `readdir`, `mkdir`, `stat`, `exists`, `copyFile`, `moveFile`, `deleteFile`), which is where most agent tool time was going. ([#20664](https://github.com/mastra-ai/mastra/pull/20664))
|
|
46
|
+
|
|
47
|
+
Direct-path availability is a runtime property, not a configuration knob. When it's not available — no address registry wired up, the workspace-proxy hasn't discovered the sandbox address yet, or a direct dial fails — `executeCommand` transparently falls back to the existing exec-lease path with no behavior change. Timed-out execs are never retried on the fallback path (they're returned to the caller as-is), so this is safe for non-idempotent commands.
|
|
48
|
+
|
|
49
|
+
### Enabling the direct path
|
|
50
|
+
|
|
51
|
+
Wire a `SandboxAddressRegistry` into `PlatformSandbox`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { PlatformSandbox, InProcessSandboxAddressRegistry } from '@mastra/platform-workspace';
|
|
55
|
+
|
|
56
|
+
const registry = new InProcessSandboxAddressRegistry();
|
|
57
|
+
|
|
58
|
+
const sandbox = new PlatformSandbox({
|
|
59
|
+
accessToken: process.env.MASTRA_PLATFORM_ACCESS_TOKEN,
|
|
60
|
+
projectId: process.env.MASTRA_PLATFORM_PROJECT_ID,
|
|
61
|
+
environmentId: process.env.MASTRA_PLATFORM_ENVIRONMENT_ID,
|
|
62
|
+
addressRegistry: registry,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`PlatformSandbox.start()` populates the registry from the workspace-proxy's response; `executeCommand` reads it, tries the direct path first, evicts on transport failure. `destroy()` also evicts. `clone()` shares the same registry — each child sandbox looks up its own id.
|
|
67
|
+
|
|
68
|
+
### New public exports
|
|
69
|
+
- `SandboxAddressRegistry` — the `{ get, set, delete }` interface `PlatformSandbox` sees. Callers can implement their own (e.g. shared across a worker pool) or use the default.
|
|
70
|
+
- `InProcessSandboxAddressRegistry` — the default `Map`-backed implementation.
|
|
71
|
+
- `PlatformSandboxOptions.addressRegistry?` — DI seam. Optional; omitted keeps pre-existing behavior.
|
|
72
|
+
- `execViaPrivateNetwork`, `PrivateNetExecHttpError`, `PrivateNetExecOptions`, `PrivateNetExecResult`, `PrivateNetFetch` — standalone transport for callers that want to talk to a sandbox directly without going through `PlatformSandbox`.
|
|
73
|
+
|
|
74
|
+
### Patch Changes
|
|
75
|
+
|
|
76
|
+
- Updated dependencies [[`89200ba`](https://github.com/mastra-ai/mastra/commit/89200bafa05444bb7949b363ce7b743e29867561), [`c950138`](https://github.com/mastra-ai/mastra/commit/c950138e72e4f317a40187e3800588731ab790ce), [`063c8b2`](https://github.com/mastra-ai/mastra/commit/063c8b2eb14e4e5ca021779bc33e8c3c031c8604), [`f4e964c`](https://github.com/mastra-ai/mastra/commit/f4e964cad57057301d6bed5c55bcdd730175b941), [`1f7bbd7`](https://github.com/mastra-ai/mastra/commit/1f7bbd7785a8d230aad02454ecabeb4a0b2cc96f), [`e47ff36`](https://github.com/mastra-ai/mastra/commit/e47ff36945720f4ee4caa09f6e83514d7d188608), [`fb9a6ac`](https://github.com/mastra-ai/mastra/commit/fb9a6ac11c9560518742ece60b49d6b062845fd3), [`aa2cec8`](https://github.com/mastra-ai/mastra/commit/aa2cec8501f634d51c2f3ebfb3dd3aa7af8d2ca2), [`2adf8eb`](https://github.com/mastra-ai/mastra/commit/2adf8eb4a70ed2b6cff2dd39281496ea0e025fac), [`8264611`](https://github.com/mastra-ai/mastra/commit/8264611510e421b818bc7395dc2ae4d9c2d518b2), [`44fc98b`](https://github.com/mastra-ai/mastra/commit/44fc98b9d1242aa87a3ab44bdce9e9f12c44d8c9), [`0f2ef41`](https://github.com/mastra-ai/mastra/commit/0f2ef4118da022e4f30dac4e9856cc3a8c97671c)]:
|
|
77
|
+
- @mastra/core@1.57.0-alpha.1
|
|
78
|
+
|
|
3
79
|
## 1.0.0
|
|
4
80
|
|
|
5
81
|
### Major Changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process `sandboxId → instanceUrl` registry populated by
|
|
3
|
+
* {@link PlatformSandbox.start} from the `instanceUrl` field workspace-proxy
|
|
4
|
+
* includes on create + get responses, and consumed by
|
|
5
|
+
* {@link PlatformSandbox.executeCommand} on the private-network fast path.
|
|
6
|
+
*
|
|
7
|
+
* The registry lives in the same Node process as the `PlatformSandbox`
|
|
8
|
+
* consumer — for shipyard, that is the Mastra runtime deployed from
|
|
9
|
+
* `mastracode/web`. There is no receiver route and no cross-service dance;
|
|
10
|
+
* the address is just a field copied from a response the runtime already
|
|
11
|
+
* receives.
|
|
12
|
+
*
|
|
13
|
+
* State intentionally does not persist:
|
|
14
|
+
*
|
|
15
|
+
* - The IPv6 rotates on every sandbox recreate.
|
|
16
|
+
* - The URL has no meaning after the sandbox is destroyed.
|
|
17
|
+
* - The live sandbox binding, session context, and lease all die with the
|
|
18
|
+
* runtime process; the address dying with them is correct.
|
|
19
|
+
* - The proxy's `environment_sandboxes.instance_url` column is the durable
|
|
20
|
+
* source of truth — a runtime restart re-populates the registry on the
|
|
21
|
+
* next `start()` / reattach from the proxy's response.
|
|
22
|
+
*/
|
|
23
|
+
import type { SandboxAddressRegistry } from './sandbox.js';
|
|
24
|
+
/**
|
|
25
|
+
* Concrete in-process {@link SandboxAddressRegistry}. Backed by a `Map`; no
|
|
26
|
+
* eviction policy, no TTL — entries live until an observed transport failure
|
|
27
|
+
* calls `delete`, until the sandbox is explicitly destroyed, or until the
|
|
28
|
+
* process exits.
|
|
29
|
+
*/
|
|
30
|
+
export declare class InProcessSandboxAddressRegistry implements SandboxAddressRegistry {
|
|
31
|
+
#private;
|
|
32
|
+
get(sandboxId: string): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Populate or overwrite the address for a sandbox. Called by
|
|
35
|
+
* {@link PlatformSandbox.start} on every fresh provision and every reattach;
|
|
36
|
+
* overwriting is intentional so a re-provision with a fresh IPv6 heals the
|
|
37
|
+
* map without a branch.
|
|
38
|
+
*/
|
|
39
|
+
set(sandboxId: string, instanceUrl: string): void;
|
|
40
|
+
delete(sandboxId: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Test-only introspection. Not part of {@link SandboxAddressRegistry} —
|
|
43
|
+
* production callers must not read the registry as a whole.
|
|
44
|
+
*/
|
|
45
|
+
get size(): number;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=address-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address-registry.d.ts","sourceRoot":"","sources":["../src/address-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D;;;;;GAKG;AACH,qBAAa,+BAAgC,YAAW,sBAAsB;;IAG5E,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI1C;;;;;OAKG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAIjD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI/B;;;OAGG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
package/dist/index.cjs
CHANGED
|
@@ -512,6 +512,175 @@ function execViaLease(lease, options) {
|
|
|
512
512
|
});
|
|
513
513
|
}
|
|
514
514
|
//#endregion
|
|
515
|
+
//#region src/private-net-exec.ts
|
|
516
|
+
/**
|
|
517
|
+
* Thrown by {@link execViaPrivateNetwork} when the sidecar returns a non-2xx
|
|
518
|
+
* HTTP response. This is an *application* error, not a transport error — the
|
|
519
|
+
* sidecar is reachable and answered, it just refused the exec. Callers should
|
|
520
|
+
* fall back to the lease path for this one call but MUST NOT invalidate the
|
|
521
|
+
* cached `instanceUrl` (the address is still good).
|
|
522
|
+
*/
|
|
523
|
+
var PrivateNetExecHttpError = class extends Error {
|
|
524
|
+
status;
|
|
525
|
+
body;
|
|
526
|
+
constructor(status, body) {
|
|
527
|
+
super(`Sidecar /exec returned ${status}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
528
|
+
this.name = "PrivateNetExecHttpError";
|
|
529
|
+
this.status = status;
|
|
530
|
+
this.body = body;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
const DEFAULT_FETCH = (input, init) => {
|
|
534
|
+
const f = globalThis.fetch;
|
|
535
|
+
if (!f) throw new Error("Private-network exec requires a fetch implementation. Node 22+ provides one globally; on older runtimes, pass fetch explicitly.");
|
|
536
|
+
return f(input, init);
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* Dial `${instanceUrl}/exec` and stream the response, resolving with the
|
|
540
|
+
* accumulated stdout/stderr + exit code.
|
|
541
|
+
*
|
|
542
|
+
* Errors:
|
|
543
|
+
* - Connection failure (DNS, refused, reset) → resolves with
|
|
544
|
+
* `{opened:false, exitCode:null, transportErrorMessage}`. Never throws for
|
|
545
|
+
* transport failures — the shape matches the lease-path result so the
|
|
546
|
+
* caller can treat both transports uniformly.
|
|
547
|
+
* - Non-2xx HTTP response from the sidecar → throws {@link PrivateNetExecHttpError}.
|
|
548
|
+
* Application-level; caller decides whether to fall back.
|
|
549
|
+
* - Stream ends without an `exit` frame → resolves with
|
|
550
|
+
* `{opened:true, exitCode:null}`, matching the lease-path semantics for a
|
|
551
|
+
* mid-stream drop.
|
|
552
|
+
* - `timeoutMs` elapsed → aborts the request, resolves with
|
|
553
|
+
* `{timedOut:true, exitCode:124}`.
|
|
554
|
+
*/
|
|
555
|
+
async function execViaPrivateNetwork(instanceUrl, options) {
|
|
556
|
+
const fetchImpl = options.fetch ?? DEFAULT_FETCH;
|
|
557
|
+
const url = `${instanceUrl.replace(/\/$/, "")}/exec`;
|
|
558
|
+
const controller = new AbortController();
|
|
559
|
+
let timedOut = false;
|
|
560
|
+
let timeoutTimer;
|
|
561
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) timeoutTimer = setTimeout(() => {
|
|
562
|
+
timedOut = true;
|
|
563
|
+
controller.abort();
|
|
564
|
+
}, options.timeoutMs);
|
|
565
|
+
const body = { command: options.command };
|
|
566
|
+
if (options.cwd) body.cwd = options.cwd;
|
|
567
|
+
if (options.env && Object.keys(options.env).length > 0) body.env = options.env;
|
|
568
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) body.timeoutMs = options.timeoutMs;
|
|
569
|
+
const headers = { "content-type": "application/json" };
|
|
570
|
+
if (options.bearerToken) headers.authorization = `Bearer ${options.bearerToken}`;
|
|
571
|
+
let response;
|
|
572
|
+
try {
|
|
573
|
+
response = await fetchImpl(url, {
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers,
|
|
576
|
+
body: JSON.stringify(body),
|
|
577
|
+
signal: controller.signal
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
581
|
+
if (timedOut) return {
|
|
582
|
+
exitCode: 124,
|
|
583
|
+
stdout: "",
|
|
584
|
+
stderr: "",
|
|
585
|
+
timedOut: true,
|
|
586
|
+
opened: false
|
|
587
|
+
};
|
|
588
|
+
return {
|
|
589
|
+
exitCode: null,
|
|
590
|
+
stdout: "",
|
|
591
|
+
stderr: "",
|
|
592
|
+
timedOut: false,
|
|
593
|
+
opened: false,
|
|
594
|
+
transportErrorMessage: error instanceof Error ? error.message : String(error)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (!response.ok) {
|
|
598
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
599
|
+
const text = await response.text().catch(() => "");
|
|
600
|
+
throw new PrivateNetExecHttpError(response.status, text);
|
|
601
|
+
}
|
|
602
|
+
if (!response.body) {
|
|
603
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
604
|
+
return {
|
|
605
|
+
exitCode: null,
|
|
606
|
+
stdout: "",
|
|
607
|
+
stderr: "",
|
|
608
|
+
timedOut: false,
|
|
609
|
+
opened: true,
|
|
610
|
+
status: response.status
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
let stdout = "";
|
|
614
|
+
let stderr = "";
|
|
615
|
+
let exitCode = null;
|
|
616
|
+
const decoder = new TextDecoder();
|
|
617
|
+
let buffer = "";
|
|
618
|
+
const handleLine = (line) => {
|
|
619
|
+
if (!line) return;
|
|
620
|
+
let frame;
|
|
621
|
+
try {
|
|
622
|
+
frame = JSON.parse(line);
|
|
623
|
+
} catch {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (!frame || typeof frame !== "object") return;
|
|
627
|
+
if (frame.type === "stdout" && typeof frame.data === "string") {
|
|
628
|
+
stdout += frame.data;
|
|
629
|
+
options.onStdout?.(frame.data);
|
|
630
|
+
} else if (frame.type === "stderr" && typeof frame.data === "string") {
|
|
631
|
+
stderr += frame.data;
|
|
632
|
+
options.onStderr?.(frame.data);
|
|
633
|
+
} else if (frame.type === "exit" && typeof frame.code === "number") exitCode = frame.code;
|
|
634
|
+
};
|
|
635
|
+
try {
|
|
636
|
+
const reader = response.body.getReader();
|
|
637
|
+
while (true) {
|
|
638
|
+
const { value, done } = await reader.read();
|
|
639
|
+
if (done) break;
|
|
640
|
+
buffer += decoder.decode(value, { stream: true });
|
|
641
|
+
let newlineIdx = buffer.indexOf("\n");
|
|
642
|
+
while (newlineIdx !== -1) {
|
|
643
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
644
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
645
|
+
handleLine(line);
|
|
646
|
+
newlineIdx = buffer.indexOf("\n");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
buffer += decoder.decode();
|
|
650
|
+
const trailing = buffer.trim();
|
|
651
|
+
if (trailing) handleLine(trailing);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
654
|
+
if (timedOut) return {
|
|
655
|
+
exitCode: 124,
|
|
656
|
+
stdout,
|
|
657
|
+
stderr,
|
|
658
|
+
timedOut: true,
|
|
659
|
+
opened: true,
|
|
660
|
+
status: response.status
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
exitCode,
|
|
664
|
+
stdout,
|
|
665
|
+
stderr,
|
|
666
|
+
timedOut: false,
|
|
667
|
+
opened: true,
|
|
668
|
+
status: response.status,
|
|
669
|
+
transportErrorMessage: error instanceof Error ? error.message : String(error)
|
|
670
|
+
};
|
|
671
|
+
} finally {
|
|
672
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
exitCode,
|
|
676
|
+
stdout,
|
|
677
|
+
stderr,
|
|
678
|
+
timedOut: false,
|
|
679
|
+
opened: true,
|
|
680
|
+
status: response.status
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
515
684
|
//#region src/sandbox.ts
|
|
516
685
|
/**
|
|
517
686
|
* How long before a lease's stated `expiresAt` we should treat it as
|
|
@@ -662,6 +831,19 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
662
831
|
_instructionsOverride;
|
|
663
832
|
_createdAt = null;
|
|
664
833
|
_webSocketFactory;
|
|
834
|
+
_privateNetFetch;
|
|
835
|
+
/**
|
|
836
|
+
* Registry that maps `sandboxId → instanceUrl` for the private-network
|
|
837
|
+
* exec path. Injected by the composition site via
|
|
838
|
+
* {@link PlatformSandboxOptions.addressRegistry} and populated by this
|
|
839
|
+
* class itself in `start()` when the workspace-proxy's create/reattach
|
|
840
|
+
* response includes an `instanceUrl` field. The registry IS the cache —
|
|
841
|
+
* there is no per-instance mirror on `PlatformSandbox`, so every exec is
|
|
842
|
+
* a `Map.get()` (in the default in-process impl) against the live view.
|
|
843
|
+
* When absent, executes go straight to the lease path with no extra
|
|
844
|
+
* round-trip.
|
|
845
|
+
*/
|
|
846
|
+
_addressRegistry;
|
|
665
847
|
/**
|
|
666
848
|
* Cached exec lease for this sandbox. `null` before the first exec and
|
|
667
849
|
* after {@link destroy}. Refreshed when `expiresAt - LEASE_REFRESH_MARGIN_MS < now`
|
|
@@ -693,6 +875,8 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
693
875
|
this._timeout = options.timeout;
|
|
694
876
|
this._instructionsOverride = options.instructions;
|
|
695
877
|
this._webSocketFactory = options.webSocketFactory;
|
|
878
|
+
this._privateNetFetch = options.privateNetFetch;
|
|
879
|
+
this._addressRegistry = options.addressRegistry;
|
|
696
880
|
}
|
|
697
881
|
generateId() {
|
|
698
882
|
return `platform-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -723,7 +907,9 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
723
907
|
env: options.env ?? this._env,
|
|
724
908
|
...this._timeout !== void 0 && { timeout: this._timeout },
|
|
725
909
|
...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride },
|
|
726
|
-
...this._webSocketFactory !== void 0 && { webSocketFactory: this._webSocketFactory }
|
|
910
|
+
...this._webSocketFactory !== void 0 && { webSocketFactory: this._webSocketFactory },
|
|
911
|
+
...this._privateNetFetch !== void 0 && { privateNetFetch: this._privateNetFetch },
|
|
912
|
+
...this._addressRegistry !== void 0 && { addressRegistry: this._addressRegistry }
|
|
727
913
|
});
|
|
728
914
|
}
|
|
729
915
|
async start() {
|
|
@@ -731,6 +917,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
731
917
|
const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
|
|
732
918
|
if (!json.destroyedAt) {
|
|
733
919
|
this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
|
|
920
|
+
this._populateAddressFromResponse(json);
|
|
734
921
|
return;
|
|
735
922
|
}
|
|
736
923
|
this._sandboxId = void 0;
|
|
@@ -761,16 +948,40 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
761
948
|
const json = await response.json();
|
|
762
949
|
this._sandboxId = json.id;
|
|
763
950
|
this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
|
|
951
|
+
this._populateAddressFromResponse(json);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Copy `response.instanceUrl` into the injected {@link SandboxAddressRegistry}
|
|
955
|
+
* when both are present. Called from both {@link start} branches (fresh
|
|
956
|
+
* provision + reattach) with the workspace-proxy response for this sandbox.
|
|
957
|
+
*
|
|
958
|
+
* The proxy discovers the IPv6 during `Sandbox.create()` and stores it in
|
|
959
|
+
* `environment_sandboxes.instance_url`; both the create response and
|
|
960
|
+
* `GET /sandbox/:id` echo the same field. The runtime does not do any
|
|
961
|
+
* discovery of its own — it only mirrors the field into an in-process map
|
|
962
|
+
* so {@link executeCommand} can `Map.get()` before every exec without an
|
|
963
|
+
* HTTP round-trip.
|
|
964
|
+
*
|
|
965
|
+
* `null`/absent `instanceUrl` (proxy discovery failed, or an older proxy
|
|
966
|
+
* that predates the field) leaves the registry untouched — executes fall
|
|
967
|
+
* through to the lease path with no branch here.
|
|
968
|
+
*/
|
|
969
|
+
_populateAddressFromResponse(json) {
|
|
970
|
+
if (!this._addressRegistry) return;
|
|
971
|
+
if (!json.instanceUrl) return;
|
|
972
|
+
this._addressRegistry.set(json.id, json.instanceUrl);
|
|
764
973
|
}
|
|
765
974
|
async stop() {
|
|
766
975
|
await this.destroy();
|
|
767
976
|
}
|
|
768
977
|
async destroy() {
|
|
769
978
|
if (!this._sandboxId) return;
|
|
770
|
-
|
|
979
|
+
const destroyedSandboxId = this._sandboxId;
|
|
980
|
+
await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
|
|
771
981
|
this._sandboxId = void 0;
|
|
772
982
|
this._createdAt = null;
|
|
773
983
|
this._lease = null;
|
|
984
|
+
this._addressRegistry?.delete(destroyedSandboxId);
|
|
774
985
|
}
|
|
775
986
|
/**
|
|
776
987
|
* Execute a command on the remote sandbox.
|
|
@@ -795,6 +1006,22 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
795
1006
|
const started = Date.now();
|
|
796
1007
|
const fullCommand = buildCommand(command, args);
|
|
797
1008
|
const effectiveTimeout = options?.timeout ?? this._timeout;
|
|
1009
|
+
const instanceUrl = this._addressRegistry?.get(this._sandboxId);
|
|
1010
|
+
if (instanceUrl) {
|
|
1011
|
+
const privateNet = await this._tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options);
|
|
1012
|
+
if (privateNet) {
|
|
1013
|
+
const privateExit = privateNet.exitCode ?? 124;
|
|
1014
|
+
return {
|
|
1015
|
+
success: privateExit === 0,
|
|
1016
|
+
exitCode: privateExit,
|
|
1017
|
+
stdout: privateNet.stdout,
|
|
1018
|
+
stderr: privateNet.stderr,
|
|
1019
|
+
timedOut: privateNet.timedOut,
|
|
1020
|
+
command: fullCommand,
|
|
1021
|
+
executionTimeMs: Date.now() - started
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
798
1025
|
const result = await this._runDirectExec(fullCommand, effectiveTimeout, options);
|
|
799
1026
|
const exitCode = result.exitCode ?? 124;
|
|
800
1027
|
return {
|
|
@@ -877,6 +1104,55 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
877
1104
|
});
|
|
878
1105
|
}
|
|
879
1106
|
/**
|
|
1107
|
+
* Try to run the exec against the in-sandbox sidecar over Railway's private
|
|
1108
|
+
* network. Returns the result on success (including non-zero exit codes and
|
|
1109
|
+
* timeouts — those are real command results, not failures). Returns
|
|
1110
|
+
* `undefined` when the caller should fall back to the lease path:
|
|
1111
|
+
*
|
|
1112
|
+
* - Transport failure (connection refused, mid-stream drop, no `exit`
|
|
1113
|
+
* frame). The registry entry is evicted so subsequent execs skip the
|
|
1114
|
+
* private-net dial until the sidecar re-registers.
|
|
1115
|
+
* - Sidecar answered with a non-2xx HTTP status. Registry is left intact —
|
|
1116
|
+
* the address is still valid; something else is wrong (bad request,
|
|
1117
|
+
* sidecar bug). Only this specific exec falls back.
|
|
1118
|
+
*/
|
|
1119
|
+
async _tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options) {
|
|
1120
|
+
const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
|
|
1121
|
+
const execOptions = {
|
|
1122
|
+
command: fullCommand,
|
|
1123
|
+
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
1124
|
+
...filteredEnv !== void 0 && { env: filteredEnv },
|
|
1125
|
+
...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
|
|
1126
|
+
...this._privateNetFetch && { fetch: this._privateNetFetch }
|
|
1127
|
+
};
|
|
1128
|
+
let result;
|
|
1129
|
+
try {
|
|
1130
|
+
result = await execViaPrivateNetwork(instanceUrl, execOptions);
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
if (error instanceof PrivateNetExecHttpError) return;
|
|
1133
|
+
this._invalidateAddress();
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (result.timedOut) {
|
|
1137
|
+
if (!result.opened) this._invalidateAddress();
|
|
1138
|
+
return result;
|
|
1139
|
+
}
|
|
1140
|
+
if (!result.opened || result.exitCode === null) {
|
|
1141
|
+
this._invalidateAddress();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Evict this sandbox's entry from the address registry after an observed
|
|
1148
|
+
* transport failure. The entry stays gone until the next start() re-reads
|
|
1149
|
+
* `instanceUrl` from a workspace-proxy response — until then, execs skip
|
|
1150
|
+
* the private-net dial and go straight to the lease path.
|
|
1151
|
+
*/
|
|
1152
|
+
_invalidateAddress() {
|
|
1153
|
+
if (this._sandboxId) this._addressRegistry?.delete(this._sandboxId);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
880
1156
|
* Return a cached exec lease, minting a fresh one when the cache is empty
|
|
881
1157
|
* or the JWT is within {@link LEASE_REFRESH_MARGIN_MS} of `expiresAt`.
|
|
882
1158
|
*
|
|
@@ -1018,12 +1294,48 @@ const platformFilesystemProvider = {
|
|
|
1018
1294
|
createFilesystem: (config) => new PlatformFilesystem(config)
|
|
1019
1295
|
};
|
|
1020
1296
|
//#endregion
|
|
1297
|
+
//#region src/address-registry.ts
|
|
1298
|
+
/**
|
|
1299
|
+
* Concrete in-process {@link SandboxAddressRegistry}. Backed by a `Map`; no
|
|
1300
|
+
* eviction policy, no TTL — entries live until an observed transport failure
|
|
1301
|
+
* calls `delete`, until the sandbox is explicitly destroyed, or until the
|
|
1302
|
+
* process exits.
|
|
1303
|
+
*/
|
|
1304
|
+
var InProcessSandboxAddressRegistry = class {
|
|
1305
|
+
#map = /* @__PURE__ */ new Map();
|
|
1306
|
+
get(sandboxId) {
|
|
1307
|
+
return this.#map.get(sandboxId);
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Populate or overwrite the address for a sandbox. Called by
|
|
1311
|
+
* {@link PlatformSandbox.start} on every fresh provision and every reattach;
|
|
1312
|
+
* overwriting is intentional so a re-provision with a fresh IPv6 heals the
|
|
1313
|
+
* map without a branch.
|
|
1314
|
+
*/
|
|
1315
|
+
set(sandboxId, instanceUrl) {
|
|
1316
|
+
this.#map.set(sandboxId, instanceUrl);
|
|
1317
|
+
}
|
|
1318
|
+
delete(sandboxId) {
|
|
1319
|
+
this.#map.delete(sandboxId);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Test-only introspection. Not part of {@link SandboxAddressRegistry} —
|
|
1323
|
+
* production callers must not read the registry as a whole.
|
|
1324
|
+
*/
|
|
1325
|
+
get size() {
|
|
1326
|
+
return this.#map.size;
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
//#endregion
|
|
1330
|
+
exports.InProcessSandboxAddressRegistry = InProcessSandboxAddressRegistry;
|
|
1021
1331
|
exports.PlatformApiError = PlatformApiError;
|
|
1022
1332
|
exports.PlatformClient = PlatformClient;
|
|
1023
1333
|
exports.PlatformFilesystem = PlatformFilesystem;
|
|
1024
1334
|
exports.PlatformSandbox = PlatformSandbox;
|
|
1335
|
+
exports.PrivateNetExecHttpError = PrivateNetExecHttpError;
|
|
1025
1336
|
exports.SandboxDestroyedError = SandboxDestroyedError;
|
|
1026
1337
|
exports.SandboxExecTransportError = SandboxExecTransportError;
|
|
1338
|
+
exports.execViaPrivateNetwork = execViaPrivateNetwork;
|
|
1027
1339
|
exports.platformFilesystemProvider = platformFilesystemProvider;
|
|
1028
1340
|
exports.platformSandboxProvider = platformSandboxProvider;
|
|
1029
1341
|
|