@the-open-engine/zeroshot 6.23.0 → 6.25.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/cli/index.js +37 -2
- package/docker/zeroshot-cluster/Dockerfile +7 -0
- package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/omp.js +37 -11
- package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
- package/lib/agent-cli-provider/omp-release.d.ts +3 -0
- package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-release.js +20 -1
- package/lib/agent-cli-provider/omp-release.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-driver.js +27 -2
- package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-session.js +3 -3
- package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +20 -8
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +56 -9
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +2 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/docker-config.js +122 -5
- package/package.json +2 -2
- package/src/agent/agent-lifecycle.js +78 -3
- package/src/agent/agent-task-executor.js +72 -3
- package/src/agent/provider-session.js +112 -2
- package/src/agent-cli-provider/adapters/omp.ts +41 -11
- package/src/agent-cli-provider/omp-release.ts +26 -0
- package/src/agent-cli-provider/omp-rpc-driver.ts +31 -3
- package/src/agent-cli-provider/omp-rpc-session.ts +3 -3
- package/src/agent-cli-provider/provider-registry.ts +102 -20
- package/src/agent-cli-provider/types.ts +4 -0
- package/src/isolation-manager.js +535 -89
- package/src/omp-blob-root.js +110 -0
- package/src/omp-config-overlay.js +9 -1
- package/src/omp-execution-fingerprint.js +62 -0
- package/src/omp-session-limits.js +17 -0
- package/src/omp-session-partition.js +297 -0
- package/src/omp-session-verifier.js +576 -0
- package/src/orchestrator.js +11 -1
- package/src/preflight.js +15 -2
- package/task-lib/commands/clean.js +23 -0
- package/task-lib/commands/resume.js +42 -0
- package/task-lib/commands/run.js +65 -0
- package/task-lib/omp-session-cleanup.js +160 -0
- package/task-lib/omp-session-ownership-schema.js +262 -0
- package/task-lib/omp-session-ownership.js +332 -0
- package/task-lib/omp-storage-root.js +35 -0
- package/task-lib/rpc-watcher.js +332 -2
- package/task-lib/runner.js +195 -4
- package/task-lib/store.js +42 -7
package/src/isolation-manager.js
CHANGED
|
@@ -15,7 +15,7 @@ const path = require('path');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const { loadSettings } = require('../lib/settings');
|
|
18
|
-
const {
|
|
18
|
+
const { resolveClaudeAuth } = require('../lib/settings/claude-auth');
|
|
19
19
|
const {
|
|
20
20
|
normalizeProviderName,
|
|
21
21
|
getProviderMetadata,
|
|
@@ -23,9 +23,12 @@ const {
|
|
|
23
23
|
} = require('../lib/provider-names');
|
|
24
24
|
const {
|
|
25
25
|
MOUNT_PRESETS,
|
|
26
|
+
ENV_PRESETS,
|
|
26
27
|
resolveMounts,
|
|
27
28
|
resolveEnvs,
|
|
28
29
|
expandEnvPatterns,
|
|
30
|
+
isUsableEnvValue,
|
|
31
|
+
validateProviderEnvAuth,
|
|
29
32
|
} = require('../lib/docker-config');
|
|
30
33
|
const { getProvider } = require('./providers');
|
|
31
34
|
const { readRepoSettings } = require('../lib/repo-settings');
|
|
@@ -181,6 +184,40 @@ function providerDockerInstall(providerName) {
|
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Registry-owned Docker platform (e.g. 'linux/amd64') for a provider, or null when unset.
|
|
189
|
+
* Providers other than the ones that declare `docker.platform` keep today's host-native
|
|
190
|
+
* (unset `--platform`) behavior.
|
|
191
|
+
* @param {string} providerName
|
|
192
|
+
* @returns {string|null}
|
|
193
|
+
*/
|
|
194
|
+
function providerDockerPlatform(providerName) {
|
|
195
|
+
if (!providerName) return null;
|
|
196
|
+
try {
|
|
197
|
+
const metadata = getProviderMetadata(providerName);
|
|
198
|
+
const platform = metadata && metadata.docker && metadata.docker.platform;
|
|
199
|
+
return typeof platform === 'string' && platform.trim() ? platform.trim() : null;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Registry-owned $HOME-placeholder config/overlay roots for a provider, unexpanded.
|
|
207
|
+
* @param {string} providerName
|
|
208
|
+
* @returns {readonly string[]}
|
|
209
|
+
*/
|
|
210
|
+
function providerDockerConfigRootsRaw(providerName) {
|
|
211
|
+
if (!providerName) return [];
|
|
212
|
+
try {
|
|
213
|
+
const metadata = getProviderMetadata(providerName);
|
|
214
|
+
const roots = metadata && metadata.docker && metadata.docker.configRoots;
|
|
215
|
+
return Array.isArray(roots) ? roots : [];
|
|
216
|
+
} catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
184
221
|
class IsolationManager {
|
|
185
222
|
constructor(options = {}) {
|
|
186
223
|
this.image = options.image || DEFAULT_IMAGE;
|
|
@@ -234,35 +271,48 @@ class IsolationManager {
|
|
|
234
271
|
return runningContainerId;
|
|
235
272
|
}
|
|
236
273
|
|
|
237
|
-
this._removeContainerByName(containerName);
|
|
238
|
-
|
|
239
|
-
workDir = await this._prepareIsolatedWorkspace(clusterId, workDir, reuseExisting);
|
|
240
|
-
|
|
241
274
|
const settings = loadSettings();
|
|
242
275
|
const providerName = normalizeProviderName(
|
|
243
276
|
config.provider || settings.defaultProvider || getDefaultProviderId()
|
|
244
277
|
);
|
|
245
278
|
const containerHome = config.containerHome || settings.dockerContainerHome || '/root';
|
|
246
279
|
|
|
247
|
-
|
|
248
|
-
|
|
280
|
+
// Pre-effect auth gate. The effective env/mount plan is computed (read-only) and validated
|
|
281
|
+
// BEFORE the stale-container removal and the isolated-workspace copy, so a missing or
|
|
282
|
+
// malformed credential plan leaves no container or workspace side effect behind — matching
|
|
283
|
+
// the platform probe's ordering in orchestrator/agent-lifecycle/preflight.
|
|
284
|
+
const credentialPlan = this._buildCredentialPlan(config, settings, containerHome, providerName);
|
|
285
|
+
this._assertProviderCredentialPlan(providerName, {
|
|
286
|
+
...credentialPlan,
|
|
287
|
+
config,
|
|
288
|
+
containerHome,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
this._removeContainerByName(containerName);
|
|
292
|
+
|
|
293
|
+
workDir = await this._prepareIsolatedWorkspace(clusterId, workDir, reuseExisting);
|
|
294
|
+
|
|
295
|
+
// The cluster config dir carries Claude credentials (via provisionClaudeCredentials) and the
|
|
296
|
+
// Claude-specific AskUserQuestion-blocking hook (~/.claude/settings.json PreToolUse), which
|
|
297
|
+
// only the `claude` CLI reads. Creating and mounting it for every provider — regardless of
|
|
298
|
+
// whether Claude is even running in the container — was an unconditional Claude-auth side
|
|
299
|
+
// channel into other providers' containers (e.g. omp, whose Docker isolation must be
|
|
300
|
+
// env/broker-only with zero automatic mounts). Scope it to the claude provider only.
|
|
301
|
+
const clusterConfigDir =
|
|
302
|
+
providerName === 'claude' ? this._createClusterConfigDir(clusterId, containerHome) : null;
|
|
303
|
+
if (clusterConfigDir) {
|
|
304
|
+
console.log(`[IsolationManager] Created cluster config dir at ${clusterConfigDir}`);
|
|
305
|
+
}
|
|
249
306
|
|
|
250
307
|
const args = this._buildBaseDockerArgs({
|
|
251
308
|
containerName,
|
|
252
309
|
workDir,
|
|
253
310
|
containerHome,
|
|
254
311
|
clusterConfigDir,
|
|
312
|
+
platform: config.platform,
|
|
255
313
|
});
|
|
256
314
|
|
|
257
|
-
|
|
258
|
-
args,
|
|
259
|
-
config,
|
|
260
|
-
settings,
|
|
261
|
-
containerHome,
|
|
262
|
-
providerName
|
|
263
|
-
);
|
|
264
|
-
this._warnMissingProviderCredentials(providerName, mountedHosts, config, containerHome);
|
|
265
|
-
|
|
315
|
+
args.push(...credentialPlan.args);
|
|
266
316
|
args.push('-w', '/workspace', image, 'tail', '-f', '/dev/null');
|
|
267
317
|
|
|
268
318
|
const containerId = await this._spawnContainer(clusterId, args, workDir);
|
|
@@ -340,21 +390,26 @@ class IsolationManager {
|
|
|
340
390
|
return isolatedDir;
|
|
341
391
|
}
|
|
342
392
|
|
|
343
|
-
_buildBaseDockerArgs({ containerName, workDir, containerHome, clusterConfigDir }) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
'
|
|
347
|
-
|
|
348
|
-
|
|
393
|
+
_buildBaseDockerArgs({ containerName, workDir, containerHome, clusterConfigDir, platform }) {
|
|
394
|
+
const args = ['run', '-d', '--name', containerName];
|
|
395
|
+
if (platform) {
|
|
396
|
+
args.push('--platform', platform);
|
|
397
|
+
}
|
|
398
|
+
args.push(
|
|
349
399
|
'-v',
|
|
350
400
|
`${workDir}:/workspace`,
|
|
351
401
|
'-v',
|
|
352
402
|
'/var/run/docker.sock:/var/run/docker.sock',
|
|
353
403
|
'--group-add',
|
|
354
|
-
this._getDockerGid()
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
404
|
+
this._getDockerGid()
|
|
405
|
+
);
|
|
406
|
+
// Only mounted when the active provider is claude (see createContainer) — carries Claude
|
|
407
|
+
// credentials and the Claude-specific AskUserQuestion-blocking hook, neither of which any
|
|
408
|
+
// other provider's CLI reads.
|
|
409
|
+
if (clusterConfigDir) {
|
|
410
|
+
args.push('-v', `${clusterConfigDir}:${containerHome}/.claude`);
|
|
411
|
+
}
|
|
412
|
+
return args;
|
|
358
413
|
}
|
|
359
414
|
|
|
360
415
|
_resolveMountConfig(config, settings) {
|
|
@@ -374,26 +429,56 @@ class IsolationManager {
|
|
|
374
429
|
return settings.dockerMounts;
|
|
375
430
|
}
|
|
376
431
|
|
|
377
|
-
// Auto-activate the running provider's own credential preset (mount
|
|
378
|
-
// without listing it in dockerMounts.
|
|
432
|
+
// Auto-activate the running provider's own credential preset (mount and/or env) so `--docker`
|
|
433
|
+
// works without listing it in dockerMounts. Env-only providers (e.g. omp) have no MOUNT_PRESETS
|
|
434
|
+
// entry, so both preset maps are checked.
|
|
379
435
|
_withActiveProviderPreset(mountConfig, providerName) {
|
|
380
|
-
if (!providerName
|
|
381
|
-
if (!MOUNT_PRESETS[providerName]) return mountConfig;
|
|
436
|
+
if (!providerName) return mountConfig;
|
|
437
|
+
if (!MOUNT_PRESETS[providerName] && !ENV_PRESETS[providerName]) return mountConfig;
|
|
382
438
|
if (mountConfig.some((item) => item === providerName)) return mountConfig;
|
|
383
439
|
return [...mountConfig, providerName];
|
|
384
440
|
}
|
|
385
441
|
|
|
386
|
-
|
|
387
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Compute the *effective* credential plan for a container — the exact `-v`/`-e` argv the
|
|
444
|
+
* container would receive, plus which of it the user explicitly opted into — WITHOUT applying
|
|
445
|
+
* any side effect. Callers validate the plan (see `_assertProviderCredentialPlan`) before
|
|
446
|
+
* touching containers or workspaces, then splice `plan.args` into the final argv.
|
|
447
|
+
*
|
|
448
|
+
* `forwardedEnv` holds the ACTUAL values the container would receive, so a forced-empty entry
|
|
449
|
+
* (`dockerEnvPassthrough: ["OPENAI_API_KEY="]`) is distinguishable from a real key. Values stay
|
|
450
|
+
* internal — they are never logged or included in any error message.
|
|
451
|
+
*
|
|
452
|
+
* @param {object} config
|
|
453
|
+
* @param {object} settings
|
|
454
|
+
* @param {string} containerHome
|
|
455
|
+
* @param {string} providerName
|
|
456
|
+
* @returns {{args: string[], mountedHosts: string[], explicitMountContainerPaths: string[],
|
|
457
|
+
* forwardedEnv: Record<string, string>, explicitEnvNames: Set<string>}}
|
|
458
|
+
*/
|
|
459
|
+
_buildCredentialPlan(config, settings, containerHome, providerName) {
|
|
460
|
+
const plan = {
|
|
461
|
+
args: [],
|
|
462
|
+
mountedHosts: [],
|
|
463
|
+
explicitMountContainerPaths: [],
|
|
464
|
+
forwardedEnv: {},
|
|
465
|
+
explicitEnvNames: new Set(),
|
|
466
|
+
};
|
|
467
|
+
|
|
388
468
|
if (config.noMounts) {
|
|
389
|
-
return
|
|
469
|
+
return plan;
|
|
390
470
|
}
|
|
391
471
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
);
|
|
472
|
+
// The user's own config, before the running provider's preset is auto-activated. Anything
|
|
473
|
+
// sourced from here is an *explicit* opt-in; anything added by `_withActiveProviderPreset` is
|
|
474
|
+
// automatic. Credential accounting depends on that distinction.
|
|
475
|
+
const userMountConfig = this._resolveMountConfig(config, settings);
|
|
476
|
+
const mountConfig = this._withActiveProviderPreset(userMountConfig, providerName);
|
|
477
|
+
|
|
396
478
|
const mounts = resolveMounts(mountConfig, { containerHome });
|
|
479
|
+
const explicitContainerPaths = new Set(
|
|
480
|
+
resolveMounts(userMountConfig, { containerHome }).map((mount) => mount.container)
|
|
481
|
+
);
|
|
397
482
|
const claudeContainerPath = path.posix.join(containerHome, '.claude');
|
|
398
483
|
|
|
399
484
|
for (const mount of mounts) {
|
|
@@ -419,21 +504,52 @@ class IsolationManager {
|
|
|
419
504
|
const mountSpec = mount.readonly
|
|
420
505
|
? `${hostPath}:${mount.container}:ro`
|
|
421
506
|
: `${hostPath}:${mount.container}`;
|
|
422
|
-
args.push('-v', mountSpec);
|
|
423
|
-
mountedHosts.push(hostPath);
|
|
507
|
+
plan.args.push('-v', mountSpec);
|
|
508
|
+
plan.mountedHosts.push(hostPath);
|
|
509
|
+
if (explicitContainerPaths.has(mount.container)) {
|
|
510
|
+
plan.explicitMountContainerPaths.push(mount.container);
|
|
511
|
+
}
|
|
424
512
|
}
|
|
425
513
|
|
|
426
|
-
const envToPass = this._collectDockerEnvVars(
|
|
514
|
+
const { envToPass, explicitNames } = this._collectDockerEnvVars(
|
|
515
|
+
mountConfig,
|
|
516
|
+
userMountConfig,
|
|
517
|
+
settings
|
|
518
|
+
);
|
|
427
519
|
for (const [key, value] of Object.entries(envToPass)) {
|
|
428
|
-
args.push('-e', `${key}=${value}`);
|
|
520
|
+
plan.args.push('-e', `${key}=${value}`);
|
|
521
|
+
plan.forwardedEnv[key] = value;
|
|
429
522
|
}
|
|
523
|
+
plan.explicitEnvNames = explicitNames;
|
|
430
524
|
|
|
431
|
-
return
|
|
525
|
+
return plan;
|
|
432
526
|
}
|
|
433
527
|
|
|
434
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Apply the credential plan to an argv array. Thin wrapper around `_buildCredentialPlan` kept
|
|
530
|
+
* for callers that build argv incrementally.
|
|
531
|
+
*/
|
|
532
|
+
_applyCredentialMounts(args, config, settings, containerHome, providerName) {
|
|
533
|
+
const plan = this._buildCredentialPlan(config, settings, containerHome, providerName);
|
|
534
|
+
args.push(...plan.args);
|
|
535
|
+
return plan;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @param {Array<string|object>} mountConfig - effective config (user's + auto provider preset)
|
|
540
|
+
* @param {Array<string|object>} userMountConfig - the user's config only
|
|
541
|
+
* @param {object} settings
|
|
542
|
+
* @returns {{envToPass: Record<string, string>, explicitNames: Set<string>}}
|
|
543
|
+
*/
|
|
544
|
+
_collectDockerEnvVars(mountConfig, userMountConfig, settings) {
|
|
435
545
|
const envToPass = {};
|
|
436
546
|
const envSpecs = expandEnvPatterns(resolveEnvs(mountConfig, settings.dockerEnvPassthrough));
|
|
547
|
+
// Names the user opted into by name (dockerEnvPassthrough) or by explicitly listing a preset,
|
|
548
|
+
// as opposed to the running provider's automatically-activated preset.
|
|
549
|
+
const explicitSpecs = expandEnvPatterns(
|
|
550
|
+
resolveEnvs(userMountConfig, settings.dockerEnvPassthrough)
|
|
551
|
+
);
|
|
552
|
+
const explicitNames = new Set(explicitSpecs.map((spec) => spec.name));
|
|
437
553
|
|
|
438
554
|
for (const spec of envSpecs) {
|
|
439
555
|
if (spec.forced) {
|
|
@@ -443,69 +559,215 @@ class IsolationManager {
|
|
|
443
559
|
}
|
|
444
560
|
}
|
|
445
561
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
envToPass[key] = value;
|
|
562
|
+
// Claude's own auth resolution only applies when Claude's preset is actually active (either
|
|
563
|
+
// as the running provider or explicitly configured) — never as an unconditional side channel
|
|
564
|
+
// into another provider's container (e.g. omp).
|
|
565
|
+
if (mountConfig.includes('claude')) {
|
|
566
|
+
const authEnv = resolveClaudeAuth(settings);
|
|
567
|
+
for (const [key, value] of Object.entries(authEnv)) {
|
|
568
|
+
if (!(key in envToPass)) {
|
|
569
|
+
envToPass[key] = value;
|
|
570
|
+
}
|
|
456
571
|
}
|
|
457
572
|
}
|
|
458
573
|
|
|
459
|
-
return envToPass;
|
|
574
|
+
return { envToPass, explicitNames };
|
|
460
575
|
}
|
|
461
576
|
|
|
462
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Decide whether the running provider has usable credentials in the *effective* container plan
|
|
579
|
+
* (what would actually be mounted/forwarded, with actual values), not host presence. Providers
|
|
580
|
+
* with no `docker.mount` (env-only, e.g. omp) fail closed — throw with remediation and never
|
|
581
|
+
* fall back to another provider. All other providers keep today's non-fatal warning.
|
|
582
|
+
*
|
|
583
|
+
* A credential counts when ALL of the following hold:
|
|
584
|
+
* - it is a registry-known credential env key for this provider, AND
|
|
585
|
+
* - it is in the provider's automatic allowlist (`docker.envAuth.requireOneOf`) OR the user
|
|
586
|
+
* explicitly opted it in (dockerEnvPassthrough / an explicitly listed preset), AND
|
|
587
|
+
* - its forwarded value is non-empty/non-whitespace, AND
|
|
588
|
+
* - if that value is an absolute container path (a *path* credential), an explicitly
|
|
589
|
+
* configured mount actually provides that path inside the container.
|
|
590
|
+
*
|
|
591
|
+
* Hard plan defects (a partial required pair, a non-http(s) broker URL) are never compensated
|
|
592
|
+
* for by another credential.
|
|
593
|
+
*
|
|
594
|
+
* Error/warning text names variables and paths from the *configuration*, never a forwarded
|
|
595
|
+
* value.
|
|
596
|
+
*
|
|
597
|
+
* @param {string} providerName
|
|
598
|
+
* @param {{mountedHosts: string[], explicitMountContainerPaths: string[],
|
|
599
|
+
* forwardedEnv: Record<string, string>, explicitEnvNames: Set<string>,
|
|
600
|
+
* config: object, containerHome: string}} plan
|
|
601
|
+
*/
|
|
602
|
+
_assertProviderCredentialPlan(
|
|
603
|
+
providerName,
|
|
604
|
+
{
|
|
605
|
+
mountedHosts,
|
|
606
|
+
explicitMountContainerPaths = [],
|
|
607
|
+
forwardedEnv,
|
|
608
|
+
explicitEnvNames = new Set(),
|
|
609
|
+
config,
|
|
610
|
+
containerHome,
|
|
611
|
+
}
|
|
612
|
+
) {
|
|
463
613
|
if (providerName === 'claude') {
|
|
464
614
|
return;
|
|
465
615
|
}
|
|
466
616
|
|
|
467
617
|
const metadata = getProviderMetadata(providerName);
|
|
468
618
|
const provider = getProvider(providerName);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
619
|
+
const docker = metadata.docker || {};
|
|
620
|
+
|
|
621
|
+
// Structural env/broker validation: required-pair completeness and URL shape.
|
|
622
|
+
const envAuthResult = validateProviderEnvAuth(providerName, forwardedEnv);
|
|
623
|
+
const { satisfying, notOptedIn, unmountedPath } = this._classifyForwardedCredentials(metadata, {
|
|
624
|
+
forwardedEnv,
|
|
625
|
+
explicitEnvNames,
|
|
626
|
+
explicitMountContainerPaths,
|
|
627
|
+
});
|
|
475
628
|
|
|
476
629
|
// A mount only counts if it carries the secret (credentialInMount !== false).
|
|
477
|
-
const credentialInMount = metadata.docker && metadata.docker.credentialInMount === false;
|
|
478
630
|
const credentialPaths = provider.getCredentialPaths ? provider.getCredentialPaths() : [];
|
|
479
631
|
const expandedCreds = credentialPaths.map((cred) => expandHomePath(cred));
|
|
480
632
|
const hasCredentialMount =
|
|
481
|
-
|
|
633
|
+
docker.credentialInMount !== false &&
|
|
482
634
|
mountedHosts.some((hostPath) =>
|
|
483
635
|
expandedCreds.some(
|
|
484
636
|
(credPath) => pathContains(hostPath, credPath) || pathContains(credPath, hostPath)
|
|
485
637
|
)
|
|
486
638
|
);
|
|
487
|
-
|
|
639
|
+
|
|
640
|
+
if (envAuthResult.malformed.length === 0 && (satisfying.length > 0 || hasCredentialMount)) {
|
|
488
641
|
return;
|
|
489
642
|
}
|
|
490
643
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
`${credentialEnvKeys.join(', ')} before running with --docker.`
|
|
644
|
+
const reasons = [...envAuthResult.malformed];
|
|
645
|
+
if (unmountedPath.length > 0) {
|
|
646
|
+
reasons.push(
|
|
647
|
+
`${unmountedPath.join(', ')} points at a container path that no explicit --mount provides`
|
|
496
648
|
);
|
|
497
|
-
return;
|
|
498
649
|
}
|
|
650
|
+
if (notOptedIn.length > 0) {
|
|
651
|
+
reasons.push(
|
|
652
|
+
`${notOptedIn.join(', ')} (known ${provider.displayName} credentials outside the ` +
|
|
653
|
+
'automatic allowlist) were not explicitly opted in'
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (reasons.length === 0) {
|
|
657
|
+
reasons.push('no credential env var or mount found in the effective container plan');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const mountNote = config.noMounts ? 'Credential mounts are disabled. ' : '';
|
|
661
|
+
const allowlist = docker.envPassthrough || [];
|
|
662
|
+
const message =
|
|
663
|
+
`${mountNote}No usable credentials found for ${provider.displayName} in the effective ` +
|
|
664
|
+
`Docker env/mount plan (${reasons.join('; ')}). Automatic env allowlist: ` +
|
|
665
|
+
`${allowlist.join(', ') || '(none)'}. ` +
|
|
666
|
+
this._credentialRemediation(provider, docker, credentialPaths, containerHome);
|
|
667
|
+
|
|
668
|
+
// Env-only providers (no automatic mount) have no fallback credential surface, so an
|
|
669
|
+
// unsatisfied/malformed plan must fail closed before the container ever starts.
|
|
670
|
+
if (!docker.mount) {
|
|
671
|
+
throw new Error(`[IsolationManager] ${message}`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
console.warn(`[IsolationManager] ⚠️ ${message}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Split this provider's registry-known credential env vars into the ones that actually
|
|
679
|
+
* authenticate the container and the two ways they can fail to.
|
|
680
|
+
*
|
|
681
|
+
* @param {object} metadata - registry provider metadata
|
|
682
|
+
* @param {{forwardedEnv: Record<string, string>, explicitEnvNames: Set<string>,
|
|
683
|
+
* explicitMountContainerPaths: string[]}} plan
|
|
684
|
+
* @returns {{satisfying: string[], notOptedIn: string[], unmountedPath: string[]}}
|
|
685
|
+
* `notOptedIn`: a known credential carrying a real value that is neither on the automatic
|
|
686
|
+
* allowlist nor explicitly passed through. `unmountedPath`: a path credential whose container
|
|
687
|
+
* path no explicit mount provides — the file simply is not there.
|
|
688
|
+
*/
|
|
689
|
+
_classifyForwardedCredentials(
|
|
690
|
+
metadata,
|
|
691
|
+
{ forwardedEnv, explicitEnvNames, explicitMountContainerPaths }
|
|
692
|
+
) {
|
|
693
|
+
const docker = metadata.docker || {};
|
|
694
|
+
const envAuth = docker.envAuth;
|
|
695
|
+
const automatic = new Set(envAuth ? envAuth.requireOneOf : []);
|
|
696
|
+
const usable = (metadata.credentialEnvKeys || []).filter((key) =>
|
|
697
|
+
isUsableEnvValue(forwardedEnv[key])
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
const satisfying = [];
|
|
701
|
+
const notOptedIn = [];
|
|
702
|
+
const unmountedPath = [];
|
|
499
703
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
704
|
+
for (const name of usable) {
|
|
705
|
+
// A registry-known credential outside the automatic allowlist is usable only when explicitly
|
|
706
|
+
// opted in — that is the "custom env credential requires explicit passthrough" rule, not a
|
|
707
|
+
// reason for it to stay permanently unusable.
|
|
708
|
+
if (envAuth && !automatic.has(name) && !explicitEnvNames.has(name)) {
|
|
709
|
+
notOptedIn.push(name);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
// A path credential (value is an absolute container path) is only real when an explicitly
|
|
713
|
+
// configured mount actually provides that path. Host-side existence proves nothing about
|
|
714
|
+
// what the container can read.
|
|
715
|
+
const value = forwardedEnv[name];
|
|
716
|
+
const isContainerPath = value.startsWith('/');
|
|
717
|
+
if (
|
|
718
|
+
isContainerPath &&
|
|
719
|
+
!explicitMountContainerPaths.some((containerPath) => pathContains(containerPath, value))
|
|
720
|
+
) {
|
|
721
|
+
unmountedPath.push(name);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
satisfying.push(name);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return { satisfying, notOptedIn, unmountedPath };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Remediation sentence for a provider with no usable credential plan.
|
|
732
|
+
*
|
|
733
|
+
* Env-only providers (no `docker.mount`) must NEVER be told to mount their host auth store —
|
|
734
|
+
* not mounting/copying it is the whole point of their Docker contract. They get the broker
|
|
735
|
+
* pair (when the registry declares one) plus a generic custom-path example instead. Mount-based
|
|
736
|
+
* providers keep the concrete "mount your credential dir" hint.
|
|
737
|
+
*
|
|
738
|
+
* @param {{displayName: string}} provider
|
|
739
|
+
* @param {object} docker - registry docker metadata
|
|
740
|
+
* @param {readonly string[]} credentialPaths
|
|
741
|
+
* @param {string} containerHome
|
|
742
|
+
* @returns {string}
|
|
743
|
+
*/
|
|
744
|
+
_credentialRemediation(provider, docker, credentialPaths, containerHome) {
|
|
745
|
+
const customEnvHint =
|
|
746
|
+
`Any other credential needs explicit opt-in: ` +
|
|
747
|
+
`zeroshot settings set dockerEnvPassthrough '["MY_KEY"]'`;
|
|
748
|
+
const genericPathHint =
|
|
749
|
+
`for a file credential also mount it and point the var at the container path: ` +
|
|
750
|
+
`--mount /host/path/to/credential:${path.posix.join(containerHome, 'credential')}:ro ` +
|
|
751
|
+
`with dockerEnvPassthrough '["MY_PATH_CREDENTIAL=${path.posix.join(containerHome, 'credential')}"]'`;
|
|
752
|
+
|
|
753
|
+
if (!docker.mount) {
|
|
754
|
+
const brokerPair =
|
|
755
|
+
(docker.envAuth && docker.envAuth.requireTogether && docker.envAuth.requireTogether[0]) ||
|
|
756
|
+
null;
|
|
757
|
+
const brokerHint = brokerPair
|
|
758
|
+
? `Prefer the auth broker (${brokerPair.join(' + ')}) so host refresh tokens never cross. `
|
|
759
|
+
: '';
|
|
760
|
+
return (
|
|
761
|
+
`Export one of the listed vars. ${brokerHint}${customEnvHint}, or ${genericPathHint}. ` +
|
|
762
|
+
`${provider.displayName}'s host auth store is never mounted or copied into the container.`
|
|
507
763
|
);
|
|
508
764
|
}
|
|
765
|
+
|
|
766
|
+
const exampleHost = credentialPaths[0];
|
|
767
|
+
const mountHint = exampleHost
|
|
768
|
+
? ` or --mount ${exampleHost}:${exampleHost.replace(/^~(?=\/|$)/, containerHome)}:ro for a custom path credential`
|
|
769
|
+
: '';
|
|
770
|
+
return `Export one of the listed vars, or add a custom credential with ${customEnvHint}${mountHint}.`;
|
|
509
771
|
}
|
|
510
772
|
|
|
511
773
|
_spawnContainer(clusterId, args, workDir) {
|
|
@@ -1374,40 +1636,207 @@ class IsolationManager {
|
|
|
1374
1636
|
}
|
|
1375
1637
|
}
|
|
1376
1638
|
|
|
1639
|
+
/**
|
|
1640
|
+
* Registry-owned Docker platform for a provider (e.g. 'linux/amd64'), or null when the
|
|
1641
|
+
* provider declares no `docker.platform` (host-native, unset `--platform`).
|
|
1642
|
+
* @param {string} providerName
|
|
1643
|
+
* @returns {string|null}
|
|
1644
|
+
*/
|
|
1645
|
+
static providerDockerPlatform(providerName) {
|
|
1646
|
+
return providerDockerPlatform(providerName);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Registry-owned config/overlay roots for a provider, with $HOME expanded to containerHome.
|
|
1651
|
+
* @param {string} providerName
|
|
1652
|
+
* @param {string} [containerHome]
|
|
1653
|
+
* @returns {string[]}
|
|
1654
|
+
*/
|
|
1655
|
+
static providerConfigRoots(providerName, containerHome = '/root') {
|
|
1656
|
+
return providerDockerConfigRootsRaw(providerName).map((root) =>
|
|
1657
|
+
root.replace(/\$HOME/g, containerHome)
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Parse the exact platform tokens advertised by `docker buildx inspect`.
|
|
1663
|
+
*
|
|
1664
|
+
* The `Platforms:` line is comma-delimited, and buildx marks preferred entries with a trailing
|
|
1665
|
+
* `*`. Tokens are returned verbatim (minus that marker) so callers can compare exactly:
|
|
1666
|
+
* `linux/amd64/v2` is a *variant*, not `linux/amd64`, and a substring test would wrongly accept
|
|
1667
|
+
* a builder that only advertises the variant. Multiple builder nodes each emit their own
|
|
1668
|
+
* `Platforms:` line; all are collected.
|
|
1669
|
+
*
|
|
1670
|
+
* @param {string} buildxOutput
|
|
1671
|
+
* @returns {string[]}
|
|
1672
|
+
*/
|
|
1673
|
+
static parseBuildxPlatforms(buildxOutput) {
|
|
1674
|
+
const platforms = [];
|
|
1675
|
+
for (const line of (buildxOutput || '').split('\n')) {
|
|
1676
|
+
const trimmed = line.trim();
|
|
1677
|
+
if (!trimmed.startsWith('Platforms:')) {
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
for (const token of trimmed.slice('Platforms:'.length).split(',')) {
|
|
1681
|
+
const platform = token.trim().replace(/\*$/, '');
|
|
1682
|
+
if (platform) {
|
|
1683
|
+
platforms.push(platform);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return platforms;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Pre-effect probe: throws before any workspace/container side effect when the Docker engine
|
|
1692
|
+
* cannot run `platform`. No-op when `platform` is null (provider declares no platform
|
|
1693
|
+
* requirement). Native arch match satisfies the platform directly; otherwise a Buildx builder
|
|
1694
|
+
* advertising exactly that platform (emulation) is required.
|
|
1695
|
+
* @param {string|null} platform
|
|
1696
|
+
* @param {{info?: () => string, buildxInspect?: () => string}} [probe] - command runners,
|
|
1697
|
+
* injectable for tests; defaults to the real `docker info` / `docker buildx inspect`
|
|
1698
|
+
*/
|
|
1699
|
+
static assertPlatformSupported(platform, probe = {}) {
|
|
1700
|
+
if (!platform) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const runInfo =
|
|
1705
|
+
probe.info ||
|
|
1706
|
+
(() =>
|
|
1707
|
+
runSync('docker', ['info', '--format', '{{.OSType}}|{{.Architecture}}'], {
|
|
1708
|
+
encoding: 'utf8',
|
|
1709
|
+
stdio: 'pipe',
|
|
1710
|
+
}));
|
|
1711
|
+
const runBuildxInspect =
|
|
1712
|
+
probe.buildxInspect ||
|
|
1713
|
+
(() => runSync('docker', ['buildx', 'inspect'], { encoding: 'utf8', stdio: 'pipe' }));
|
|
1714
|
+
|
|
1715
|
+
let info;
|
|
1716
|
+
try {
|
|
1717
|
+
info = String(runInfo()).trim();
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
throw new Error(`Cannot determine Docker engine platform: ${err.message}`);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const [osType, arch] = info.split('|').map((part) => (part || '').trim());
|
|
1723
|
+
const requiredArch = platform.split('/')[1] || '';
|
|
1724
|
+
const nativeMatch = arch === requiredArch || (requiredArch === 'amd64' && arch === 'x86_64');
|
|
1725
|
+
|
|
1726
|
+
if (osType === 'linux' && nativeMatch) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
let buildxOutput = '';
|
|
1731
|
+
if (osType === 'linux') {
|
|
1732
|
+
try {
|
|
1733
|
+
buildxOutput = String(runBuildxInspect());
|
|
1734
|
+
} catch {
|
|
1735
|
+
buildxOutput = '';
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
if (
|
|
1740
|
+
osType === 'linux' &&
|
|
1741
|
+
IsolationManager.parseBuildxPlatforms(buildxOutput).includes(platform)
|
|
1742
|
+
) {
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
throw new Error(
|
|
1747
|
+
`Docker engine cannot run ${platform} (server ${osType || 'unknown'}/${arch || 'unknown'}, ` +
|
|
1748
|
+
`no buildx builder advertising ${platform}). Install Buildx and run: ` +
|
|
1749
|
+
`docker run --privileged --rm tonistiigi/binfmt --install ${requiredArch || platform}`
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Split a Docker image reference into `{name, tag, digest}` per the reference grammar
|
|
1755
|
+
* `[registry[:port]/]name[:tag][@digest]`.
|
|
1756
|
+
*
|
|
1757
|
+
* The only ambiguity is `:` — it is a registry port when it appears before the last `/`, and a
|
|
1758
|
+
* tag separator only when it appears after it. `registry.example:5000/base` therefore has no
|
|
1759
|
+
* tag, while `base:v2` does.
|
|
1760
|
+
*
|
|
1761
|
+
* @param {string} reference
|
|
1762
|
+
* @returns {{name: string, tag: string|null, digest: string|null}}
|
|
1763
|
+
*/
|
|
1764
|
+
static parseImageReference(reference) {
|
|
1765
|
+
const atIndex = reference.indexOf('@');
|
|
1766
|
+
const digest = atIndex === -1 ? null : reference.slice(atIndex + 1);
|
|
1767
|
+
const withoutDigest = atIndex === -1 ? reference : reference.slice(0, atIndex);
|
|
1768
|
+
|
|
1769
|
+
const lastSlash = withoutDigest.lastIndexOf('/');
|
|
1770
|
+
const colonIndex = withoutDigest.indexOf(':', lastSlash + 1);
|
|
1771
|
+
const name = colonIndex === -1 ? withoutDigest : withoutDigest.slice(0, colonIndex);
|
|
1772
|
+
const tag = colonIndex === -1 ? null : withoutDigest.slice(colonIndex + 1);
|
|
1773
|
+
|
|
1774
|
+
return { name, tag, digest };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1377
1777
|
/**
|
|
1378
1778
|
* Resolve the cluster image tag for a provider. Providers baked into the base image (e.g.
|
|
1379
1779
|
* Claude) run on the base image directly; providers with a `docker.install` command get a
|
|
1380
|
-
* per-provider image variant `<
|
|
1381
|
-
*
|
|
1780
|
+
* per-provider image variant `<baseName>-<providerId>-<hash>`.
|
|
1781
|
+
*
|
|
1782
|
+
* `<baseName>` is the base reference's NAME only. A tag or digest may not be carried over: the
|
|
1783
|
+
* derived value is a new locally-built tag, and `registry/base@sha256:…-omp-<hash>` /
|
|
1784
|
+
* `base:v2-omp-<hash>` are not valid references (the first is a malformed digest, the second
|
|
1785
|
+
* silently reinterprets the tag). A registry port is preserved because it belongs to the name
|
|
1786
|
+
* (`registry.example:5000/base` → `registry.example:5000/base-omp-<hash>`).
|
|
1787
|
+
*
|
|
1788
|
+
* `<hash>` covers the FULL base reference (tag and digest included) alongside the install
|
|
1789
|
+
* command and platform, so two different pins of the same base name — or a pinned-version or
|
|
1790
|
+
* platform change — never collide on one cached tag.
|
|
1791
|
+
*
|
|
1382
1792
|
* @param {string} providerName
|
|
1383
1793
|
* @param {string} [baseImage]
|
|
1384
1794
|
* @returns {string}
|
|
1385
1795
|
*/
|
|
1386
1796
|
static imageForProvider(providerName, baseImage = DEFAULT_IMAGE) {
|
|
1387
|
-
|
|
1797
|
+
const install = providerDockerInstall(providerName);
|
|
1798
|
+
if (!install) {
|
|
1388
1799
|
return baseImage;
|
|
1389
1800
|
}
|
|
1390
|
-
|
|
1801
|
+
const platform = providerDockerPlatform(providerName) || '';
|
|
1802
|
+
const { name } = IsolationManager.parseImageReference(baseImage);
|
|
1803
|
+
const hash = crypto
|
|
1804
|
+
.createHash('sha256')
|
|
1805
|
+
.update(`${baseImage}\n${platform}\n${install}`)
|
|
1806
|
+
.digest('hex')
|
|
1807
|
+
.slice(0, 12);
|
|
1808
|
+
return `${name}-${normalizeProviderName(providerName)}-${hash}`;
|
|
1391
1809
|
}
|
|
1392
1810
|
|
|
1393
1811
|
/**
|
|
1394
|
-
* Docker `--build-arg` values that install a provider's CLI
|
|
1395
|
-
* the provider is baked into the base image or has no installer.
|
|
1812
|
+
* Docker `--build-arg` values that install a provider's CLI (and create its config roots) in
|
|
1813
|
+
* its image variant, or [] when the provider is baked into the base image or has no installer.
|
|
1396
1814
|
* @param {string} providerName
|
|
1815
|
+
* @param {string} [containerHome]
|
|
1397
1816
|
* @returns {string[]}
|
|
1398
1817
|
*/
|
|
1399
|
-
static providerBuildArgs(providerName) {
|
|
1818
|
+
static providerBuildArgs(providerName, containerHome = '/home/node') {
|
|
1400
1819
|
const install = providerDockerInstall(providerName);
|
|
1401
|
-
|
|
1820
|
+
if (!install) {
|
|
1821
|
+
return [];
|
|
1822
|
+
}
|
|
1823
|
+
const args = [`PROVIDER_INSTALL=${install}`];
|
|
1824
|
+
const configRoots = IsolationManager.providerConfigRoots(providerName, containerHome);
|
|
1825
|
+
if (configRoots.length > 0) {
|
|
1826
|
+
args.push(`PROVIDER_CONFIG_ROOTS=${configRoots.join(' ')}`);
|
|
1827
|
+
}
|
|
1828
|
+
return args;
|
|
1402
1829
|
}
|
|
1403
1830
|
|
|
1404
1831
|
/**
|
|
1405
1832
|
* Build the Docker image with retry logic
|
|
1406
1833
|
* @param {string} [image] - Image name to build
|
|
1407
1834
|
* @param {number} [maxRetries=3] - Maximum retry attempts
|
|
1835
|
+
* @param {string[]} [buildArgs] - `--build-arg KEY=VALUE` pairs
|
|
1836
|
+
* @param {string|null} [platform] - Registry-owned `--platform` value, or null for host-native
|
|
1408
1837
|
* @returns {Promise<void>}
|
|
1409
1838
|
*/
|
|
1410
|
-
static async buildImage(image = DEFAULT_IMAGE, maxRetries = 3, buildArgs = []) {
|
|
1839
|
+
static async buildImage(image = DEFAULT_IMAGE, maxRetries = 3, buildArgs = [], platform = null) {
|
|
1411
1840
|
// Repository root is one level up from src/
|
|
1412
1841
|
const repoRoot = path.join(__dirname, '..');
|
|
1413
1842
|
const dockerfilePath = path.join(repoRoot, 'docker', 'zeroshot-cluster', 'Dockerfile');
|
|
@@ -1421,6 +1850,7 @@ class IsolationManager {
|
|
|
1421
1850
|
for (const arg of buildArgs) {
|
|
1422
1851
|
buildArgFlags.push('--build-arg', arg);
|
|
1423
1852
|
}
|
|
1853
|
+
const platformFlags = platform ? ['--platform', platform] : [];
|
|
1424
1854
|
|
|
1425
1855
|
console.log(`[IsolationManager] Building Docker image '${image}'...`);
|
|
1426
1856
|
|
|
@@ -1432,7 +1862,16 @@ class IsolationManager {
|
|
|
1432
1862
|
// Use -f flag to specify Dockerfile location
|
|
1433
1863
|
runSync(
|
|
1434
1864
|
'docker',
|
|
1435
|
-
[
|
|
1865
|
+
[
|
|
1866
|
+
'build',
|
|
1867
|
+
'-f',
|
|
1868
|
+
'docker/zeroshot-cluster/Dockerfile',
|
|
1869
|
+
...platformFlags,
|
|
1870
|
+
...buildArgFlags,
|
|
1871
|
+
'-t',
|
|
1872
|
+
image,
|
|
1873
|
+
'.',
|
|
1874
|
+
],
|
|
1436
1875
|
{
|
|
1437
1876
|
cwd: repoRoot,
|
|
1438
1877
|
encoding: 'utf8',
|
|
@@ -1466,9 +1905,16 @@ class IsolationManager {
|
|
|
1466
1905
|
* Ensure Docker image exists, building it if necessary
|
|
1467
1906
|
* @param {string} [image] - Image name to ensure
|
|
1468
1907
|
* @param {boolean} [autoBuild=true] - Auto-build if missing
|
|
1908
|
+
* @param {string[]} [buildArgs] - `--build-arg KEY=VALUE` pairs
|
|
1909
|
+
* @param {string|null} [platform] - Registry-owned `--platform` value, or null for host-native
|
|
1469
1910
|
* @returns {Promise<void>}
|
|
1470
1911
|
*/
|
|
1471
|
-
static async ensureImage(
|
|
1912
|
+
static async ensureImage(
|
|
1913
|
+
image = DEFAULT_IMAGE,
|
|
1914
|
+
autoBuild = true,
|
|
1915
|
+
buildArgs = [],
|
|
1916
|
+
platform = null
|
|
1917
|
+
) {
|
|
1472
1918
|
if (this.imageExists(image)) {
|
|
1473
1919
|
return;
|
|
1474
1920
|
}
|
|
@@ -1481,7 +1927,7 @@ class IsolationManager {
|
|
|
1481
1927
|
}
|
|
1482
1928
|
|
|
1483
1929
|
console.log(`[IsolationManager] Image '${image}' not found, building automatically...`);
|
|
1484
|
-
await this.buildImage(image, 3, buildArgs);
|
|
1930
|
+
await this.buildImage(image, 3, buildArgs, platform);
|
|
1485
1931
|
}
|
|
1486
1932
|
|
|
1487
1933
|
/**
|