@agent-vm/agent-vm 0.0.93 → 0.0.94
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 +4 -1
- package/dist/cli/agent-vm-cli-support.d.ts.map +1 -1
- package/dist/cli/agent-vm-cli-support.js.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 +19 -0
- package/dist/cli/controller-operation-commands.d.ts.map +1 -1
- package/dist/cli/controller-operation-commands.js +63 -44
- package/dist/cli/controller-operation-commands.js.map +1 -1
- package/dist/controller/controller-runtime.js +1 -1
- package/dist/controller/controller-runtime.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/gateway/mcp-portal-effective-config.d.ts +11 -0
- package/dist/gateway/mcp-portal-effective-config.d.ts.map +1 -1
- package/dist/gateway/mcp-portal-effective-config.js +27 -8
- 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 +371 -37
- 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/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)}`;
|
|
123
155
|
}
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
|
126
164
|
}
|
|
127
|
-
throw new Error(
|
|
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;
|
|
355
|
+
}
|
|
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
|
+
});
|
|
369
|
+
}
|
|
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,41 +517,120 @@ 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
|
+
}
|
|
278
610
|
}
|
|
279
|
-
|
|
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-');
|
|
619
|
+
}
|
|
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
|
}
|
|
291
636
|
async function useLocalToolVmMcpPortalPackageTarballs(options) {
|
|
@@ -353,7 +698,7 @@ export async function useLocalToolVmMcpPortalPackage(options) {
|
|
|
353
698
|
});
|
|
354
699
|
}
|
|
355
700
|
finally {
|
|
356
|
-
await
|
|
701
|
+
await removeE2eLocalPackageTarballs([
|
|
357
702
|
localConfigContractsTarballPath,
|
|
358
703
|
localSecretManagementTarballPath,
|
|
359
704
|
localMcpPortalTarballPath,
|
|
@@ -573,7 +918,7 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
|
|
|
573
918
|
delete gatewayProfile.source;
|
|
574
919
|
}
|
|
575
920
|
finally {
|
|
576
|
-
await
|
|
921
|
+
await removeE2eLocalPackageTarballs([
|
|
577
922
|
localConfigContractsTarballPath,
|
|
578
923
|
localSecretManagementTarballPath,
|
|
579
924
|
localGondolinAdapterTarballPath,
|
|
@@ -655,7 +1000,7 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
655
1000
|
delete gatewayProfile.source;
|
|
656
1001
|
}
|
|
657
1002
|
finally {
|
|
658
|
-
await
|
|
1003
|
+
await removeE2eLocalPackageTarballs([
|
|
659
1004
|
localSecretManagementTarballPath,
|
|
660
1005
|
localGondolinAdapterTarballPath,
|
|
661
1006
|
localGatewayInterfaceTarballPath,
|
|
@@ -690,21 +1035,10 @@ export async function scaffoldOpenClawE2eProject(options) {
|
|
|
690
1035
|
};
|
|
691
1036
|
}
|
|
692
1037
|
export async function prepareLocalWorkerPackageForGatewayImage(repoRoot) {
|
|
693
|
-
await
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
cwd: path.join(repoRoot, 'packages', 'agent-vm-worker'),
|
|
697
|
-
stdio: 'pipe',
|
|
1038
|
+
return await packLocalPackageTarball({
|
|
1039
|
+
packageDirectory: path.join(repoRoot, 'packages', 'agent-vm-worker'),
|
|
1040
|
+
packageName: 'agent-vm-worker',
|
|
698
1041
|
});
|
|
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
1042
|
}
|
|
709
1043
|
export async function scaffoldWorkerE2eProject(options) {
|
|
710
1044
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), options.prefix));
|