@agent-vm/agent-vm 0.0.93 → 0.0.95
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/dist/cli/agent-vm-cli-support.d.ts +6 -2
- package/dist/cli/agent-vm-cli-support.d.ts.map +1 -1
- package/dist/cli/agent-vm-cli-support.js +2 -1
- package/dist/cli/agent-vm-cli-support.js.map +1 -1
- package/dist/cli/commands/controller-definition.d.ts +78 -0
- package/dist/cli/commands/controller-definition.d.ts.map +1 -1
- package/dist/cli/commands/controller-definition.js +34 -0
- package/dist/cli/commands/controller-definition.js.map +1 -1
- package/dist/cli/commands/create-app.d.ts +78 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -1
- package/dist/cli/commands/doctor-definition.d.ts.map +1 -1
- package/dist/cli/commands/doctor-definition.js +6 -0
- package/dist/cli/commands/doctor-definition.js.map +1 -1
- package/dist/cli/controller-operation-commands.d.ts +22 -1
- package/dist/cli/controller-operation-commands.d.ts.map +1 -1
- package/dist/cli/controller-operation-commands.js +187 -49
- package/dist/cli/controller-operation-commands.js.map +1 -1
- package/dist/cli/manual-templates.d.ts.map +1 -1
- package/dist/cli/manual-templates.js +11 -5
- package/dist/cli/manual-templates.js.map +1 -1
- package/dist/config/system-config.d.ts +0 -3
- package/dist/config/system-config.d.ts.map +1 -1
- package/dist/config/system-config.js +1 -7
- package/dist/config/system-config.js.map +1 -1
- package/dist/controller/controller-runtime-operations.d.ts +9 -1
- package/dist/controller/controller-runtime-operations.d.ts.map +1 -1
- package/dist/controller/controller-runtime-operations.js +1 -0
- package/dist/controller/controller-runtime-operations.js.map +1 -1
- package/dist/controller/controller-runtime-types.d.ts +2 -1
- package/dist/controller/controller-runtime-types.d.ts.map +1 -1
- package/dist/controller/controller-runtime.d.ts.map +1 -1
- package/dist/controller/controller-runtime.js +37 -9
- package/dist/controller/controller-runtime.js.map +1 -1
- package/dist/controller/health/channel-provider-recovery-observation.d.ts.map +1 -1
- package/dist/controller/health/channel-provider-recovery-observation.js +4 -1
- package/dist/controller/health/channel-provider-recovery-observation.js.map +1 -1
- package/dist/controller/health/gateway-vm-recovery-policy.d.ts +2 -1
- package/dist/controller/health/gateway-vm-recovery-policy.d.ts.map +1 -1
- package/dist/controller/health/gateway-vm-recovery-policy.js +11 -1
- package/dist/controller/health/gateway-vm-recovery-policy.js.map +1 -1
- package/dist/controller/health/gateway-vm-recovery-runner.d.ts.map +1 -1
- package/dist/controller/health/gateway-vm-recovery-runner.js +20 -4
- package/dist/controller/health/gateway-vm-recovery-runner.js.map +1 -1
- package/dist/controller/http/controller-client.d.ts +3 -0
- package/dist/controller/http/controller-client.d.ts.map +1 -1
- package/dist/controller/http/controller-client.js +12 -0
- package/dist/controller/http/controller-client.js.map +1 -1
- package/dist/controller/http/controller-http-route-support.d.ts +3 -0
- package/dist/controller/http/controller-http-route-support.d.ts.map +1 -1
- package/dist/controller/http/controller-http-route-support.js.map +1 -1
- package/dist/controller/http/controller-zone-operation-routes.d.ts.map +1 -1
- package/dist/controller/http/controller-zone-operation-routes.js +12 -0
- package/dist/controller/http/controller-zone-operation-routes.js.map +1 -1
- package/dist/controller/worker-task-runner.d.ts +6 -0
- package/dist/controller/worker-task-runner.d.ts.map +1 -1
- package/dist/controller/worker-task-runner.js +13 -4
- package/dist/controller/worker-task-runner.js.map +1 -1
- package/dist/controller/zone-runtimes/gateway-zone-state-machine.d.ts.map +1 -1
- package/dist/controller/zone-runtimes/gateway-zone-state-machine.js +3 -0
- package/dist/controller/zone-runtimes/gateway-zone-state-machine.js.map +1 -1
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.d.ts +8 -1
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.d.ts.map +1 -1
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.js +150 -34
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.js.map +1 -1
- package/dist/controller/zone-runtimes/zone-runtime-types.d.ts +8 -0
- package/dist/controller/zone-runtimes/zone-runtime-types.d.ts.map +1 -1
- package/dist/gateway/credential-manager.d.ts +1 -1
- package/dist/gateway/credential-manager.d.ts.map +1 -1
- package/dist/gateway/credential-manager.js +3 -18
- package/dist/gateway/credential-manager.js.map +1 -1
- package/dist/gateway/gateway-recovery.d.ts +7 -2
- package/dist/gateway/gateway-recovery.d.ts.map +1 -1
- package/dist/gateway/gateway-recovery.js +73 -0
- package/dist/gateway/gateway-recovery.js.map +1 -1
- package/dist/gateway/gateway-zone-orchestrator.d.ts +8 -1
- package/dist/gateway/gateway-zone-orchestrator.d.ts.map +1 -1
- package/dist/gateway/gateway-zone-orchestrator.js +216 -84
- package/dist/gateway/gateway-zone-orchestrator.js.map +1 -1
- package/dist/gateway/gateway-zone-support.d.ts +1 -0
- package/dist/gateway/gateway-zone-support.d.ts.map +1 -1
- package/dist/gateway/gateway-zone-support.js.map +1 -1
- package/dist/gateway/mcp-portal-effective-config.d.ts +13 -0
- package/dist/gateway/mcp-portal-effective-config.d.ts.map +1 -1
- package/dist/gateway/mcp-portal-effective-config.js +67 -16
- package/dist/gateway/mcp-portal-effective-config.js.map +1 -1
- package/dist/integration-tests/e2e-harness.d.ts +7 -0
- package/dist/integration-tests/e2e-harness.d.ts.map +1 -1
- package/dist/integration-tests/e2e-harness.js +463 -105
- package/dist/integration-tests/e2e-harness.js.map +1 -1
- package/dist/integration-tests/e2e-protocol-wait.d.ts +2 -0
- package/dist/integration-tests/e2e-protocol-wait.d.ts.map +1 -0
- package/dist/integration-tests/e2e-protocol-wait.js +5 -0
- package/dist/integration-tests/e2e-protocol-wait.js.map +1 -0
- package/dist/integration-tests/e2e-workspace-build-global-setup.d.ts +13 -2
- package/dist/integration-tests/e2e-workspace-build-global-setup.d.ts.map +1 -1
- package/dist/integration-tests/e2e-workspace-build-global-setup.js +23 -1
- package/dist/integration-tests/e2e-workspace-build-global-setup.js.map +1 -1
- package/dist/operations/config-validation.js +1 -1
- package/dist/operations/doctor.d.ts.map +1 -1
- package/dist/operations/doctor.js +1 -6
- package/dist/operations/doctor.js.map +1 -1
- package/dist/operations/zone-git-doctor.d.ts +1 -0
- package/dist/operations/zone-git-doctor.d.ts.map +1 -1
- package/dist/operations/zone-git-doctor.js +3 -1
- package/dist/operations/zone-git-doctor.js.map +1 -1
- package/managed-images.json +1 -1
- package/package.json +11 -11
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { execFile, execFileSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs/promises';
|
|
3
4
|
import net from 'node:net';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
7
|
+
import { setTimeout as waitForRetryInterval } from 'node:timers/promises';
|
|
6
8
|
import { promisify } from 'node:util';
|
|
7
9
|
import { hasBuiltImageAssets, resolveGondolinMinimumZigVersion } from '@agent-vm/gondolin-adapter';
|
|
8
10
|
import { computeFingerprintFromConfigPath } from '../build/gondolin-image-builder.js';
|
|
9
11
|
import { generateManagedDockerfile, resolveManagedImageRelease, } from '../build/managed-image-dockerfile.js';
|
|
12
|
+
import { readPreparedGondolinImage, writePreparedGondolinImage, } from '../build/prepared-gondolin-image-cache.js';
|
|
10
13
|
import { isZigVersionAtLeast, resolveHostZigVersion } from '../build/zig-compatibility.js';
|
|
14
|
+
import { runBuildCommand } from '../cli/build-command.js';
|
|
11
15
|
import { scaffoldAgentVmProject } from '../cli/init-command.js';
|
|
12
16
|
import { loadJsonConfigFile } from '../config/json-config-file.js';
|
|
13
17
|
import { loadSystemConfig } from '../config/system-config.js';
|
|
14
18
|
import { startControllerRuntime } from '../controller/controller-runtime.js';
|
|
15
19
|
import { startGatewayZone } from '../gateway/gateway-zone-orchestrator.js';
|
|
16
20
|
const defaultOpenClawMcpPortalExtensionsPath = '/home/openclaw/.openclaw/extensions/mcp-portal';
|
|
21
|
+
const dockerContextLocalPackageTimestamp = new Date('2000-01-01T00:00:00.000Z');
|
|
22
|
+
const e2ePreparedImageManifestFileName = 'prepared-e2e-images.json';
|
|
23
|
+
const e2ePreparedImageManifestSchemaVersion = 1;
|
|
17
24
|
const execFileAsync = promisify(execFile);
|
|
18
25
|
const openClawMcpPortalPluginName = 'mcp-portal';
|
|
19
26
|
const e2eTempRootPrefixes = [
|
|
@@ -32,6 +39,9 @@ function resolveE2eCacheRoot() {
|
|
|
32
39
|
}
|
|
33
40
|
return path.join(os.tmpdir(), 'agent-vm-e2e-cache');
|
|
34
41
|
}
|
|
42
|
+
function isObjectRecord(value) {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
35
45
|
export function shouldCleanupE2eDockerImages(options = {}) {
|
|
36
46
|
const env = options.env ?? process.env;
|
|
37
47
|
return options.cleanupImages === true || env.AGENT_VM_E2E_CLEAN_IMAGES === '1';
|
|
@@ -114,17 +124,253 @@ export async function findAvailablePort() {
|
|
|
114
124
|
});
|
|
115
125
|
});
|
|
116
126
|
}
|
|
127
|
+
function readNodeNetworkErrorCode(error) {
|
|
128
|
+
if (!(error instanceof TypeError) || error.message !== 'fetch failed') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const cause = error.cause;
|
|
132
|
+
if (typeof cause !== 'object' || cause === null || !('code' in cause)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return typeof cause.code === 'string' ? cause.code : null;
|
|
136
|
+
}
|
|
137
|
+
function isRecoverableControllerReadyError(error) {
|
|
138
|
+
return ['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH'].includes(readNodeNetworkErrorCode(error) ?? '');
|
|
139
|
+
}
|
|
117
140
|
export async function waitForControllerReady(controllerPort) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
const timeoutMs = 5_000;
|
|
142
|
+
const retryIntervalMs = 50;
|
|
143
|
+
const startedAtMs = performance.now();
|
|
144
|
+
let lastError = 'not attempted';
|
|
145
|
+
while (performance.now() - startedAtMs <= timeoutMs) {
|
|
146
|
+
try {
|
|
147
|
+
// oxlint-disable-next-line no-await-in-loop -- controller startup readiness must observe sequential protocol state.
|
|
148
|
+
const response = await fetch(`http://127.0.0.1:${String(controllerPort)}/controller-status`, {
|
|
149
|
+
signal: AbortSignal.timeout(1_000),
|
|
150
|
+
});
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
lastError = `HTTP ${String(response.status)}`;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (!isRecoverableControllerReadyError(error)) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
161
|
+
}
|
|
162
|
+
// oxlint-disable-next-line no-await-in-loop -- controller readiness has no event source from the subprocess boundary.
|
|
163
|
+
await waitForRetryInterval(retryIntervalMs);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Controller did not report ready within ${String(timeoutMs)}ms. Last error: ${lastError}`);
|
|
166
|
+
}
|
|
167
|
+
async function pathExists(filePath) {
|
|
168
|
+
try {
|
|
169
|
+
await fs.access(filePath);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function listDirectoryFiles(directoryPath) {
|
|
180
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
181
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
182
|
+
const entryPath = path.join(directoryPath, entry.name);
|
|
183
|
+
if (entry.isDirectory()) {
|
|
184
|
+
return await listDirectoryFiles(entryPath);
|
|
185
|
+
}
|
|
186
|
+
return entry.isFile() ? [entryPath] : [];
|
|
187
|
+
}));
|
|
188
|
+
return files.flat().toSorted((leftPath, rightPath) => leftPath.localeCompare(rightPath));
|
|
189
|
+
}
|
|
190
|
+
async function hashFileInto(hasher, filePath, baseDirectory) {
|
|
191
|
+
const relativePath = path.relative(baseDirectory, filePath).split(path.sep).join('/');
|
|
192
|
+
hasher.update(`file:${relativePath}\0`);
|
|
193
|
+
hasher.update(await fs.readFile(filePath));
|
|
194
|
+
hasher.update('\0');
|
|
195
|
+
}
|
|
196
|
+
async function computeDockerContextFingerprint(dockerfilePath) {
|
|
197
|
+
const dockerContextDirectory = path.dirname(dockerfilePath);
|
|
198
|
+
const hasher = crypto.createHash('sha256');
|
|
199
|
+
for (const filePath of await listDirectoryFiles(dockerContextDirectory)) {
|
|
200
|
+
// oxlint-disable-next-line no-await-in-loop -- hash order is deterministic and low-volume for e2e Docker contexts.
|
|
201
|
+
await hashFileInto(hasher, filePath, dockerContextDirectory);
|
|
202
|
+
}
|
|
203
|
+
return hasher.digest('hex');
|
|
204
|
+
}
|
|
205
|
+
async function readTextIfPresent(filePath) {
|
|
206
|
+
if (filePath === undefined || !(await pathExists(filePath))) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
return await fs.readFile(filePath, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
async function normalizeE2eImageSourceForFingerprint(source) {
|
|
212
|
+
if (!isObjectRecord(source)) {
|
|
213
|
+
return source;
|
|
214
|
+
}
|
|
215
|
+
const overlay = source.overlay;
|
|
216
|
+
return {
|
|
217
|
+
...source,
|
|
218
|
+
...(typeof overlay === 'string'
|
|
219
|
+
? { overlay: { content: await readTextIfPresent(overlay) } }
|
|
220
|
+
: {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async function computeE2eImageRecipeFingerprint(options) {
|
|
224
|
+
const hasher = crypto.createHash('sha256');
|
|
225
|
+
hasher.update(JSON.stringify({
|
|
226
|
+
buildConfig: await readTextIfPresent(options.buildConfigPath),
|
|
227
|
+
dockerContext: options.dockerfile === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: await computeDockerContextFingerprint(options.dockerfile),
|
|
230
|
+
source: await normalizeE2eImageSourceForFingerprint(options.source),
|
|
231
|
+
}));
|
|
232
|
+
return hasher.digest('hex');
|
|
233
|
+
}
|
|
234
|
+
async function collectE2eImageTargets(project) {
|
|
235
|
+
const createImageTarget = async (family, profileName, profile) => {
|
|
236
|
+
const target = {
|
|
237
|
+
buildConfigPath: profile.buildConfig,
|
|
238
|
+
cacheDirectory: path.join(project.systemConfig.cacheDir, family === 'gateway' ? 'gateway-images' : 'tool-vm-images', profileName),
|
|
239
|
+
e2eManifestEligible: profile.source === undefined,
|
|
240
|
+
family,
|
|
241
|
+
name: profileName,
|
|
242
|
+
recipeFingerprint: await computeE2eImageRecipeFingerprint({
|
|
243
|
+
buildConfigPath: profile.buildConfig,
|
|
244
|
+
...(profile.dockerfile === undefined ? {} : { dockerfile: profile.dockerfile }),
|
|
245
|
+
...(profile.source === undefined ? {} : { source: profile.source }),
|
|
246
|
+
}),
|
|
247
|
+
};
|
|
248
|
+
if (profile.dockerfile !== undefined) {
|
|
249
|
+
return { ...target, dockerfile: profile.dockerfile };
|
|
250
|
+
}
|
|
251
|
+
if (profile.source !== undefined) {
|
|
252
|
+
return { ...target, source: profile.source };
|
|
253
|
+
}
|
|
254
|
+
return target;
|
|
255
|
+
};
|
|
256
|
+
const gatewayTargets = await Promise.all(Object.entries(project.systemConfig.imageProfiles.gateways).map(async ([profileName, profile]) => await createImageTarget('gateway', profileName, profile)));
|
|
257
|
+
const toolVmTargets = await Promise.all(Object.entries(project.systemConfig.imageProfiles.toolVms).map(async ([profileName, profile]) => await createImageTarget('toolVm', profileName, profile)));
|
|
258
|
+
return [...gatewayTargets, ...toolVmTargets];
|
|
259
|
+
}
|
|
260
|
+
function e2ePreparedImageManifestPath(cacheDir) {
|
|
261
|
+
return path.join(cacheDir, e2ePreparedImageManifestFileName);
|
|
262
|
+
}
|
|
263
|
+
function parseE2ePreparedImageManifest(value) {
|
|
264
|
+
if (!isObjectRecord(value) || value.schemaVersion !== e2ePreparedImageManifestSchemaVersion) {
|
|
265
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
266
|
+
}
|
|
267
|
+
if (!Array.isArray(value.entries)) {
|
|
268
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
269
|
+
}
|
|
270
|
+
const entries = value.entries.filter((entry) => {
|
|
271
|
+
if (!isObjectRecord(entry)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
return ((entry.family === 'gateway' || entry.family === 'toolVm') &&
|
|
275
|
+
typeof entry.name === 'string' &&
|
|
276
|
+
typeof entry.recipeFingerprint === 'string' &&
|
|
277
|
+
typeof entry.buildConfigPath === 'string' &&
|
|
278
|
+
typeof entry.cacheDirectory === 'string' &&
|
|
279
|
+
typeof entry.fingerprint === 'string' &&
|
|
280
|
+
typeof entry.imagePath === 'string');
|
|
281
|
+
});
|
|
282
|
+
return { entries, schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
283
|
+
}
|
|
284
|
+
async function readE2ePreparedImageManifest(cacheDir) {
|
|
285
|
+
try {
|
|
286
|
+
const parsed = JSON.parse(await fs.readFile(e2ePreparedImageManifestPath(cacheDir), 'utf8'));
|
|
287
|
+
return parseE2ePreparedImageManifest(parsed);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (error instanceof SyntaxError) {
|
|
291
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
292
|
+
}
|
|
293
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
|
|
294
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function writeE2ePreparedImageManifest(cacheDir, manifest) {
|
|
300
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
301
|
+
await fs.writeFile(e2ePreparedImageManifestPath(cacheDir), `${JSON.stringify(manifest, null, '\t')}\n`, 'utf8');
|
|
302
|
+
}
|
|
303
|
+
function e2eImageTargetKey(target) {
|
|
304
|
+
return `${target.family}:${target.name}:${target.recipeFingerprint}`;
|
|
305
|
+
}
|
|
306
|
+
function e2eManifestEntryKey(entry) {
|
|
307
|
+
return `${entry.family}:${entry.name}:${entry.recipeFingerprint}`;
|
|
308
|
+
}
|
|
309
|
+
async function materializePreparedE2eImagesFromManifest(project, targets) {
|
|
310
|
+
if (targets.some((target) => !target.e2eManifestEligible)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
const manifest = await readE2ePreparedImageManifest(project.systemConfig.cacheDir);
|
|
314
|
+
const entriesByKey = new Map(manifest.entries.map((entry) => [e2eManifestEntryKey(entry), entry]));
|
|
315
|
+
const targetReadiness = await Promise.all(targets.map(async (target) => {
|
|
316
|
+
const entry = entriesByKey.get(e2eImageTargetKey(target));
|
|
317
|
+
return entry !== undefined && (await hasBuiltImageAssets(entry.imagePath));
|
|
318
|
+
}));
|
|
319
|
+
if (targetReadiness.some((isReady) => !isReady)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
await Promise.all(targets.map(async (target) => {
|
|
323
|
+
const entry = entriesByKey.get(e2eImageTargetKey(target));
|
|
324
|
+
if (entry === undefined) {
|
|
325
|
+
throw new Error(`Missing prepared e2e image manifest entry for ${target.family}/${target.name}.`);
|
|
326
|
+
}
|
|
327
|
+
await writePreparedGondolinImage({
|
|
328
|
+
buildConfigPath: target.buildConfigPath,
|
|
329
|
+
cacheDir: target.cacheDirectory,
|
|
330
|
+
fingerprint: entry.fingerprint,
|
|
331
|
+
...(entry.fingerprintInput === undefined
|
|
332
|
+
? {}
|
|
333
|
+
: { fingerprintInput: entry.fingerprintInput }),
|
|
334
|
+
imagePath: entry.imagePath,
|
|
335
|
+
});
|
|
336
|
+
}));
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
async function recordPreparedE2eImages(project, targets) {
|
|
340
|
+
const manifest = await readE2ePreparedImageManifest(project.systemConfig.cacheDir);
|
|
341
|
+
const entriesByKey = new Map(manifest.entries.map((entry) => [e2eManifestEntryKey(entry), entry]));
|
|
342
|
+
const preparedImages = await Promise.all(targets.map(async (target) => {
|
|
343
|
+
if (!target.e2eManifestEligible) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const preparedImage = await readPreparedGondolinImage({
|
|
347
|
+
buildConfigPath: target.buildConfigPath,
|
|
348
|
+
cacheDir: target.cacheDirectory,
|
|
349
|
+
});
|
|
350
|
+
return { preparedImage, target };
|
|
351
|
+
}));
|
|
352
|
+
for (const item of preparedImages) {
|
|
353
|
+
if (item === null || item.preparedImage === undefined) {
|
|
354
|
+
continue;
|
|
123
355
|
}
|
|
124
|
-
|
|
125
|
-
|
|
356
|
+
const { preparedImage, target } = item;
|
|
357
|
+
entriesByKey.set(e2eImageTargetKey(target), {
|
|
358
|
+
buildConfigPath: path.resolve(target.buildConfigPath),
|
|
359
|
+
cacheDirectory: path.resolve(target.cacheDirectory),
|
|
360
|
+
family: target.family,
|
|
361
|
+
fingerprint: preparedImage.fingerprint,
|
|
362
|
+
...(preparedImage.fingerprintInput === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: { fingerprintInput: preparedImage.fingerprintInput }),
|
|
365
|
+
imagePath: preparedImage.imagePath,
|
|
366
|
+
name: target.name,
|
|
367
|
+
recipeFingerprint: target.recipeFingerprint,
|
|
368
|
+
});
|
|
126
369
|
}
|
|
127
|
-
|
|
370
|
+
await writeE2ePreparedImageManifest(project.systemConfig.cacheDir, {
|
|
371
|
+
entries: [...entriesByKey.values()].toSorted((leftEntry, rightEntry) => e2eManifestEntryKey(leftEntry).localeCompare(e2eManifestEntryKey(rightEntry))),
|
|
372
|
+
schemaVersion: e2ePreparedImageManifestSchemaVersion,
|
|
373
|
+
});
|
|
128
374
|
}
|
|
129
375
|
export async function findReusableGatewayImageDirectory(currentProjectRoot, gatewayBuildConfigPath, imageProfileName = 'worker') {
|
|
130
376
|
const explicitE2eCacheRoot = process.env.AGENT_VM_E2E_CACHE_DIR;
|
|
@@ -132,6 +378,9 @@ export async function findReusableGatewayImageDirectory(currentProjectRoot, gate
|
|
|
132
378
|
return null;
|
|
133
379
|
}
|
|
134
380
|
const requiredFingerprint = await computeFingerprintFromConfigPath(gatewayBuildConfigPath);
|
|
381
|
+
if (!(await pathExists(explicitE2eCacheRoot))) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
135
384
|
const tempRootEntries = await fs.readdir(explicitE2eCacheRoot, { withFileTypes: true });
|
|
136
385
|
const e2eRunDirectories = tempRootEntries
|
|
137
386
|
.filter((entry) => entry.isDirectory())
|
|
@@ -168,6 +417,23 @@ export async function seedGatewayImageCacheIfAvailable(options) {
|
|
|
168
417
|
await fs.mkdir(path.dirname(activeImageDir), { recursive: true });
|
|
169
418
|
await fs.symlink(reusableImageDir, activeImageDir, 'dir');
|
|
170
419
|
}
|
|
420
|
+
export async function prepareGatewayE2eProjectImages(options) {
|
|
421
|
+
const imageTargets = await collectE2eImageTargets(options.project);
|
|
422
|
+
if (await materializePreparedE2eImagesFromManifest(options.project, imageTargets)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
await Promise.all(Object.entries(options.project.systemConfig.imageProfiles.gateways).map(async ([profileName, gatewayProfile]) => {
|
|
426
|
+
await seedGatewayImageCacheIfAvailable({
|
|
427
|
+
activeCacheDir: options.project.systemConfig.cacheDir,
|
|
428
|
+
currentProjectRoot: options.project.tempRoot,
|
|
429
|
+
gatewayBuildConfigPath: gatewayProfile.buildConfig,
|
|
430
|
+
imageProfileName: profileName,
|
|
431
|
+
});
|
|
432
|
+
}));
|
|
433
|
+
const runBuild = options.runBuild ?? runBuildCommand;
|
|
434
|
+
await runBuild({ systemConfig: options.project.systemConfig });
|
|
435
|
+
await recordPreparedE2eImages(options.project, imageTargets);
|
|
436
|
+
}
|
|
171
437
|
export function createSmokeSecretResolver(secrets) {
|
|
172
438
|
const resolve = async (ref) => {
|
|
173
439
|
if (ref.source === 'config') {
|
|
@@ -251,43 +517,162 @@ async function assertLocalPackageFilesExist(props) {
|
|
|
251
517
|
export function resolveLocalPackagePackArgs(packDirectory) {
|
|
252
518
|
return ['pack', '--pack-destination', packDirectory, '--config.ignore-scripts=true'];
|
|
253
519
|
}
|
|
520
|
+
function parseLocalPackagePackPlan(value, packageName) {
|
|
521
|
+
if (!isJsonRecord(value)) {
|
|
522
|
+
throw new Error(`Expected pnpm pack dry-run for ${packageName} to return an object.`);
|
|
523
|
+
}
|
|
524
|
+
const files = Array.isArray(value.files)
|
|
525
|
+
? value.files.filter((file) => {
|
|
526
|
+
return isJsonRecord(file) && typeof file.path === 'string' && file.path.length > 0;
|
|
527
|
+
})
|
|
528
|
+
: [];
|
|
529
|
+
if (typeof value.name !== 'string' ||
|
|
530
|
+
typeof value.version !== 'string' ||
|
|
531
|
+
typeof value.filename !== 'string' ||
|
|
532
|
+
files.length === 0) {
|
|
533
|
+
throw new Error(`Unexpected pnpm pack dry-run result for ${packageName}.`);
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
filename: value.filename,
|
|
537
|
+
files,
|
|
538
|
+
name: value.name,
|
|
539
|
+
version: value.version,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function resolveLocalPackagePackPlan(packageDirectory, packageName) {
|
|
543
|
+
const dryRunOutput = execFileSync('pnpm', ['pack', '--dry-run', '--json', '--config.ignore-scripts=true'], {
|
|
544
|
+
cwd: packageDirectory,
|
|
545
|
+
encoding: 'utf8',
|
|
546
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
547
|
+
});
|
|
548
|
+
return parseLocalPackagePackPlan(JSON.parse(dryRunOutput), packageName);
|
|
549
|
+
}
|
|
550
|
+
function cacheSafeLocalPackageName(packageName) {
|
|
551
|
+
return packageName.replace(/^@/u, '').replaceAll('/', '__');
|
|
552
|
+
}
|
|
553
|
+
async function computeLocalPackagePackFingerprint(packageDirectory, packPlan) {
|
|
554
|
+
const hash = crypto.createHash('sha256');
|
|
555
|
+
hash.update(`${packPlan.name}\0${packPlan.version}\0${packPlan.filename}\0`);
|
|
556
|
+
for (const file of packPlan.files.toSorted((left, right) => left.path.localeCompare(right.path))) {
|
|
557
|
+
const filePath = path.join(packageDirectory, file.path);
|
|
558
|
+
// oxlint-disable-next-line no-await-in-loop -- ordered hashing keeps cache keys deterministic
|
|
559
|
+
const fileContents = await fs.readFile(filePath);
|
|
560
|
+
hash.update(`${file.path}\0`);
|
|
561
|
+
hash.update(fileContents);
|
|
562
|
+
hash.update('\0');
|
|
563
|
+
}
|
|
564
|
+
return hash.digest('hex').slice(0, 24);
|
|
565
|
+
}
|
|
254
566
|
async function packLocalPackageTarball(props) {
|
|
255
567
|
const packageJsonPath = path.join(props.packageDirectory, 'package.json');
|
|
256
568
|
await fs.access(packageJsonPath);
|
|
257
569
|
await assertLocalPackageFilesExist(props);
|
|
570
|
+
const packPlan = resolveLocalPackagePackPlan(props.packageDirectory, props.packageName);
|
|
571
|
+
const packFingerprint = await computeLocalPackagePackFingerprint(props.packageDirectory, packPlan);
|
|
572
|
+
const cachedPackageDirectory = path.join(resolveE2eCacheRoot(), 'local-package-tarballs', cacheSafeLocalPackageName(props.packageName), packFingerprint);
|
|
573
|
+
const cachedTarballPath = path.join(cachedPackageDirectory, packPlan.filename);
|
|
574
|
+
try {
|
|
575
|
+
await fs.access(cachedTarballPath);
|
|
576
|
+
return cachedTarballPath;
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
if (!isJsonRecord(error) || error.code !== 'ENOENT') {
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
258
583
|
const packDirectory = await fs.mkdtemp(path.join(os.tmpdir(), `${props.packageName}-pack-`));
|
|
259
584
|
try {
|
|
260
585
|
execFileSync('pnpm', [...resolveLocalPackagePackArgs(packDirectory)], {
|
|
261
586
|
cwd: props.packageDirectory,
|
|
262
587
|
stdio: 'pipe',
|
|
263
588
|
});
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
589
|
+
const packedTarballPath = path.join(packDirectory, packPlan.filename);
|
|
590
|
+
await fs.access(packedTarballPath);
|
|
591
|
+
await fs.mkdir(cachedPackageDirectory, { recursive: true });
|
|
592
|
+
const temporaryCachedTarballPath = path.join(cachedPackageDirectory, `${packPlan.filename}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
593
|
+
await fs.copyFile(packedTarballPath, temporaryCachedTarballPath);
|
|
594
|
+
await fs.rename(temporaryCachedTarballPath, cachedTarballPath).catch(async (error) => {
|
|
595
|
+
await fs.rm(temporaryCachedTarballPath, { force: true });
|
|
596
|
+
if (isJsonRecord(error) && error.code === 'EEXIST') {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
throw error;
|
|
600
|
+
});
|
|
601
|
+
return cachedTarballPath;
|
|
273
602
|
}
|
|
274
603
|
catch (error) {
|
|
275
604
|
await fs.rm(packDirectory, { force: true, recursive: true });
|
|
276
605
|
throw error;
|
|
277
606
|
}
|
|
607
|
+
finally {
|
|
608
|
+
await fs.rm(packDirectory, { force: true, recursive: true });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function isOwnedLocalPackagePackDirectory(packDirectory) {
|
|
612
|
+
const resolvedPackDirectory = path.resolve(packDirectory);
|
|
613
|
+
const resolvedSystemTempRoot = path.resolve(os.tmpdir());
|
|
614
|
+
if (resolvedPackDirectory === resolvedSystemTempRoot ||
|
|
615
|
+
!resolvedPackDirectory.startsWith(`${resolvedSystemTempRoot}${path.sep}`)) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
return path.basename(resolvedPackDirectory).includes('-pack-');
|
|
278
619
|
}
|
|
279
|
-
async function
|
|
620
|
+
export async function removeE2eLocalPackageTarballs(tarballPaths) {
|
|
280
621
|
await Promise.all(Array.from(new Set(tarballPaths
|
|
281
622
|
.filter((tarballPath) => tarballPath !== undefined)
|
|
282
|
-
.map((tarballPath) => path.dirname(tarballPath))))
|
|
623
|
+
.map((tarballPath) => path.dirname(tarballPath))))
|
|
624
|
+
.filter(isOwnedLocalPackagePackDirectory)
|
|
625
|
+
.map(async (packDirectory) => {
|
|
283
626
|
await fs.rm(packDirectory, { force: true, recursive: true });
|
|
284
627
|
}));
|
|
285
628
|
}
|
|
286
629
|
async function copyLocalPackageTarballsToDockerContext(options) {
|
|
287
630
|
await Promise.all(options.tarballs.map(async (tarball) => {
|
|
288
|
-
|
|
631
|
+
const targetPath = path.join(options.dockerContextDirectory, tarball.archiveName);
|
|
632
|
+
await fs.copyFile(tarball.sourcePath, targetPath);
|
|
633
|
+
await fs.utimes(targetPath, dockerContextLocalPackageTimestamp, dockerContextLocalPackageTimestamp);
|
|
289
634
|
}));
|
|
290
635
|
}
|
|
636
|
+
function shellSingleQuote(value) {
|
|
637
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
638
|
+
}
|
|
639
|
+
function createLocalDockerPackageTarball(props) {
|
|
640
|
+
return {
|
|
641
|
+
archiveName: path.basename(props.sourcePath),
|
|
642
|
+
packageName: props.packageName,
|
|
643
|
+
sourcePath: props.sourcePath,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function localDockerPackageDependencyName(tarball) {
|
|
647
|
+
return `@agent-vm/${tarball.packageName}`;
|
|
648
|
+
}
|
|
649
|
+
function renderLocalDockerPackageManifest(tarballs) {
|
|
650
|
+
const dependencies = Object.fromEntries(tarballs.map((tarball) => [
|
|
651
|
+
localDockerPackageDependencyName(tarball),
|
|
652
|
+
`file:/tmp/${tarball.archiveName}`,
|
|
653
|
+
]));
|
|
654
|
+
return `${JSON.stringify({
|
|
655
|
+
private: true,
|
|
656
|
+
dependencies,
|
|
657
|
+
pnpm: {
|
|
658
|
+
overrides: dependencies,
|
|
659
|
+
},
|
|
660
|
+
}, null, 2)}\n`;
|
|
661
|
+
}
|
|
662
|
+
function renderLocalDockerPackageInstallLines(tarballs) {
|
|
663
|
+
const manifestWriterScript = [
|
|
664
|
+
'require("node:fs").writeFileSync(',
|
|
665
|
+
'"/opt/agent-vm/local-packages/package.json",',
|
|
666
|
+
JSON.stringify(renderLocalDockerPackageManifest(tarballs)),
|
|
667
|
+
')',
|
|
668
|
+
].join('');
|
|
669
|
+
return [
|
|
670
|
+
'RUN mkdir -p /opt/agent-vm/local-packages && \\',
|
|
671
|
+
` node -e ${shellSingleQuote(manifestWriterScript)} && \\`,
|
|
672
|
+
' cd /opt/agent-vm/local-packages && pnpm install --prod --ignore-scripts && \\',
|
|
673
|
+
' rm -f ' + tarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' '),
|
|
674
|
+
];
|
|
675
|
+
}
|
|
291
676
|
async function useLocalToolVmMcpPortalPackageTarballs(options) {
|
|
292
677
|
const managedImageRelease = await resolveManagedImageRelease();
|
|
293
678
|
const baseImage = managedImageRelease.baseImages['tool-vm'];
|
|
@@ -295,35 +680,32 @@ async function useLocalToolVmMcpPortalPackageTarballs(options) {
|
|
|
295
680
|
await Promise.all(toolVmProfiles.map(async ([profileName, toolVmProfile]) => {
|
|
296
681
|
const dockerContextDirectory = path.join(options.projectRoot, 'vm-images', 'tool-vms', `${profileName}-local-mcp-portal`);
|
|
297
682
|
const dockerfilePath = path.join(dockerContextDirectory, 'Dockerfile');
|
|
298
|
-
const localConfigContractsTarballName = 'config-contracts-local.tgz';
|
|
299
|
-
const localSecretManagementTarballName = 'secret-management-local.tgz';
|
|
300
|
-
const localTarballName = 'mcp-portal-local.tgz';
|
|
301
683
|
await fs.rm(dockerContextDirectory, { force: true, recursive: true });
|
|
302
684
|
await fs.mkdir(dockerContextDirectory, { recursive: true });
|
|
685
|
+
const localPackageTarballs = [
|
|
686
|
+
createLocalDockerPackageTarball({
|
|
687
|
+
packageName: 'config-contracts',
|
|
688
|
+
sourcePath: options.localConfigContractsTarballPath,
|
|
689
|
+
}),
|
|
690
|
+
createLocalDockerPackageTarball({
|
|
691
|
+
packageName: 'secret-management',
|
|
692
|
+
sourcePath: options.localSecretManagementTarballPath,
|
|
693
|
+
}),
|
|
694
|
+
createLocalDockerPackageTarball({
|
|
695
|
+
packageName: 'mcp-portal',
|
|
696
|
+
sourcePath: options.localMcpPortalTarballPath,
|
|
697
|
+
}),
|
|
698
|
+
];
|
|
303
699
|
await copyLocalPackageTarballsToDockerContext({
|
|
304
700
|
dockerContextDirectory,
|
|
305
|
-
tarballs:
|
|
306
|
-
{
|
|
307
|
-
archiveName: localConfigContractsTarballName,
|
|
308
|
-
sourcePath: options.localConfigContractsTarballPath,
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
archiveName: localSecretManagementTarballName,
|
|
312
|
-
sourcePath: options.localSecretManagementTarballPath,
|
|
313
|
-
},
|
|
314
|
-
{ archiveName: localTarballName, sourcePath: options.localMcpPortalTarballPath },
|
|
315
|
-
],
|
|
701
|
+
tarballs: localPackageTarballs,
|
|
316
702
|
});
|
|
317
703
|
await fs.writeFile(dockerfilePath, [
|
|
318
704
|
`FROM ${baseImage.repository}:${baseImage.tag}`,
|
|
319
705
|
'',
|
|
320
706
|
'# Generated by the OpenClaw smoke harness from the local MCP Portal package.',
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
'COPY mcp-portal-local.tgz /tmp/mcp-portal-local.tgz',
|
|
324
|
-
'RUN mkdir -p /opt/agent-vm/local-packages && \\',
|
|
325
|
-
' npm install --omit=dev --no-audit --no-fund --prefix /opt/agent-vm/local-packages /tmp/config-contracts-local.tgz /tmp/secret-management-local.tgz /tmp/mcp-portal-local.tgz && \\',
|
|
326
|
-
' rm -f /tmp/config-contracts-local.tgz /tmp/secret-management-local.tgz /tmp/mcp-portal-local.tgz',
|
|
707
|
+
...localPackageTarballs.map((tarball) => `COPY ${tarball.archiveName} /tmp/${tarball.archiveName}`),
|
|
708
|
+
...renderLocalDockerPackageInstallLines(localPackageTarballs),
|
|
327
709
|
'',
|
|
328
710
|
].join('\n'), 'utf8');
|
|
329
711
|
toolVmProfile.dockerfile = dockerfilePath;
|
|
@@ -353,16 +735,13 @@ export async function useLocalToolVmMcpPortalPackage(options) {
|
|
|
353
735
|
});
|
|
354
736
|
}
|
|
355
737
|
finally {
|
|
356
|
-
await
|
|
738
|
+
await removeE2eLocalPackageTarballs([
|
|
357
739
|
localConfigContractsTarballPath,
|
|
358
740
|
localSecretManagementTarballPath,
|
|
359
741
|
localMcpPortalTarballPath,
|
|
360
742
|
]);
|
|
361
743
|
}
|
|
362
744
|
}
|
|
363
|
-
function localPackageTarballArchiveName(packageName) {
|
|
364
|
-
return `${packageName}-local.tgz`;
|
|
365
|
-
}
|
|
366
745
|
async function writeManagedOpenClawE2eDockerfileBase(options) {
|
|
367
746
|
const managedImageRelease = await resolveManagedImageRelease();
|
|
368
747
|
const result = await generateManagedDockerfile({
|
|
@@ -506,34 +885,34 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
|
|
|
506
885
|
});
|
|
507
886
|
try {
|
|
508
887
|
const localPackageTarballs = [
|
|
509
|
-
{
|
|
510
|
-
|
|
888
|
+
createLocalDockerPackageTarball({
|
|
889
|
+
packageName: 'config-contracts',
|
|
511
890
|
sourcePath: localConfigContractsTarballPath,
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
|
|
891
|
+
}),
|
|
892
|
+
createLocalDockerPackageTarball({
|
|
893
|
+
packageName: 'secret-management',
|
|
515
894
|
sourcePath: localSecretManagementTarballPath,
|
|
516
|
-
},
|
|
517
|
-
{
|
|
518
|
-
|
|
895
|
+
}),
|
|
896
|
+
createLocalDockerPackageTarball({
|
|
897
|
+
packageName: 'gondolin-adapter',
|
|
519
898
|
sourcePath: localGondolinAdapterTarballPath,
|
|
520
|
-
},
|
|
521
|
-
{
|
|
522
|
-
|
|
899
|
+
}),
|
|
900
|
+
createLocalDockerPackageTarball({
|
|
901
|
+
packageName: 'gateway-interface',
|
|
523
902
|
sourcePath: localGatewayInterfaceTarballPath,
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
|
|
903
|
+
}),
|
|
904
|
+
createLocalDockerPackageTarball({
|
|
905
|
+
packageName: 'mcp-portal',
|
|
527
906
|
sourcePath: localMcpPortalTarballPath,
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
|
|
907
|
+
}),
|
|
908
|
+
createLocalDockerPackageTarball({
|
|
909
|
+
packageName: 'openclaw-agent-vm-plugin',
|
|
531
910
|
sourcePath: localOpenClawAgentVmPluginTarballPath,
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
|
|
911
|
+
}),
|
|
912
|
+
createLocalDockerPackageTarball({
|
|
913
|
+
packageName: 'openclaw-mcp-portal-plugin',
|
|
535
914
|
sourcePath: localOpenClawMcpPortalPluginTarballPath,
|
|
536
|
-
},
|
|
915
|
+
}),
|
|
537
916
|
];
|
|
538
917
|
const dockerfilePath = await writeManagedOpenClawE2eDockerfileBase({
|
|
539
918
|
dockerContextDirectory,
|
|
@@ -555,12 +934,7 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
|
|
|
555
934
|
'',
|
|
556
935
|
'# Local package overlay generated by the OpenClaw smoke harness.',
|
|
557
936
|
...localPackageTarballs.map((tarball) => `COPY ${tarball.archiveName} /tmp/${tarball.archiveName}`),
|
|
558
|
-
|
|
559
|
-
' npm install --omit=dev --no-audit --no-fund --prefix /opt/agent-vm/local-packages ' +
|
|
560
|
-
localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' ') +
|
|
561
|
-
' && \\',
|
|
562
|
-
' rm -f ' +
|
|
563
|
-
localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' '),
|
|
937
|
+
...renderLocalDockerPackageInstallLines(localPackageTarballs),
|
|
564
938
|
'RUN package_root="/opt/agent-vm/local-packages/node_modules" && \\',
|
|
565
939
|
' global_package_root="$(pnpm root -g)" && \\',
|
|
566
940
|
' mkdir -p "$global_package_root" /home/openclaw/.openclaw/extensions && \\',
|
|
@@ -573,7 +947,7 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
|
|
|
573
947
|
delete gatewayProfile.source;
|
|
574
948
|
}
|
|
575
949
|
finally {
|
|
576
|
-
await
|
|
950
|
+
await removeE2eLocalPackageTarballs([
|
|
577
951
|
localConfigContractsTarballPath,
|
|
578
952
|
localSecretManagementTarballPath,
|
|
579
953
|
localGondolinAdapterTarballPath,
|
|
@@ -608,22 +982,22 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
608
982
|
});
|
|
609
983
|
try {
|
|
610
984
|
const localPackageTarballs = [
|
|
611
|
-
{
|
|
612
|
-
|
|
985
|
+
createLocalDockerPackageTarball({
|
|
986
|
+
packageName: 'secret-management',
|
|
613
987
|
sourcePath: localSecretManagementTarballPath,
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
|
|
988
|
+
}),
|
|
989
|
+
createLocalDockerPackageTarball({
|
|
990
|
+
packageName: 'gondolin-adapter',
|
|
617
991
|
sourcePath: localGondolinAdapterTarballPath,
|
|
618
|
-
},
|
|
619
|
-
{
|
|
620
|
-
|
|
992
|
+
}),
|
|
993
|
+
createLocalDockerPackageTarball({
|
|
994
|
+
packageName: 'gateway-interface',
|
|
621
995
|
sourcePath: localGatewayInterfaceTarballPath,
|
|
622
|
-
},
|
|
623
|
-
{
|
|
624
|
-
|
|
996
|
+
}),
|
|
997
|
+
createLocalDockerPackageTarball({
|
|
998
|
+
packageName: 'openclaw-agent-vm-plugin',
|
|
625
999
|
sourcePath: localOpenClawAgentVmPluginTarballPath,
|
|
626
|
-
},
|
|
1000
|
+
}),
|
|
627
1001
|
];
|
|
628
1002
|
const dockerfilePath = await writeManagedOpenClawE2eDockerfileBase({
|
|
629
1003
|
dockerContextDirectory,
|
|
@@ -638,12 +1012,7 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
638
1012
|
'',
|
|
639
1013
|
'# Local plugin overlay generated by the OpenClaw smoke harness.',
|
|
640
1014
|
...localPackageTarballs.map((tarball) => `COPY ${tarball.archiveName} /tmp/${tarball.archiveName}`),
|
|
641
|
-
|
|
642
|
-
' npm install --omit=dev --no-audit --no-fund --prefix /opt/agent-vm/local-packages ' +
|
|
643
|
-
localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' ') +
|
|
644
|
-
' && \\',
|
|
645
|
-
' rm -f ' +
|
|
646
|
-
localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' '),
|
|
1015
|
+
...renderLocalDockerPackageInstallLines(localPackageTarballs),
|
|
647
1016
|
'RUN package_root="/opt/agent-vm/local-packages/node_modules" && \\',
|
|
648
1017
|
' global_package_root="$(pnpm root -g)" && \\',
|
|
649
1018
|
' mkdir -p "$global_package_root" /home/openclaw/.openclaw/extensions && \\',
|
|
@@ -655,7 +1024,7 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
655
1024
|
delete gatewayProfile.source;
|
|
656
1025
|
}
|
|
657
1026
|
finally {
|
|
658
|
-
await
|
|
1027
|
+
await removeE2eLocalPackageTarballs([
|
|
659
1028
|
localSecretManagementTarballPath,
|
|
660
1029
|
localGondolinAdapterTarballPath,
|
|
661
1030
|
localGatewayInterfaceTarballPath,
|
|
@@ -690,21 +1059,10 @@ export async function scaffoldOpenClawE2eProject(options) {
|
|
|
690
1059
|
};
|
|
691
1060
|
}
|
|
692
1061
|
export async function prepareLocalWorkerPackageForGatewayImage(repoRoot) {
|
|
693
|
-
await
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
cwd: path.join(repoRoot, 'packages', 'agent-vm-worker'),
|
|
697
|
-
stdio: 'pipe',
|
|
1062
|
+
return await packLocalPackageTarball({
|
|
1063
|
+
packageDirectory: path.join(repoRoot, 'packages', 'agent-vm-worker'),
|
|
1064
|
+
packageName: 'agent-vm-worker',
|
|
698
1065
|
});
|
|
699
|
-
const packedTarballs = (await fs.readdir(packDirectory)).filter((fileName) => fileName.endsWith('.tgz'));
|
|
700
|
-
const [packedTarballName] = packedTarballs;
|
|
701
|
-
if (packedTarballName === undefined) {
|
|
702
|
-
throw new Error('Failed to pack local agent-vm-worker tarball for smoke image.');
|
|
703
|
-
}
|
|
704
|
-
if (packedTarballs.length > 1) {
|
|
705
|
-
throw new Error('Expected pnpm pack to produce exactly one agent-vm-worker tarball.');
|
|
706
|
-
}
|
|
707
|
-
return path.join(packDirectory, packedTarballName);
|
|
708
1066
|
}
|
|
709
1067
|
export async function scaffoldWorkerE2eProject(options) {
|
|
710
1068
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), options.prefix));
|