@agentsh/secure-sandbox 0.1.6 → 0.1.7
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 +39 -12
- package/dist/adapters/blaxel.d.ts +1 -1
- package/dist/adapters/cloudflare.d.ts +1 -1
- package/dist/adapters/daytona.d.ts +1 -1
- package/dist/adapters/e2b.d.ts +1 -1
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +6 -1
- package/dist/adapters/vercel.d.ts +1 -1
- package/dist/chunk-4FJHYLAB.js +251 -0
- package/dist/chunk-4FJHYLAB.js.map +1 -0
- package/dist/chunk-5IG6ABIZ.js +268 -0
- package/dist/chunk-5IG6ABIZ.js.map +1 -0
- package/dist/{chunk-GFPHTJLU.js → chunk-LNDICGZU.js} +3 -243
- package/dist/chunk-LNDICGZU.js.map +1 -0
- package/dist/index-TyzWAIUD.d.ts +60 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +115 -43
- package/dist/index.js.map +1 -1
- package/dist/policies/index.d.ts +1 -1
- package/dist/policies/index.js +5 -3
- package/dist/testing/index.d.ts +1 -1
- package/dist/{types-S_fIEFHD.d.ts → types-DFMGk2GV.d.ts} +28 -1
- package/package.json +9 -1
- package/dist/chunk-GFPHTJLU.js.map +0 -1
- package/dist/chunk-KXCR2ZML.js +0 -129
- package/dist/chunk-KXCR2ZML.js.map +0 -1
- package/dist/index-D6DG8Lpi.d.ts +0 -28
- package/dist/{index-Nmlhw9oj.d.ts → index-CedRtlB6.d.ts} +22 -22
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { vercel } from './adapters/vercel.js';
|
|
2
|
+
import { e2b } from './adapters/e2b.js';
|
|
3
|
+
import { daytona } from './adapters/daytona.js';
|
|
4
|
+
import { cloudflare } from './adapters/cloudflare.js';
|
|
5
|
+
import { blaxel } from './adapters/blaxel.js';
|
|
6
|
+
import { S as SandboxAdapter, a as SecureConfig } from './types-DFMGk2GV.js';
|
|
7
|
+
|
|
8
|
+
declare function sprites(sprite: any): SandboxAdapter;
|
|
9
|
+
/**
|
|
10
|
+
* Returns Sprites-optimized defaults for SecureConfig.
|
|
11
|
+
* Spread into your secureSandbox() call:
|
|
12
|
+
*
|
|
13
|
+
* secureSandbox(sprites(s), { ...spritesDefaults(), ...yourOverrides })
|
|
14
|
+
*/
|
|
15
|
+
declare function spritesDefaults(): Partial<SecureConfig>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a Modal Sandbox object into a SandboxAdapter.
|
|
19
|
+
*
|
|
20
|
+
* Modal sandboxes run on gVisor which lacks seccomp user-notify and
|
|
21
|
+
* full FUSE support. Use `modalDefaults()` to get ptrace-optimized
|
|
22
|
+
* configuration that works on gVisor.
|
|
23
|
+
*
|
|
24
|
+
* The `sandbox` parameter is typed as `any` to avoid a hard dependency
|
|
25
|
+
* on the Modal SDK — pass any object whose `.exec()` accepts variadic
|
|
26
|
+
* string arguments and returns a process with `.stdout.read()`,
|
|
27
|
+
* `.stderr.read()`, `.returncode`, and `.wait()`.
|
|
28
|
+
*/
|
|
29
|
+
declare function modal(sandbox: any): SandboxAdapter;
|
|
30
|
+
/**
|
|
31
|
+
* Returns Modal-optimized defaults for SecureConfig.
|
|
32
|
+
*
|
|
33
|
+
* Key differences from other providers:
|
|
34
|
+
* - ptrace enabled (gVisor blocks seccomp user-notify)
|
|
35
|
+
* - seccomp_prefilter disabled (gVisor blocks BPF injection)
|
|
36
|
+
* - FUSE deferred (may not be available on gVisor)
|
|
37
|
+
* - unix_sockets disabled (mutually exclusive with ptrace)
|
|
38
|
+
* - cgroups disabled (Modal handles resource limits)
|
|
39
|
+
* - allow_degraded enabled (graceful fallback for FUSE/seccomp)
|
|
40
|
+
*
|
|
41
|
+
* Spread into your secureSandbox() call:
|
|
42
|
+
*
|
|
43
|
+
* secureSandbox(modal(sb), { ...modalDefaults(), ...yourOverrides })
|
|
44
|
+
*/
|
|
45
|
+
declare function modalDefaults(): Partial<SecureConfig>;
|
|
46
|
+
|
|
47
|
+
declare const index_blaxel: typeof blaxel;
|
|
48
|
+
declare const index_cloudflare: typeof cloudflare;
|
|
49
|
+
declare const index_daytona: typeof daytona;
|
|
50
|
+
declare const index_e2b: typeof e2b;
|
|
51
|
+
declare const index_modal: typeof modal;
|
|
52
|
+
declare const index_modalDefaults: typeof modalDefaults;
|
|
53
|
+
declare const index_sprites: typeof sprites;
|
|
54
|
+
declare const index_spritesDefaults: typeof spritesDefaults;
|
|
55
|
+
declare const index_vercel: typeof vercel;
|
|
56
|
+
declare namespace index {
|
|
57
|
+
export { index_blaxel as blaxel, index_cloudflare as cloudflare, index_daytona as daytona, index_e2b as e2b, index_modal as modal, index_modalDefaults as modalDefaults, index_sprites as sprites, index_spritesDefaults as spritesDefaults, index_vercel as vercel };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { modalDefaults as a, spritesDefaults as b, index as i, modal as m, sprites as s };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { S as SandboxAdapter, a as SecureConfig, b as SecuredSandbox } from './types-
|
|
2
|
-
export { E as ExecResult, I as InstallStrategy, L as LicenseSpdxMatch, P as PackageChecksConfig, c as PackageMatch, d as PackageRule, e as ProviderConfig, R as ReadFileResult, f as SecurityMode, T as ThreatFeed, g as ThreatFeedsConfig, W as WriteFileResult, h as defaultThreatFeeds } from './types-
|
|
3
|
-
export { P as PolicyDefinition, i as policies } from './index-
|
|
1
|
+
import { S as SandboxAdapter, a as SecureConfig, b as SecuredSandbox } from './types-DFMGk2GV.js';
|
|
2
|
+
export { E as ExecResult, I as InstallStrategy, L as LicenseSpdxMatch, P as PackageChecksConfig, c as PackageMatch, d as PackageRule, e as ProviderConfig, R as ReadFileResult, f as SecurityMode, T as ThreatFeed, g as ThreatFeedsConfig, W as WriteFileResult, h as defaultThreatFeeds } from './types-DFMGk2GV.js';
|
|
3
|
+
export { P as PolicyDefinition, i as policies } from './index-CedRtlB6.js';
|
|
4
4
|
import { ZodIssue } from 'zod';
|
|
5
|
-
export { i as adapters } from './index-
|
|
5
|
+
export { i as adapters } from './index-TyzWAIUD.js';
|
|
6
6
|
import './adapters/vercel.js';
|
|
7
7
|
import './adapters/e2b.js';
|
|
8
8
|
import './adapters/daytona.js';
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
adapters_exports
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-5IG6ABIZ.js";
|
|
4
4
|
import "./chunk-UYEAO27E.js";
|
|
5
5
|
import "./chunk-LMN3KM53.js";
|
|
6
6
|
import "./chunk-45FKFVMC.js";
|
|
7
7
|
import "./chunk-2P37YGN7.js";
|
|
8
8
|
import "./chunk-OANLKSOD.js";
|
|
9
9
|
import "./chunk-JY5ERJTX.js";
|
|
10
|
+
import {
|
|
11
|
+
policies_exports,
|
|
12
|
+
serializePolicy,
|
|
13
|
+
systemPolicyYaml
|
|
14
|
+
} from "./chunk-4FJHYLAB.js";
|
|
10
15
|
import {
|
|
11
16
|
AgentSHError,
|
|
12
17
|
IncompatibleProviderVersionError,
|
|
@@ -16,23 +21,20 @@ import {
|
|
|
16
21
|
ProvisioningError,
|
|
17
22
|
RuntimeError,
|
|
18
23
|
agentDefault,
|
|
19
|
-
policies_exports,
|
|
20
|
-
serializePolicy,
|
|
21
|
-
systemPolicyYaml,
|
|
22
24
|
validatePolicy
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-LNDICGZU.js";
|
|
24
26
|
import "./chunk-PZ5AY32C.js";
|
|
25
27
|
|
|
26
28
|
// src/core/integrity.ts
|
|
27
|
-
var PINNED_VERSION = "0.
|
|
29
|
+
var PINNED_VERSION = "0.16.2";
|
|
28
30
|
var CHECKSUMS = {
|
|
31
|
+
"0.16.2": {
|
|
32
|
+
linux_amd64: "7ff357066a61694626d4c19afa92fdf368318bced9be90391cc2f3808976f995",
|
|
33
|
+
linux_arm64: "a48b3e4a60804cca98326619a68409e8ee83556d69ee2cf5d574e4361e0c19c6"
|
|
34
|
+
},
|
|
29
35
|
"0.15.0": {
|
|
30
36
|
linux_amd64: "89f7ebbfd75ffd961245ec62b2602fd0cc387740502ac858dbc39c367c5699c5",
|
|
31
37
|
linux_arm64: "3fabbd749f9e98fb9f96ddfc94c389a6868cda7ed3668daa8440c39ceec85f3b"
|
|
32
|
-
},
|
|
33
|
-
"0.14.0": {
|
|
34
|
-
linux_amd64: "2ab8ba0d6637fe1a5badf840c3db197161a6f9865d721ed216029d229b1b9bbc",
|
|
35
|
-
linux_arm64: "929d18dd9fe36e9b2fa830d7ae64b4fb481853e743ade8674fcfcdc73470ed53"
|
|
36
38
|
}
|
|
37
39
|
};
|
|
38
40
|
function getChecksum(version, arch, override) {
|
|
@@ -128,10 +130,20 @@ function generateServerConfig(opts) {
|
|
|
128
130
|
},
|
|
129
131
|
sandbox: {
|
|
130
132
|
enabled: true,
|
|
131
|
-
allow_degraded: true,
|
|
132
|
-
|
|
133
|
+
allow_degraded: opts.allowDegraded ?? true,
|
|
134
|
+
// FUSE disabled by default: when agentsh server runs as root and exec
|
|
135
|
+
// users are non-root (e.g. E2B, Daytona), the FUSE workspace-mnt is
|
|
136
|
+
// inaccessible to non-root users causing exec to fail with exit code 2.
|
|
137
|
+
// File policy is still enforced via landlock. Enable FUSE
|
|
138
|
+
// explicitly via serverConfig: { fuse: { enabled: true } } if needed.
|
|
139
|
+
fuse: { enabled: false },
|
|
133
140
|
network: { enabled: true },
|
|
134
|
-
|
|
141
|
+
// Seccomp NOTIFY disabled by default: many container environments
|
|
142
|
+
// (Daytona, E2B custom images) restrict the seccomp() syscall via their
|
|
143
|
+
// container seccomp profile, causing "install seccomp filter: operation
|
|
144
|
+
// canceled" on every exec. Policy is still enforced via landlock and
|
|
145
|
+
// network rules. When ptrace is enabled, seccomp is also incompatible.
|
|
146
|
+
seccomp: { enabled: false }
|
|
135
147
|
}
|
|
136
148
|
};
|
|
137
149
|
if (opts.watchtower) config.watchtower = opts.watchtower;
|
|
@@ -145,7 +157,12 @@ function generateServerConfig(opts) {
|
|
|
145
157
|
if (opts.serverTimeouts.maxRequestSize) http.max_request_size = opts.serverTimeouts.maxRequestSize;
|
|
146
158
|
}
|
|
147
159
|
if (opts.logging) config.logging = { ...opts.logging };
|
|
148
|
-
const sessionsObj = {
|
|
160
|
+
const sessionsObj = {
|
|
161
|
+
// Default sessions to a writable location outside /etc/agentsh (which is
|
|
162
|
+
// locked to 555/444 during provisioning). v0.16.2+ resolves workspace mount
|
|
163
|
+
// symlinks inside the sessions dir, which requires write access.
|
|
164
|
+
base_dir: "/var/lib/agentsh/sessions"
|
|
165
|
+
};
|
|
149
166
|
if (opts.realPaths) sessionsObj.real_paths = true;
|
|
150
167
|
if (opts.sessions) {
|
|
151
168
|
if (opts.sessions.baseDir) sessionsObj.base_dir = opts.sessions.baseDir;
|
|
@@ -168,8 +185,11 @@ function generateServerConfig(opts) {
|
|
|
168
185
|
...opts.sandboxLimits.maxProcesses !== void 0 && { max_processes: opts.sandboxLimits.maxProcesses }
|
|
169
186
|
};
|
|
170
187
|
}
|
|
171
|
-
if (opts.fuse
|
|
172
|
-
config.sandbox.fuse
|
|
188
|
+
if (opts.fuse) {
|
|
189
|
+
const fuseObj = config.sandbox.fuse;
|
|
190
|
+
if (opts.fuse.deferred !== void 0) fuseObj.deferred = opts.fuse.deferred;
|
|
191
|
+
if (opts.fuse.deferredMarkerFile) fuseObj.deferred_marker_file = opts.fuse.deferredMarkerFile;
|
|
192
|
+
if (opts.fuse.deferredEnableCommand) fuseObj.deferred_enable_command = opts.fuse.deferredEnableCommand;
|
|
173
193
|
}
|
|
174
194
|
if (opts.networkIntercept) {
|
|
175
195
|
const net = config.sandbox.network;
|
|
@@ -178,6 +198,7 @@ function generateServerConfig(opts) {
|
|
|
178
198
|
}
|
|
179
199
|
if (opts.seccompDetails) {
|
|
180
200
|
const sec = config.sandbox.seccomp;
|
|
201
|
+
if (!opts.ptrace?.enabled) sec.enabled = true;
|
|
181
202
|
if (opts.seccompDetails.execve !== void 0) sec.execve = opts.seccompDetails.execve;
|
|
182
203
|
if (opts.seccompDetails.fileMonitor) {
|
|
183
204
|
sec.file_monitor = {
|
|
@@ -192,6 +213,32 @@ function generateServerConfig(opts) {
|
|
|
192
213
|
if (opts.unixSockets) {
|
|
193
214
|
config.sandbox.unix_sockets = { ...opts.unixSockets };
|
|
194
215
|
}
|
|
216
|
+
if (opts.ptrace) {
|
|
217
|
+
const ptraceObj = {};
|
|
218
|
+
if (opts.ptrace.enabled !== void 0) ptraceObj.enabled = opts.ptrace.enabled;
|
|
219
|
+
if (opts.ptrace.attachMode) ptraceObj.attach_mode = opts.ptrace.attachMode;
|
|
220
|
+
if (opts.ptrace.maskTracerPid) ptraceObj.mask_tracer_pid = opts.ptrace.maskTracerPid;
|
|
221
|
+
if (opts.ptrace.trace) {
|
|
222
|
+
const traceObj = {};
|
|
223
|
+
if (opts.ptrace.trace.execve !== void 0) traceObj.execve = opts.ptrace.trace.execve;
|
|
224
|
+
if (opts.ptrace.trace.file !== void 0) traceObj.file = opts.ptrace.trace.file;
|
|
225
|
+
if (opts.ptrace.trace.network !== void 0) traceObj.network = opts.ptrace.trace.network;
|
|
226
|
+
if (opts.ptrace.trace.signal !== void 0) traceObj.signal = opts.ptrace.trace.signal;
|
|
227
|
+
ptraceObj.trace = traceObj;
|
|
228
|
+
}
|
|
229
|
+
if (opts.ptrace.performance) {
|
|
230
|
+
const perfObj = {};
|
|
231
|
+
if (opts.ptrace.performance.seccompPrefilter !== void 0) perfObj.seccomp_prefilter = opts.ptrace.performance.seccompPrefilter;
|
|
232
|
+
if (opts.ptrace.performance.maxTracees !== void 0) perfObj.max_tracees = opts.ptrace.performance.maxTracees;
|
|
233
|
+
if (opts.ptrace.performance.maxHoldMs !== void 0) perfObj.max_hold_ms = opts.ptrace.performance.maxHoldMs;
|
|
234
|
+
ptraceObj.performance = perfObj;
|
|
235
|
+
}
|
|
236
|
+
if (opts.ptrace.onAttachFailure) ptraceObj.on_attach_failure = opts.ptrace.onAttachFailure;
|
|
237
|
+
config.sandbox.ptrace = ptraceObj;
|
|
238
|
+
}
|
|
239
|
+
if (opts.envInject) {
|
|
240
|
+
config.sandbox.env_inject = { ...opts.envInject };
|
|
241
|
+
}
|
|
195
242
|
if (opts.proxy) {
|
|
196
243
|
config.proxy = { ...opts.proxy };
|
|
197
244
|
}
|
|
@@ -287,7 +334,8 @@ async function getTraceparent() {
|
|
|
287
334
|
|
|
288
335
|
// src/core/provision.ts
|
|
289
336
|
var SECURITY_MODE_RANK = {
|
|
290
|
-
full:
|
|
337
|
+
full: 5,
|
|
338
|
+
ptrace: 4,
|
|
291
339
|
landlock: 3,
|
|
292
340
|
"landlock-only": 2,
|
|
293
341
|
minimal: 1
|
|
@@ -333,13 +381,22 @@ async function provision(adapter, config = {}) {
|
|
|
333
381
|
policyName = "policy",
|
|
334
382
|
threatFeeds,
|
|
335
383
|
packageChecks,
|
|
384
|
+
skipShim = false,
|
|
336
385
|
serverConfig: extendedConfig
|
|
337
386
|
} = config;
|
|
338
387
|
const policy = rawPolicy ? validatePolicy(rawPolicy) : agentDefault();
|
|
339
388
|
let securityMode = "full";
|
|
340
389
|
if (installStrategy === "running") {
|
|
341
390
|
await healthCheck(adapter);
|
|
342
|
-
|
|
391
|
+
if (config.securityMode) {
|
|
392
|
+
securityMode = config.securityMode;
|
|
393
|
+
} else if (minimumSecurityMode) {
|
|
394
|
+
throw new ProvisioningError({
|
|
395
|
+
phase: "install",
|
|
396
|
+
command: "securityMode check",
|
|
397
|
+
stderr: `Cannot verify security mode in 'running' strategy \u2014 set securityMode explicitly when using minimumSecurityMode`
|
|
398
|
+
});
|
|
399
|
+
}
|
|
343
400
|
if (minimumSecurityMode && isWeakerThan(securityMode, minimumSecurityMode)) {
|
|
344
401
|
throw new ProvisioningError({
|
|
345
402
|
phase: "install",
|
|
@@ -371,7 +428,15 @@ async function provision(adapter, config = {}) {
|
|
|
371
428
|
});
|
|
372
429
|
}
|
|
373
430
|
} else if (installStrategy === "download" || installStrategy === "upload") {
|
|
374
|
-
|
|
431
|
+
let needsInstall = !exists;
|
|
432
|
+
if (exists && agentshVersion !== "skip-version-check") {
|
|
433
|
+
const versionResult = await adapter.exec("agentsh", ["--version"]);
|
|
434
|
+
const installedVersion = versionResult.stdout.trim().replace(/^v/, "");
|
|
435
|
+
if (!installedVersion.startsWith(agentshVersion)) {
|
|
436
|
+
needsInstall = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (needsInstall) {
|
|
375
440
|
const arch = archOverride ?? await detectArch(adapter);
|
|
376
441
|
if (installStrategy === "download") {
|
|
377
442
|
await downloadBinary(adapter, agentshVersion, arch, agentshBinaryUrl);
|
|
@@ -416,28 +481,29 @@ async function provision(adapter, config = {}) {
|
|
|
416
481
|
stderr: `Detected security mode '${securityMode}' is weaker than required '${minimumSecurityMode}'`
|
|
417
482
|
});
|
|
418
483
|
}
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
484
|
+
const realPaths = realPathsOverride ?? false;
|
|
485
|
+
if (!skipShim) {
|
|
486
|
+
const shimResult = await adapter.exec(
|
|
487
|
+
"agentsh",
|
|
488
|
+
[
|
|
489
|
+
"shim",
|
|
490
|
+
"install-shell",
|
|
491
|
+
"--root",
|
|
492
|
+
"/",
|
|
493
|
+
"--shim",
|
|
494
|
+
"/usr/bin/agentsh-shell-shim",
|
|
495
|
+
"--bash",
|
|
496
|
+
"--i-understand-this-modifies-the-host"
|
|
497
|
+
],
|
|
498
|
+
{ sudo: true }
|
|
499
|
+
);
|
|
500
|
+
if (shimResult.exitCode !== 0) {
|
|
501
|
+
throw new ProvisioningError({
|
|
502
|
+
phase: "install",
|
|
503
|
+
command: "agentsh shim install-shell",
|
|
504
|
+
stderr: shimResult.stderr
|
|
505
|
+
});
|
|
506
|
+
}
|
|
441
507
|
}
|
|
442
508
|
const mkdirResult = await adapter.exec(
|
|
443
509
|
"mkdir",
|
|
@@ -508,7 +574,12 @@ async function provision(adapter, config = {}) {
|
|
|
508
574
|
stderr: chownResult.stderr
|
|
509
575
|
});
|
|
510
576
|
}
|
|
511
|
-
await adapter.exec("mkdir", ["-p", workspace], { sudo: true });
|
|
577
|
+
await adapter.exec("mkdir", ["-p", workspace, "/var/lib/agentsh/sessions"], { sudo: true });
|
|
578
|
+
await adapter.exec("chmod", ["755", "/var/lib/agentsh", "/var/lib/agentsh/sessions"], { sudo: true });
|
|
579
|
+
await adapter.exec("sh", [
|
|
580
|
+
"-c",
|
|
581
|
+
'grep -q user_allow_other /etc/fuse.conf 2>/dev/null || echo "user_allow_other" >> /etc/fuse.conf'
|
|
582
|
+
], { sudo: true });
|
|
512
583
|
const serverResult = await adapter.exec(
|
|
513
584
|
"agentsh",
|
|
514
585
|
["server", "--config", "/etc/agentsh/config.yml"],
|
|
@@ -553,6 +624,7 @@ async function provision(adapter, config = {}) {
|
|
|
553
624
|
});
|
|
554
625
|
}
|
|
555
626
|
}
|
|
627
|
+
await adapter.exec("chmod", ["-R", "755", "/var/lib/agentsh/sessions/"], { sudo: true });
|
|
556
628
|
const effectiveTraceParent = traceParent ?? await getTraceparent();
|
|
557
629
|
if (effectiveTraceParent) {
|
|
558
630
|
await adapter.exec("curl", [
|
|
@@ -699,7 +771,7 @@ async function detectSecurityMode(adapter) {
|
|
|
699
771
|
});
|
|
700
772
|
}
|
|
701
773
|
const mode = parsed.security_mode;
|
|
702
|
-
const validModes = ["full", "landlock", "landlock-only", "minimal"];
|
|
774
|
+
const validModes = ["full", "ptrace", "landlock", "landlock-only", "minimal"];
|
|
703
775
|
if (!validModes.includes(mode)) {
|
|
704
776
|
throw new ProvisioningError({
|
|
705
777
|
phase: "install",
|