@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2
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 +36 -0
- package/dist/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +401 -314
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +441 -39455
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39466
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3624
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1833
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +2 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +401 -326
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -3942
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -915
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28618
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +438 -677
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.d.ts.map +1 -1
- package/dist/spa-static.js +91 -64
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +9 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -702
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -344
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1132
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +2 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +236 -438
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-review/SKILL.md +9 -2
- package/package.json +9 -9
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
package/dist/sandbox/fleet.js
CHANGED
|
@@ -1,266 +1,288 @@
|
|
|
1
|
-
// src/sandbox/fleet.ts
|
|
2
1
|
import path from "path";
|
|
2
|
+
//#region src/sandbox/fleet.ts
|
|
3
|
+
/**
|
|
4
|
+
* Project sandbox fleet: provisioning, reattach, teardown, and budgeting.
|
|
5
|
+
*
|
|
6
|
+
* Server-hosted projects never run on the web host itself. Each project gets
|
|
7
|
+
* its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d
|
|
8
|
+
* from the machine the factory was configured with. This module owns everything
|
|
9
|
+
* about that fleet — which provider is active, where checkouts live inside a
|
|
10
|
+
* sandbox, the idle window, the per-replica budget, and the
|
|
11
|
+
* provision/reattach/teardown lifecycle — but knows nothing about what runs
|
|
12
|
+
* inside a sandbox (git materialization lives with its feature, e.g. the
|
|
13
|
+
* GitHub integration's `sandbox.ts`).
|
|
14
|
+
*
|
|
15
|
+
* The fleet is constructed once at boot with the machine config (or none, when
|
|
16
|
+
* sandboxes are disabled) and handed to consumers — no global registry.
|
|
17
|
+
* Persistence of the provider's reattach id is delegated to the caller via
|
|
18
|
+
* {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can
|
|
19
|
+
* swap the low-level construction via {@link SandboxFleet.setFactory}.
|
|
20
|
+
*/
|
|
21
|
+
/** Invoke a progress callback without letting it break the actual work. */
|
|
3
22
|
function reportProgress(onProgress, event) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
23
|
+
if (!onProgress) return;
|
|
24
|
+
try {
|
|
25
|
+
onProgress(event);
|
|
26
|
+
} catch {}
|
|
9
27
|
}
|
|
28
|
+
/** Raised when provisioning would exceed the per-replica sandbox budget. */
|
|
10
29
|
var SandboxBudgetError = class extends Error {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
max;
|
|
19
|
-
code = "sandbox-budget-exceeded";
|
|
30
|
+
max;
|
|
31
|
+
code = "sandbox-budget-exceeded";
|
|
32
|
+
constructor(max) {
|
|
33
|
+
super(`Sandbox budget exceeded: this server already has ${max} active sandbox(es), the configured per-replica maximum. Close an existing repository's sandbox and try again.`);
|
|
34
|
+
this.max = max;
|
|
35
|
+
this.name = "SandboxBudgetError";
|
|
36
|
+
}
|
|
20
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.
|
|
40
|
+
* Lifecycle goes through the `_`-prefixed wrappers when present (they add
|
|
41
|
+
* status tracking and concurrency safety on `MastraSandbox` subclasses),
|
|
42
|
+
* falling back to the plain methods for interface-only implementations.
|
|
43
|
+
*/
|
|
21
44
|
function toMaterializationSandbox(sandbox, initialEnvironment = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
45
|
+
if (typeof sandbox.executeCommand !== "function") throw new Error(`Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`);
|
|
46
|
+
const lifecycle = sandbox;
|
|
47
|
+
const environment = { ...initialEnvironment };
|
|
48
|
+
return {
|
|
49
|
+
id: sandbox.id,
|
|
50
|
+
start: async () => {
|
|
51
|
+
await (lifecycle._start ?? sandbox.start)?.call(sandbox);
|
|
52
|
+
},
|
|
53
|
+
getInfo: async () => await sandbox.getInfo?.() ?? {},
|
|
54
|
+
executeCommand: (command, args, options) => sandbox.executeCommand(command, args, {
|
|
55
|
+
...options,
|
|
56
|
+
env: {
|
|
57
|
+
...environment,
|
|
58
|
+
...options?.env
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
setEnvironmentVariable: (name, value) => {
|
|
62
|
+
environment[name] = value;
|
|
63
|
+
},
|
|
64
|
+
stop: async () => {
|
|
65
|
+
await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
46
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The provider's reattach id for a started sandbox. For Railway this is the
|
|
71
|
+
* underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a
|
|
72
|
+
* provider-native id (e.g. local) reattach by construction id, so fall back
|
|
73
|
+
* to the sandbox's own logical id.
|
|
74
|
+
*/
|
|
47
75
|
async function readProviderSandboxId(sandbox) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
const info = await sandbox.getInfo();
|
|
77
|
+
const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;
|
|
78
|
+
return typeof id === "string" ? id : sandbox.id;
|
|
51
79
|
}
|
|
80
|
+
/** Keep each path piece a single safe segment (no separators or traversal). */
|
|
52
81
|
function sanitizeSegment(segment) {
|
|
53
|
-
|
|
54
|
-
return cleaned || "repo";
|
|
82
|
+
return segment.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^\.+/, "") || "repo";
|
|
55
83
|
}
|
|
84
|
+
/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */
|
|
56
85
|
function resolveContainedLocalWorkdir(root, ...segments) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
const resolvedRoot = path.resolve(root);
|
|
87
|
+
const resolved = path.resolve(resolvedRoot, ...segments);
|
|
88
|
+
if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;
|
|
89
|
+
throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);
|
|
61
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The sandbox fleet for one deployment. Constructed once at boot — with a
|
|
93
|
+
* config when a sandbox machine was configured, or without one when sandboxes
|
|
94
|
+
* are disabled (every provisioning entry point then throws and
|
|
95
|
+
* {@link enabled} reports `false` so features stay off).
|
|
96
|
+
*/
|
|
62
97
|
var SandboxFleet = class {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
*/
|
|
250
|
-
async reattachSandbox(providerSandboxId, options = {}) {
|
|
251
|
-
const sandbox = this.#build({
|
|
252
|
-
providerSandboxId,
|
|
253
|
-
idleTimeoutMinutes: this.idleMinutes,
|
|
254
|
-
...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
|
|
255
|
-
});
|
|
256
|
-
await sandbox.start();
|
|
257
|
-
return sandbox;
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
export {
|
|
261
|
-
SandboxBudgetError,
|
|
262
|
-
SandboxFleet,
|
|
263
|
-
reportProgress,
|
|
264
|
-
resolveContainedLocalWorkdir
|
|
98
|
+
#config;
|
|
99
|
+
#factory;
|
|
100
|
+
#liveCount = 0;
|
|
101
|
+
constructor(config) {
|
|
102
|
+
this.#config = config;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* True when a sandbox machine was configured. The factory validates the
|
|
106
|
+
* machine implements `clone()` at boot, so a configured fleet is usable —
|
|
107
|
+
* sandbox-backed projects stay off only when the slot was omitted.
|
|
108
|
+
*/
|
|
109
|
+
get enabled() {
|
|
110
|
+
return this.#config !== void 0;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Name of the active sandbox provider — the configured machine's `provider`
|
|
114
|
+
* discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was
|
|
115
|
+
* constructed without a config. Diagnostic only; feature gating goes
|
|
116
|
+
* through {@link enabled}.
|
|
117
|
+
*/
|
|
118
|
+
get provider() {
|
|
119
|
+
return this.#config?.machine.provider ?? "none";
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.
|
|
123
|
+
* Read back from the machine's own config when it exposes one
|
|
124
|
+
* (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the
|
|
125
|
+
* fleet only needs it to schedule GC and stamp sandbox clones. Advisory:
|
|
126
|
+
* providers without idle GC ignore it, and a re-open detects a torn-down VM
|
|
127
|
+
* and re-provisions cleanly.
|
|
128
|
+
*/
|
|
129
|
+
get idleMinutes() {
|
|
130
|
+
const minutes = (this.#config?.machine)?.idleTimeoutMinutes;
|
|
131
|
+
return typeof minutes === "number" && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.
|
|
135
|
+
* This is a lightweight per-process budget to keep a single replica from
|
|
136
|
+
* exhausting provider quota — it is not a global, cross-replica scheduler
|
|
137
|
+
* (that is a deferred follow-up).
|
|
138
|
+
*/
|
|
139
|
+
get maxSandboxes() {
|
|
140
|
+
return this.#config?.maxSandboxes ?? 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Count of sandboxes this fleet has freshly provisioned and not yet torn
|
|
144
|
+
* down. Reattaches to existing VMs do not count (they reuse an already-billed
|
|
145
|
+
* sandbox). Used to enforce {@link maxSandboxes}.
|
|
146
|
+
*/
|
|
147
|
+
get liveCount() {
|
|
148
|
+
return this.#liveCount;
|
|
149
|
+
}
|
|
150
|
+
/** For tests: reset the live-sandbox counter to a known state. */
|
|
151
|
+
__resetLiveCount(value = 0) {
|
|
152
|
+
this.#liveCount = value;
|
|
153
|
+
}
|
|
154
|
+
/** Override the sandbox factory (tests). */
|
|
155
|
+
setFactory(factory) {
|
|
156
|
+
this.#factory = factory;
|
|
157
|
+
}
|
|
158
|
+
/** Reset to the default machine-cloning factory. */
|
|
159
|
+
resetFactory() {
|
|
160
|
+
this.#factory = void 0;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Compute the in-sandbox working directory for a repo: a nested
|
|
164
|
+
* `<base>/<owner>/<name>` layout under the factory-resolved checkout base.
|
|
165
|
+
* Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud
|
|
166
|
+
* sandboxes are one-per-project so it's merely tidy there, but local
|
|
167
|
+
* checkouts share one host root where it prevents collisions. Server-side
|
|
168
|
+
* only; never derived from client input.
|
|
169
|
+
*/
|
|
170
|
+
computeWorkdir(repoFullName) {
|
|
171
|
+
if (!this.#config) throw new Error("No sandbox configured");
|
|
172
|
+
const [owner, name] = repoFullName.split("/", 2);
|
|
173
|
+
return `${this.#config.workdirBase}/${sanitizeSegment(owner || "unknown")}/${sanitizeSegment(name || "repo")}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Compute the host working directory for a local GitHub session checkout.
|
|
177
|
+
* This is server-derived only: repo pieces are sanitized and the trusted
|
|
178
|
+
* session id is kept as a single path segment under the configured local root.
|
|
179
|
+
*/
|
|
180
|
+
computeLocalSessionWorkdir(repoFullName, sessionId) {
|
|
181
|
+
if (!this.#config) throw new Error("No sandbox configured");
|
|
182
|
+
if (this.#config.machine.provider !== "local") throw new Error("Local session workdirs require the local sandbox provider");
|
|
183
|
+
const localRoot = this.#config.machine.workingDirectory;
|
|
184
|
+
if (typeof localRoot !== "string" || localRoot.length === 0) throw new Error("Local sandbox working directory is not configured");
|
|
185
|
+
const [owner, name] = repoFullName.split("/", 2);
|
|
186
|
+
return resolveContainedLocalWorkdir(localRoot, "github-sessions", sanitizeSegment(owner || "unknown"), sanitizeSegment(name || "repo"), sanitizeSegment(sessionId));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build a (not-yet-started) sandbox: the test-provided factory when set,
|
|
190
|
+
* otherwise a per-project clone of the configured machine. The stored id is
|
|
191
|
+
* passed both as the logical `id` (providers that reattach by construction
|
|
192
|
+
* id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so
|
|
193
|
+
* reattach works across the provider matrix.
|
|
194
|
+
*/
|
|
195
|
+
#build(opts) {
|
|
196
|
+
if (this.#factory) return this.#factory(opts);
|
|
197
|
+
if (!this.#config) throw new Error("No sandbox configured");
|
|
198
|
+
return toMaterializationSandbox(this.#config.machine.clone({
|
|
199
|
+
...opts.providerSandboxId ? {
|
|
200
|
+
id: opts.providerSandboxId,
|
|
201
|
+
sandboxId: opts.providerSandboxId
|
|
202
|
+
} : {},
|
|
203
|
+
...opts.env ? { env: opts.env } : {},
|
|
204
|
+
...opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {},
|
|
205
|
+
...opts.idleTimeoutMinutes !== void 0 ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {},
|
|
206
|
+
...opts.checkpointName ? { checkpointName: opts.checkpointName } : {}
|
|
207
|
+
}), opts.env);
|
|
208
|
+
}
|
|
209
|
+
async ensureSandbox(store, envOrProgress, progressOrOptions, maybeOptions = {}) {
|
|
210
|
+
const env = typeof envOrProgress === "function" ? void 0 : envOrProgress;
|
|
211
|
+
const onProgress = typeof envOrProgress === "function" ? envOrProgress : progressOrOptions;
|
|
212
|
+
const options = typeof envOrProgress === "function" ? progressOrOptions ?? {} : maybeOptions;
|
|
213
|
+
const idleTimeoutMinutes = this.idleMinutes;
|
|
214
|
+
const checkpointName = store.checkpointName;
|
|
215
|
+
if (store.sandboxId) {
|
|
216
|
+
reportProgress(onProgress, {
|
|
217
|
+
phase: "reattaching",
|
|
218
|
+
message: "Reconnecting to your sandbox…"
|
|
219
|
+
});
|
|
220
|
+
const reattached = this.#build({
|
|
221
|
+
providerSandboxId: store.sandboxId,
|
|
222
|
+
idleTimeoutMinutes,
|
|
223
|
+
...checkpointName ? { checkpointName } : {},
|
|
224
|
+
...env ? { env } : {},
|
|
225
|
+
...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
await reattached.start();
|
|
229
|
+
return reattached;
|
|
230
|
+
} catch {
|
|
231
|
+
await store.setSandboxId(null);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const max = this.maxSandboxes;
|
|
235
|
+
if (max > 0 && this.#liveCount >= max) throw new SandboxBudgetError(max);
|
|
236
|
+
reportProgress(onProgress, {
|
|
237
|
+
phase: "provisioning",
|
|
238
|
+
message: "Provisioning a new sandbox…"
|
|
239
|
+
});
|
|
240
|
+
const sandbox = this.#build({
|
|
241
|
+
idleTimeoutMinutes,
|
|
242
|
+
...checkpointName ? { checkpointName } : {},
|
|
243
|
+
...env ? { env } : {},
|
|
244
|
+
...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
|
|
245
|
+
});
|
|
246
|
+
await sandbox.start();
|
|
247
|
+
this.#liveCount += 1;
|
|
248
|
+
const providerSandboxId = await readProviderSandboxId(sandbox);
|
|
249
|
+
if (providerSandboxId) await store.setSandboxId(providerSandboxId);
|
|
250
|
+
return sandbox;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Tear down a sandbox binding: stop the live VM (best-effort) and clear the
|
|
254
|
+
* persisted state through the binding store so the next open re-provisions
|
|
255
|
+
* cleanly. Decrements the per-replica live-sandbox counter.
|
|
256
|
+
*
|
|
257
|
+
* @param store the binding to tear down
|
|
258
|
+
* @param sandbox an already-reattached live sandbox to stop, when available
|
|
259
|
+
*/
|
|
260
|
+
async teardownSandbox(store, sandbox) {
|
|
261
|
+
if (sandbox?.stop) try {
|
|
262
|
+
await sandbox.stop();
|
|
263
|
+
} catch {}
|
|
264
|
+
if (store.sandboxId) {
|
|
265
|
+
if (this.#liveCount > 0) this.#liveCount -= 1;
|
|
266
|
+
await store.clear();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Reattach to an already-provisioned sandbox by its provider id and start it.
|
|
271
|
+
* Used by the workspace seam when opening a project that was already
|
|
272
|
+
* materialized (sandbox id + workdir carried on controller state), so no DB
|
|
273
|
+
* round-trip is needed.
|
|
274
|
+
*/
|
|
275
|
+
async reattachSandbox(providerSandboxId, options = {}) {
|
|
276
|
+
const sandbox = this.#build({
|
|
277
|
+
providerSandboxId,
|
|
278
|
+
idleTimeoutMinutes: this.idleMinutes,
|
|
279
|
+
...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
|
|
280
|
+
});
|
|
281
|
+
await sandbox.start();
|
|
282
|
+
return sandbox;
|
|
283
|
+
}
|
|
265
284
|
};
|
|
285
|
+
//#endregion
|
|
286
|
+
export { SandboxBudgetError, SandboxFleet, reportProgress, resolveContainedLocalWorkdir };
|
|
287
|
+
|
|
266
288
|
//# sourceMappingURL=fleet.js.map
|