@agent-vm/agent-vm 0.0.69 → 0.0.70

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.
Files changed (87) hide show
  1. package/dist/build/managed-image-dockerfile.d.ts.map +1 -1
  2. package/dist/build/managed-image-dockerfile.js +6 -9
  3. package/dist/build/managed-image-dockerfile.js.map +1 -1
  4. package/dist/cli/agent-vm-cli-support.d.ts +2 -2
  5. package/dist/cli/agent-vm-cli-support.d.ts.map +1 -1
  6. package/dist/cli/agent-vm-cli-support.js +2 -1
  7. package/dist/cli/agent-vm-cli-support.js.map +1 -1
  8. package/dist/cli/commands/create-app.d.ts +62 -78
  9. package/dist/cli/commands/create-app.d.ts.map +1 -1
  10. package/dist/cli/commands/migrate-definition.d.ts +0 -16
  11. package/dist/cli/commands/migrate-definition.d.ts.map +1 -1
  12. package/dist/cli/commands/migrate-definition.js +1 -20
  13. package/dist/cli/commands/migrate-definition.js.map +1 -1
  14. package/dist/cli/init-command.d.ts.map +1 -1
  15. package/dist/cli/init-command.js +7 -37
  16. package/dist/cli/init-command.js.map +1 -1
  17. package/dist/cli/manual-templates.d.ts.map +1 -1
  18. package/dist/cli/manual-templates.js +9 -7
  19. package/dist/cli/manual-templates.js.map +1 -1
  20. package/dist/cli/migrate-commands.d.ts +0 -8
  21. package/dist/cli/migrate-commands.d.ts.map +1 -1
  22. package/dist/cli/migrate-commands.js +0 -274
  23. package/dist/cli/migrate-commands.js.map +1 -1
  24. package/dist/config/system-config.d.ts +2 -1
  25. package/dist/config/system-config.d.ts.map +1 -1
  26. package/dist/config/system-config.js +24 -4
  27. package/dist/config/system-config.js.map +1 -1
  28. package/dist/controller/controller-runtime-operations.d.ts +1 -1
  29. package/dist/controller/controller-runtime-operations.d.ts.map +1 -1
  30. package/dist/controller/controller-runtime-support.d.ts +1 -2
  31. package/dist/controller/controller-runtime-support.d.ts.map +1 -1
  32. package/dist/controller/controller-runtime-support.js +1 -2
  33. package/dist/controller/controller-runtime-support.js.map +1 -1
  34. package/dist/controller/controller-runtime-types.d.ts +1 -1
  35. package/dist/controller/controller-runtime-types.d.ts.map +1 -1
  36. package/dist/controller/controller-runtime.d.ts.map +1 -1
  37. package/dist/controller/controller-runtime.js +4 -4
  38. package/dist/controller/controller-runtime.js.map +1 -1
  39. package/dist/controller/http/controller-http-routes.d.ts +1 -1
  40. package/dist/controller/http/controller-http-routes.d.ts.map +1 -1
  41. package/dist/controller/leases/agent-sandbox-seeding.d.ts +1 -1
  42. package/dist/controller/leases/agent-sandbox-seeding.d.ts.map +1 -1
  43. package/dist/controller/worker-task-runner.d.ts +1 -1
  44. package/dist/controller/worker-task-runner.d.ts.map +1 -1
  45. package/dist/controller/zone-runtimes/openclaw-zone-runtime.d.ts +1 -1
  46. package/dist/controller/zone-runtimes/openclaw-zone-runtime.d.ts.map +1 -1
  47. package/dist/controller/zone-runtimes/worker-zone-runtime.d.ts +1 -1
  48. package/dist/controller/zone-runtimes/worker-zone-runtime.d.ts.map +1 -1
  49. package/dist/controller/zone-runtimes/zone-runtime-types.d.ts +2 -1
  50. package/dist/controller/zone-runtimes/zone-runtime-types.d.ts.map +1 -1
  51. package/dist/gateway/credential-manager.d.ts +1 -1
  52. package/dist/gateway/credential-manager.d.ts.map +1 -1
  53. package/dist/gateway/gateway-zone-orchestrator.d.ts.map +1 -1
  54. package/dist/gateway/gateway-zone-orchestrator.js +44 -16
  55. package/dist/gateway/gateway-zone-orchestrator.js.map +1 -1
  56. package/dist/gateway/gateway-zone-support.d.ts +1 -1
  57. package/dist/gateway/gateway-zone-support.d.ts.map +1 -1
  58. package/dist/gateway/gateway-zone-support.js +2 -1
  59. package/dist/gateway/gateway-zone-support.js.map +1 -1
  60. package/dist/gateway/mcp-portal-effective-config.d.ts +26 -0
  61. package/dist/gateway/mcp-portal-effective-config.d.ts.map +1 -0
  62. package/dist/gateway/mcp-portal-effective-config.js +237 -0
  63. package/dist/gateway/mcp-portal-effective-config.js.map +1 -0
  64. package/dist/integration-tests/smoke-harness.d.ts +14 -1
  65. package/dist/integration-tests/smoke-harness.d.ts.map +1 -1
  66. package/dist/integration-tests/smoke-harness.js +404 -87
  67. package/dist/integration-tests/smoke-harness.js.map +1 -1
  68. package/dist/operations/config-validation.d.ts.map +1 -1
  69. package/dist/operations/config-validation.js +32 -2
  70. package/dist/operations/config-validation.js.map +1 -1
  71. package/dist/operations/openclaw-deployment-doctor.d.ts +0 -6
  72. package/dist/operations/openclaw-deployment-doctor.d.ts.map +1 -1
  73. package/dist/operations/openclaw-deployment-doctor.js +9 -83
  74. package/dist/operations/openclaw-deployment-doctor.js.map +1 -1
  75. package/dist/tool-vm/tool-vm-lifecycle.d.ts +2 -1
  76. package/dist/tool-vm/tool-vm-lifecycle.d.ts.map +1 -1
  77. package/dist/tool-vm/tool-vm-lifecycle.js +1 -1
  78. package/dist/tool-vm/tool-vm-lifecycle.js.map +1 -1
  79. package/package.json +11 -10
  80. package/dist/controller/composite-secret-resolver.d.ts +0 -3
  81. package/dist/controller/composite-secret-resolver.d.ts.map +0 -1
  82. package/dist/controller/composite-secret-resolver.js +0 -103
  83. package/dist/controller/composite-secret-resolver.js.map +0 -1
  84. package/dist/gateway/mcp-portal-openclaw-materialization.d.ts +0 -19
  85. package/dist/gateway/mcp-portal-openclaw-materialization.d.ts.map +0 -1
  86. package/dist/gateway/mcp-portal-openclaw-materialization.js +0 -26
  87. package/dist/gateway/mcp-portal-openclaw-materialization.js.map +0 -1
@@ -1,16 +1,28 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFile, execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs/promises';
3
3
  import net from 'node:net';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
- import { resolveGondolinMinimumZigVersion, } from '@agent-vm/gondolin-adapter';
6
+ import { promisify } from 'node:util';
7
+ import { resolveGondolinMinimumZigVersion } from '@agent-vm/gondolin-adapter';
7
8
  import { computeFingerprintFromConfigPath } from '../build/gondolin-image-builder.js';
8
9
  import { resolveManagedImageRelease } from '../build/managed-image-dockerfile.js';
9
10
  import { isZigVersionAtLeast, resolveHostZigVersion } from '../build/zig-compatibility.js';
10
11
  import { scaffoldAgentVmProject } from '../cli/init-command.js';
12
+ import { loadJsonConfigFile } from '../config/json-config-file.js';
11
13
  import { loadSystemConfig } from '../config/system-config.js';
12
14
  import { startControllerRuntime } from '../controller/controller-runtime.js';
13
15
  import { startGatewayZone } from '../gateway/gateway-zone-orchestrator.js';
16
+ const defaultOpenClawMcpPortalExtensionsPath = '/home/openclaw/.openclaw/extensions/mcp-portal';
17
+ const execFileAsync = promisify(execFile);
18
+ const openClawMcpPortalPluginName = 'mcp-portal';
19
+ const smokeTempRootPrefixes = [
20
+ 'agent-vm-gateway-smoke-project-',
21
+ 'agent-vm-smoke-harness-',
22
+ 'openclaw-mcp-portal-smoke-',
23
+ 'openclaw-zone-git-smoke-',
24
+ 'worker-loop-smoke-',
25
+ ];
14
26
  export function hasCommand(command) {
15
27
  try {
16
28
  execFileSync('sh', ['-lc', `command -v ${command} >/dev/null`], { stdio: 'ignore' });
@@ -26,6 +38,22 @@ export function currentSmokeArchitecture() {
26
38
  export function qemuCommandForArchitecture(architecture) {
27
39
  return architecture === 'aarch64' ? 'qemu-system-aarch64' : 'qemu-system-x86_64';
28
40
  }
41
+ function isOwnedSmokeTempRoot(tempRoot) {
42
+ const resolvedTempRoot = path.resolve(tempRoot);
43
+ const resolvedSystemTempRoot = path.resolve(os.tmpdir());
44
+ if (resolvedTempRoot === resolvedSystemTempRoot ||
45
+ !resolvedTempRoot.startsWith(`${resolvedSystemTempRoot}${path.sep}`)) {
46
+ return false;
47
+ }
48
+ const basename = path.basename(resolvedTempRoot);
49
+ return smokeTempRootPrefixes.some((prefix) => basename.startsWith(prefix));
50
+ }
51
+ export async function removeSmokeTempRoot(tempRoot) {
52
+ if (!isOwnedSmokeTempRoot(tempRoot)) {
53
+ return;
54
+ }
55
+ await fs.rm(tempRoot, { force: true, recursive: true });
56
+ }
29
57
  export async function canRunGondolinSmoke(options) {
30
58
  const commandExists = options.commandExists ?? hasCommand;
31
59
  if (!commandExists(qemuCommandForArchitecture(options.architecture)) ||
@@ -92,11 +120,15 @@ export async function waitForControllerReady(controllerPort) {
92
120
  throw new Error('Controller did not become ready in time.');
93
121
  }
94
122
  export async function findReusableGatewayImageDirectory(currentProjectRoot, gatewayBuildConfigPath) {
123
+ const explicitSmokeCacheRoot = process.env.AGENT_VM_SMOKE_CACHE_DIR;
124
+ if (!explicitSmokeCacheRoot) {
125
+ return null;
126
+ }
95
127
  const requiredFingerprint = await computeFingerprintFromConfigPath(gatewayBuildConfigPath);
96
- const tempRootEntries = await fs.readdir(os.tmpdir(), { withFileTypes: true });
128
+ const tempRootEntries = await fs.readdir(explicitSmokeCacheRoot, { withFileTypes: true });
97
129
  const smokeRunDirectories = tempRootEntries
98
130
  .filter((entry) => entry.isDirectory() && entry.name.includes('-smoke-'))
99
- .map((entry) => path.join(os.tmpdir(), entry.name));
131
+ .map((entry) => path.join(explicitSmokeCacheRoot, entry.name));
100
132
  for (const smokeRunDirectory of smokeRunDirectories) {
101
133
  if (smokeRunDirectory === currentProjectRoot) {
102
134
  continue;
@@ -185,60 +217,211 @@ function getWorkerSmokeZone(systemConfig) {
185
217
  }
186
218
  return { ...zone, gateway: zone.gateway };
187
219
  }
188
- async function archivePackageDist(props) {
189
- await fs.access(path.join(props.distDirectory, 'index.js'));
190
- execFileSync('tar', ['--no-xattrs', '-czf', props.archivePath, '-C', props.distDirectory, '.'], {
191
- env: { ...process.env, COPYFILE_DISABLE: '1' },
192
- stdio: 'inherit',
193
- });
194
- }
195
220
  async function packLocalPackageTarball(props) {
196
221
  const packageJsonPath = path.join(props.packageDirectory, 'package.json');
197
222
  await fs.access(packageJsonPath);
198
223
  const packDirectory = await fs.mkdtemp(path.join(os.tmpdir(), `${props.packageName}-pack-`));
199
- execFileSync('pnpm', ['pack', '--pack-destination', packDirectory], {
200
- cwd: props.packageDirectory,
201
- stdio: 'pipe',
202
- });
203
- const packedTarballs = (await fs.readdir(packDirectory)).filter((fileName) => fileName.endsWith('.tgz'));
204
- const [packedTarballName] = packedTarballs;
205
- if (packedTarballName === undefined) {
206
- throw new Error(`Failed to pack local ${props.packageName} tarball for smoke image.`);
224
+ try {
225
+ execFileSync('pnpm', ['pack', '--pack-destination', packDirectory], {
226
+ cwd: props.packageDirectory,
227
+ stdio: 'pipe',
228
+ });
229
+ const packedTarballs = (await fs.readdir(packDirectory)).filter((fileName) => fileName.endsWith('.tgz'));
230
+ const [packedTarballName] = packedTarballs;
231
+ if (packedTarballName === undefined) {
232
+ throw new Error(`Failed to pack local ${props.packageName} tarball for smoke image.`);
233
+ }
234
+ if (packedTarballs.length > 1) {
235
+ throw new Error(`Expected pnpm pack for ${props.packageName} to produce exactly one tarball.`);
236
+ }
237
+ return path.join(packDirectory, packedTarballName);
207
238
  }
208
- if (packedTarballs.length > 1) {
209
- throw new Error(`Expected pnpm pack for ${props.packageName} to produce exactly one tarball.`);
239
+ catch (error) {
240
+ await fs.rm(packDirectory, { force: true, recursive: true });
241
+ throw error;
210
242
  }
211
- return path.join(packDirectory, packedTarballName);
212
243
  }
213
- async function useLocalToolVmMcpPortalPackage(options) {
244
+ async function removeLocalPackageTarballDirectories(tarballPaths) {
245
+ await Promise.all(Array.from(new Set(tarballPaths
246
+ .filter((tarballPath) => tarballPath !== undefined)
247
+ .map((tarballPath) => path.dirname(tarballPath)))).map(async (packDirectory) => {
248
+ await fs.rm(packDirectory, { force: true, recursive: true });
249
+ }));
250
+ }
251
+ async function copyLocalPackageTarballsToDockerContext(options) {
252
+ await Promise.all(options.tarballs.map(async (tarball) => {
253
+ await fs.copyFile(tarball.sourcePath, path.join(options.dockerContextDirectory, tarball.archiveName));
254
+ }));
255
+ }
256
+ async function useLocalToolVmMcpPortalPackageTarballs(options) {
214
257
  const managedImageRelease = await resolveManagedImageRelease();
215
258
  const baseImage = managedImageRelease.baseImages['tool-vm'];
216
259
  const toolVmProfiles = Object.entries(options.systemConfig.imageProfiles.toolVms);
217
- for (const [profileName, toolVmProfile] of toolVmProfiles) {
260
+ await Promise.all(toolVmProfiles.map(async ([profileName, toolVmProfile]) => {
218
261
  const dockerContextDirectory = path.join(options.projectRoot, 'vm-images', 'tool-vms', `${profileName}-local-mcp-portal`);
219
262
  const dockerfilePath = path.join(dockerContextDirectory, 'Dockerfile');
220
263
  const localConfigContractsTarballName = 'config-contracts-local.tgz';
264
+ const localSecretsTarballName = 'secrets-local.tgz';
221
265
  const localTarballName = 'mcp-portal-local.tgz';
222
266
  await fs.rm(dockerContextDirectory, { force: true, recursive: true });
223
267
  await fs.mkdir(dockerContextDirectory, { recursive: true });
224
- await fs.copyFile(options.localConfigContractsTarballPath, path.join(dockerContextDirectory, localConfigContractsTarballName));
225
- await fs.copyFile(options.localMcpPortalTarballPath, path.join(dockerContextDirectory, localTarballName));
268
+ await copyLocalPackageTarballsToDockerContext({
269
+ dockerContextDirectory,
270
+ tarballs: [
271
+ {
272
+ archiveName: localConfigContractsTarballName,
273
+ sourcePath: options.localConfigContractsTarballPath,
274
+ },
275
+ {
276
+ archiveName: localSecretsTarballName,
277
+ sourcePath: options.localSecretsTarballPath,
278
+ },
279
+ { archiveName: localTarballName, sourcePath: options.localMcpPortalTarballPath },
280
+ ],
281
+ });
226
282
  await fs.writeFile(dockerfilePath, [
227
283
  `FROM ${baseImage.repository}:${baseImage.tag}`,
228
284
  '',
229
285
  '# Generated by the OpenClaw smoke harness from the local MCP Portal package.',
230
286
  'COPY config-contracts-local.tgz /tmp/config-contracts-local.tgz',
287
+ 'COPY secrets-local.tgz /tmp/secrets-local.tgz',
231
288
  'COPY mcp-portal-local.tgz /tmp/mcp-portal-local.tgz',
232
- `ENV PNPM_HOME=/pnpm`,
233
- 'ENV PATH=${PNPM_HOME}:${PATH}',
234
- 'RUN pnpm config set global-dir /pnpm/global && pnpm config set global-bin-dir /pnpm',
235
- 'RUN pnpm add -g /tmp/config-contracts-local.tgz /tmp/mcp-portal-local.tgz && rm -f /tmp/config-contracts-local.tgz /tmp/mcp-portal-local.tgz',
289
+ 'RUN mkdir -p /opt/agent-vm/local-packages && \\',
290
+ ' npm install --omit=dev --no-audit --no-fund --prefix /opt/agent-vm/local-packages /tmp/config-contracts-local.tgz /tmp/secrets-local.tgz /tmp/mcp-portal-local.tgz && \\',
291
+ ' rm -f /tmp/config-contracts-local.tgz /tmp/secrets-local.tgz /tmp/mcp-portal-local.tgz',
236
292
  '',
237
293
  ].join('\n'), 'utf8');
238
294
  toolVmProfile.dockerfile = dockerfilePath;
239
295
  delete toolVmProfile.source;
296
+ }));
297
+ }
298
+ export async function useLocalToolVmMcpPortalPackage(options) {
299
+ const localConfigContractsTarballPath = await packLocalPackageTarball({
300
+ packageDirectory: path.join(options.repoRoot, 'packages', 'config-contracts'),
301
+ packageName: 'config-contracts',
302
+ });
303
+ const localSecretsTarballPath = await packLocalPackageTarball({
304
+ packageDirectory: path.join(options.repoRoot, 'packages', 'secrets'),
305
+ packageName: 'secrets',
306
+ });
307
+ const localMcpPortalTarballPath = await packLocalPackageTarball({
308
+ packageDirectory: path.join(options.repoRoot, 'packages', 'mcp-portal'),
309
+ packageName: 'mcp-portal',
310
+ });
311
+ try {
312
+ await useLocalToolVmMcpPortalPackageTarballs({
313
+ localConfigContractsTarballPath,
314
+ localMcpPortalTarballPath,
315
+ localSecretsTarballPath,
316
+ projectRoot: options.projectRoot,
317
+ systemConfig: options.systemConfig,
318
+ });
319
+ }
320
+ finally {
321
+ await removeLocalPackageTarballDirectories([
322
+ localConfigContractsTarballPath,
323
+ localSecretsTarballPath,
324
+ localMcpPortalTarballPath,
325
+ ]);
240
326
  }
241
327
  }
328
+ function localPackageTarballArchiveName(packageName) {
329
+ return `${packageName}-local.tgz`;
330
+ }
331
+ function isJsonRecord(value) {
332
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
333
+ }
334
+ function mutableJsonRecord(value) {
335
+ if (!isJsonRecord(value)) {
336
+ return undefined;
337
+ }
338
+ return value;
339
+ }
340
+ async function runDockerCommand(args) {
341
+ await execFileAsync('docker', [...args]);
342
+ }
343
+ async function readSmokeDockerImageTag(buildConfigPath) {
344
+ let buildConfig;
345
+ try {
346
+ buildConfig = mutableJsonRecord(await loadJsonConfigFile(buildConfigPath));
347
+ }
348
+ catch (error) {
349
+ if (isJsonRecord(error) && error.code === 'ENOENT') {
350
+ return null;
351
+ }
352
+ throw error;
353
+ }
354
+ const ociConfig = mutableJsonRecord(buildConfig?.oci);
355
+ const imageTag = ociConfig?.image;
356
+ return typeof imageTag === 'string' && imageTag.length > 0 ? imageTag : null;
357
+ }
358
+ export async function collectSmokeDockerImageTags(systemConfig) {
359
+ const imageProfiles = [
360
+ ...Object.values(systemConfig.imageProfiles.gateways),
361
+ ...Object.values(systemConfig.imageProfiles.toolVms),
362
+ ];
363
+ const imageTags = [];
364
+ for (const imageProfile of imageProfiles) {
365
+ // oxlint-disable-next-line eslint/no-await-in-loop -- config files are intentionally read deterministically
366
+ const imageTag = await readSmokeDockerImageTag(imageProfile.buildConfig);
367
+ if (imageTag !== null) {
368
+ imageTags.push(imageTag);
369
+ }
370
+ }
371
+ return Array.from(new Set(imageTags));
372
+ }
373
+ export async function removeSmokeDockerImages(imageTags, options = {}) {
374
+ const dockerCommand = options.runDockerCommand ?? runDockerCommand;
375
+ for (const imageTag of Array.from(new Set(imageTags))) {
376
+ try {
377
+ // oxlint-disable-next-line eslint/no-await-in-loop -- one tag at a time keeps cleanup errors attributable
378
+ await dockerCommand(['image', 'inspect', imageTag]);
379
+ }
380
+ catch {
381
+ continue;
382
+ }
383
+ // oxlint-disable-next-line eslint/no-await-in-loop -- one tag at a time keeps cleanup errors attributable
384
+ await dockerCommand(['image', 'rm', '--force', imageTag]);
385
+ }
386
+ }
387
+ export async function removeSmokeDockerImagesForSystemConfig(systemConfig, options = {}) {
388
+ await removeSmokeDockerImages(await collectSmokeDockerImageTags(systemConfig), options);
389
+ }
390
+ function throwIfSmokeHarnessCleanupFailed(errors) {
391
+ if (errors.length === 0) {
392
+ return;
393
+ }
394
+ const [firstError] = errors;
395
+ if (errors.length === 1 && firstError !== undefined) {
396
+ throw firstError;
397
+ }
398
+ throw new AggregateError(errors, 'Smoke harness cleanup failed.');
399
+ }
400
+ function withoutStringValue(value, removedValue) {
401
+ if (!Array.isArray(value)) {
402
+ return value;
403
+ }
404
+ return value.filter((entry) => entry !== removedValue);
405
+ }
406
+ export async function disableOpenClawMcpPortalPlugin(configPath) {
407
+ const config = mutableJsonRecord(await loadJsonConfigFile(configPath));
408
+ if (!config) {
409
+ throw new Error(`OpenClaw config at ${configPath} must be an object.`);
410
+ }
411
+ const plugins = mutableJsonRecord(config.plugins);
412
+ const pluginLoad = mutableJsonRecord(plugins?.load);
413
+ if (pluginLoad) {
414
+ pluginLoad.paths = withoutStringValue(pluginLoad.paths, defaultOpenClawMcpPortalExtensionsPath);
415
+ }
416
+ if (plugins) {
417
+ plugins.allow = withoutStringValue(plugins.allow, openClawMcpPortalPluginName);
418
+ }
419
+ const pluginEntries = mutableJsonRecord(plugins?.entries);
420
+ if (pluginEntries) {
421
+ delete pluginEntries[openClawMcpPortalPluginName];
422
+ }
423
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, '\t')}\n`, 'utf8');
424
+ }
242
425
  export async function useLocalOpenClawGatewayImagePackages(options) {
243
426
  const gatewayProfile = options.systemConfig.imageProfiles.gateways[options.profileName];
244
427
  if (!gatewayProfile) {
@@ -248,61 +431,169 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
248
431
  const baseImage = managedImageRelease.baseImages['openclaw-gateway'];
249
432
  const dockerContextDirectory = path.join(options.projectRoot, 'vm-images', 'gateways', `${options.profileName}-local-packages`);
250
433
  const dockerfilePath = path.join(dockerContextDirectory, 'Dockerfile');
251
- const packages = [
252
- {
253
- archiveName: 'gondolin-dist.tgz',
254
- extensionPath: '/home/openclaw/.openclaw/extensions/gondolin',
255
- packageDirectory: path.join(options.repoRoot, 'packages', 'openclaw-agent-vm-plugin'),
256
- },
257
- {
258
- archiveName: 'mcp-portal-plugin-dist.tgz',
259
- extensionPath: '/home/openclaw/.openclaw/extensions/mcp-portal',
260
- packageDirectory: path.join(options.repoRoot, 'packages', 'openclaw-mcp-portal-plugin'),
261
- },
262
- ];
263
- await fs.rm(dockerContextDirectory, { force: true, recursive: true });
264
- await fs.mkdir(dockerContextDirectory, { recursive: true });
265
- await Promise.all(packages.map((packageConfig) => archivePackageDist({
266
- archivePath: path.join(dockerContextDirectory, packageConfig.archiveName),
267
- distDirectory: path.join(packageConfig.packageDirectory, 'dist'),
268
- })));
269
- const portalServerPath = path.join(options.repoRoot, 'packages', 'mcp-portal', 'dist', 'bin', 'portal-server.js');
270
- await fs.access(portalServerPath);
434
+ const localConfigContractsTarballPath = await packLocalPackageTarball({
435
+ packageDirectory: path.join(options.repoRoot, 'packages', 'config-contracts'),
436
+ packageName: 'config-contracts',
437
+ });
438
+ const localSecretsTarballPath = await packLocalPackageTarball({
439
+ packageDirectory: path.join(options.repoRoot, 'packages', 'secrets'),
440
+ packageName: 'secrets',
441
+ });
442
+ const localGondolinAdapterTarballPath = await packLocalPackageTarball({
443
+ packageDirectory: path.join(options.repoRoot, 'packages', 'gondolin-adapter'),
444
+ packageName: 'gondolin-adapter',
445
+ });
271
446
  const localMcpPortalTarballPath = await packLocalPackageTarball({
272
447
  packageDirectory: path.join(options.repoRoot, 'packages', 'mcp-portal'),
273
448
  packageName: 'mcp-portal',
274
449
  });
275
- const localConfigContractsTarballPath = await packLocalPackageTarball({
276
- packageDirectory: path.join(options.repoRoot, 'packages', 'config-contracts'),
277
- packageName: 'config-contracts',
450
+ const localOpenClawAgentVmPluginTarballPath = await packLocalPackageTarball({
451
+ packageDirectory: path.join(options.repoRoot, 'packages', 'openclaw-agent-vm-plugin'),
452
+ packageName: 'openclaw-agent-vm-plugin',
278
453
  });
279
- await useLocalToolVmMcpPortalPackage({
280
- localConfigContractsTarballPath,
281
- localMcpPortalTarballPath,
282
- projectRoot: options.projectRoot,
283
- systemConfig: options.systemConfig,
454
+ const localOpenClawMcpPortalPluginTarballPath = await packLocalPackageTarball({
455
+ packageDirectory: path.join(options.repoRoot, 'packages', 'openclaw-mcp-portal-plugin'),
456
+ packageName: 'openclaw-mcp-portal-plugin',
284
457
  });
285
- await fs.writeFile(dockerfilePath, [
286
- `FROM ${baseImage.repository}:${baseImage.tag}`,
287
- '',
288
- '# Generated by the OpenClaw smoke harness from local package dist outputs.',
289
- 'COPY gondolin-dist.tgz /tmp/gondolin-dist.tgz',
290
- 'COPY mcp-portal-plugin-dist.tgz /tmp/mcp-portal-plugin-dist.tgz',
291
- 'RUN rm -rf /home/openclaw/.openclaw/extensions/gondolin /home/openclaw/.openclaw/extensions/mcp-portal && \\',
292
- ' mkdir -p /home/openclaw/.openclaw/extensions/gondolin /home/openclaw/.openclaw/extensions/mcp-portal /opt/agent-vm/portal/bin && \\',
293
- ' tar -xzf /tmp/gondolin-dist.tgz -C /home/openclaw/.openclaw/extensions/gondolin && \\',
294
- ' tar -xzf /tmp/mcp-portal-plugin-dist.tgz -C /home/openclaw/.openclaw/extensions/mcp-portal && \\',
295
- ' chown -R root:root /home/openclaw/.openclaw/extensions/gondolin /home/openclaw/.openclaw/extensions/mcp-portal && \\',
296
- ' rm -f /tmp/gondolin-dist.tgz /tmp/mcp-portal-plugin-dist.tgz',
297
- 'RUN printf \'#!/bin/sh\\nexec node /work/repo/packages/mcp-portal/dist/bin/portal-server.js "$@"\\n\' > /opt/agent-vm/portal/bin/agent-vm-mcp-portal-server && \\',
298
- ' chmod 755 /opt/agent-vm/portal/bin/agent-vm-mcp-portal-server',
299
- '',
300
- ].join('\n'), 'utf8');
301
- gatewayProfile.dockerfile = dockerfilePath;
302
- delete gatewayProfile.source;
458
+ try {
459
+ const localPackageTarballs = [
460
+ {
461
+ archiveName: localPackageTarballArchiveName('config-contracts'),
462
+ sourcePath: localConfigContractsTarballPath,
463
+ },
464
+ {
465
+ archiveName: localPackageTarballArchiveName('secrets'),
466
+ sourcePath: localSecretsTarballPath,
467
+ },
468
+ {
469
+ archiveName: localPackageTarballArchiveName('gondolin-adapter'),
470
+ sourcePath: localGondolinAdapterTarballPath,
471
+ },
472
+ {
473
+ archiveName: localPackageTarballArchiveName('mcp-portal'),
474
+ sourcePath: localMcpPortalTarballPath,
475
+ },
476
+ {
477
+ archiveName: localPackageTarballArchiveName('openclaw-agent-vm-plugin'),
478
+ sourcePath: localOpenClawAgentVmPluginTarballPath,
479
+ },
480
+ {
481
+ archiveName: localPackageTarballArchiveName('openclaw-mcp-portal-plugin'),
482
+ sourcePath: localOpenClawMcpPortalPluginTarballPath,
483
+ },
484
+ ];
485
+ await fs.rm(dockerContextDirectory, { force: true, recursive: true });
486
+ await fs.mkdir(dockerContextDirectory, { recursive: true });
487
+ await copyLocalPackageTarballsToDockerContext({
488
+ dockerContextDirectory,
489
+ tarballs: localPackageTarballs,
490
+ });
491
+ await useLocalToolVmMcpPortalPackageTarballs({
492
+ localConfigContractsTarballPath,
493
+ localMcpPortalTarballPath,
494
+ localSecretsTarballPath,
495
+ projectRoot: options.projectRoot,
496
+ systemConfig: options.systemConfig,
497
+ });
498
+ await fs.writeFile(dockerfilePath, [
499
+ `FROM ${baseImage.repository}:${baseImage.tag}`,
500
+ '',
501
+ '# Generated by the OpenClaw smoke harness from local package tarballs.',
502
+ ...localPackageTarballs.map((tarball) => `COPY ${tarball.archiveName} /tmp/${tarball.archiveName}`),
503
+ 'RUN mkdir -p /opt/agent-vm/local-packages && \\',
504
+ ' npm install --omit=dev --no-audit --no-fund --prefix /opt/agent-vm/local-packages ' +
505
+ localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' ') +
506
+ ' && \\',
507
+ ' rm -f ' +
508
+ localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' '),
509
+ 'RUN package_root="/opt/agent-vm/local-packages/node_modules" && \\',
510
+ ' mkdir -p /home/openclaw/.openclaw/extensions && \\',
511
+ ' ln -sfn "$package_root/@agent-vm/openclaw-agent-vm-plugin/dist" /home/openclaw/.openclaw/extensions/gondolin && \\',
512
+ ' ln -sfn "$package_root/@agent-vm/openclaw-mcp-portal-plugin/dist" /home/openclaw/.openclaw/extensions/mcp-portal',
513
+ '',
514
+ ].join('\n'), 'utf8');
515
+ gatewayProfile.dockerfile = dockerfilePath;
516
+ delete gatewayProfile.source;
517
+ }
518
+ finally {
519
+ await removeLocalPackageTarballDirectories([
520
+ localConfigContractsTarballPath,
521
+ localSecretsTarballPath,
522
+ localGondolinAdapterTarballPath,
523
+ localMcpPortalTarballPath,
524
+ localOpenClawAgentVmPluginTarballPath,
525
+ localOpenClawMcpPortalPluginTarballPath,
526
+ ]);
527
+ }
303
528
  }
304
529
  export async function useLocalOpenClawPluginGatewayImage(options) {
305
- await useLocalOpenClawGatewayImagePackages(options);
530
+ const gatewayProfile = options.systemConfig.imageProfiles.gateways[options.profileName];
531
+ if (!gatewayProfile) {
532
+ throw new Error(`Gateway image profile '${options.profileName}' is not configured.`);
533
+ }
534
+ const managedImageRelease = await resolveManagedImageRelease();
535
+ const baseImage = managedImageRelease.baseImages['openclaw-gateway'];
536
+ const dockerContextDirectory = path.join(options.projectRoot, 'vm-images', 'gateways', `${options.profileName}-local-plugin`);
537
+ const dockerfilePath = path.join(dockerContextDirectory, 'Dockerfile');
538
+ const localSecretsTarballPath = await packLocalPackageTarball({
539
+ packageDirectory: path.join(options.repoRoot, 'packages', 'secrets'),
540
+ packageName: 'secrets',
541
+ });
542
+ const localGondolinAdapterTarballPath = await packLocalPackageTarball({
543
+ packageDirectory: path.join(options.repoRoot, 'packages', 'gondolin-adapter'),
544
+ packageName: 'gondolin-adapter',
545
+ });
546
+ const localOpenClawAgentVmPluginTarballPath = await packLocalPackageTarball({
547
+ packageDirectory: path.join(options.repoRoot, 'packages', 'openclaw-agent-vm-plugin'),
548
+ packageName: 'openclaw-agent-vm-plugin',
549
+ });
550
+ try {
551
+ const localPackageTarballs = [
552
+ {
553
+ archiveName: localPackageTarballArchiveName('secrets'),
554
+ sourcePath: localSecretsTarballPath,
555
+ },
556
+ {
557
+ archiveName: localPackageTarballArchiveName('gondolin-adapter'),
558
+ sourcePath: localGondolinAdapterTarballPath,
559
+ },
560
+ {
561
+ archiveName: localPackageTarballArchiveName('openclaw-agent-vm-plugin'),
562
+ sourcePath: localOpenClawAgentVmPluginTarballPath,
563
+ },
564
+ ];
565
+ await fs.rm(dockerContextDirectory, { force: true, recursive: true });
566
+ await fs.mkdir(dockerContextDirectory, { recursive: true });
567
+ await copyLocalPackageTarballsToDockerContext({
568
+ dockerContextDirectory,
569
+ tarballs: localPackageTarballs,
570
+ });
571
+ await fs.writeFile(dockerfilePath, [
572
+ `FROM ${baseImage.repository}:${baseImage.tag}`,
573
+ '',
574
+ '# Generated by the OpenClaw smoke harness from local plugin package tarballs.',
575
+ ...localPackageTarballs.map((tarball) => `COPY ${tarball.archiveName} /tmp/${tarball.archiveName}`),
576
+ 'RUN mkdir -p /opt/agent-vm/local-packages && \\',
577
+ ' npm install --omit=dev --no-audit --no-fund --prefix /opt/agent-vm/local-packages ' +
578
+ localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' ') +
579
+ ' && \\',
580
+ ' rm -f ' +
581
+ localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' '),
582
+ 'RUN package_root="/opt/agent-vm/local-packages/node_modules" && \\',
583
+ ' mkdir -p /home/openclaw/.openclaw/extensions && \\',
584
+ ' ln -sfn "$package_root/@agent-vm/openclaw-agent-vm-plugin/dist" /home/openclaw/.openclaw/extensions/gondolin',
585
+ '',
586
+ ].join('\n'), 'utf8');
587
+ gatewayProfile.dockerfile = dockerfilePath;
588
+ delete gatewayProfile.source;
589
+ }
590
+ finally {
591
+ await removeLocalPackageTarballDirectories([
592
+ localSecretsTarballPath,
593
+ localGondolinAdapterTarballPath,
594
+ localOpenClawAgentVmPluginTarballPath,
595
+ ]);
596
+ }
306
597
  }
307
598
  export async function scaffoldOpenClawSmokeProject(options) {
308
599
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), options.prefix));
@@ -430,19 +721,12 @@ export async function writeOpenClawMcpPortalSmokeConfigs(options) {
430
721
  },
431
722
  },
432
723
  schemaVersion: 1,
433
- server: {
434
- accessHeader: {
435
- name: options.portalAccessHeaderName,
436
- secret: { name: 'MCP_PORTAL_SERVER_SECRET', source: 'environment' },
437
- },
438
- host: '127.0.0.1',
439
- port: 18790,
440
- },
441
724
  }, null, '\t')}\n`, 'utf8');
442
725
  }
443
726
  export async function startSmokeControllerRuntime(options) {
444
727
  const restoreEnvironment = applySmokeEnvironment(options.secrets);
445
728
  const secretResolver = createSmokeSecretResolver(options.secrets);
729
+ const tempRoot = path.dirname(path.dirname(options.startOptions.systemConfig.systemConfigPath));
446
730
  try {
447
731
  const runtime = await startControllerRuntime(options.startOptions, {
448
732
  createSecretResolver: async () => secretResolver,
@@ -474,19 +758,52 @@ export async function startSmokeControllerRuntime(options) {
474
758
  controllerUrl: `http://127.0.0.1:${options.startOptions.systemConfig.host.controllerPort}`,
475
759
  runtime,
476
760
  systemConfig: options.startOptions.systemConfig,
477
- tempRoot: path.dirname(path.dirname(options.startOptions.systemConfig.systemConfigPath)),
761
+ tempRoot,
478
762
  close: async () => {
763
+ const cleanupErrors = [];
479
764
  try {
480
765
  await runtime.close();
481
766
  }
767
+ catch (error) {
768
+ cleanupErrors.push(error);
769
+ }
770
+ try {
771
+ await removeSmokeDockerImagesForSystemConfig(options.startOptions.systemConfig);
772
+ }
773
+ catch (error) {
774
+ cleanupErrors.push(error);
775
+ }
776
+ try {
777
+ await removeSmokeTempRoot(tempRoot);
778
+ }
779
+ catch (error) {
780
+ cleanupErrors.push(error);
781
+ }
482
782
  finally {
483
783
  restoreEnvironment();
484
784
  }
785
+ throwIfSmokeHarnessCleanupFailed(cleanupErrors);
485
786
  },
486
787
  };
487
788
  }
488
789
  catch (error) {
489
- restoreEnvironment();
790
+ const cleanupErrors = [error];
791
+ try {
792
+ await removeSmokeDockerImagesForSystemConfig(options.startOptions.systemConfig);
793
+ }
794
+ catch (cleanupError) {
795
+ cleanupErrors.push(cleanupError);
796
+ }
797
+ try {
798
+ await removeSmokeTempRoot(tempRoot);
799
+ }
800
+ catch (cleanupError) {
801
+ cleanupErrors.push(cleanupError);
802
+ }
803
+ finally {
804
+ restoreEnvironment();
805
+ }
806
+ throwIfSmokeHarnessCleanupFailed(cleanupErrors);
490
807
  throw error;
491
808
  }
492
809
  }