@agent-vm/gondolin-vm-adapter 0.0.115
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/LICENSE +21 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1772 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1772 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path, { basename } from "node:path";
|
|
4
|
+
import { assertPositiveHostProcessId, createOwnedHostDirectoryController, validateManagedVmFilteredWorkspacePolicy } from "@agent-vm/managed-vm";
|
|
5
|
+
import { ERRNO, MemoryProvider, ReadonlyProvider, RealFSProvider, ShadowProvider, VM, VirtualDirent, createHttpHooks, createShadowPathPredicate, createVirtualDirStats, getInfoFromSshExecRequest, isWriteFlag, validateBuildConfig } from "@earendil-works/gondolin";
|
|
6
|
+
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import fs$1 from "node:fs";
|
|
12
|
+
import * as net$1 from "node:net";
|
|
13
|
+
import net from "node:net";
|
|
14
|
+
import * as dns from "node:dns";
|
|
15
|
+
//#region src/rootfs-init-extra.ts
|
|
16
|
+
const agentVmRootfsInitExtraScript = `# Generated by agent-vm.
|
|
17
|
+
# Gondolin mounts devtmpfs over /dev at boot, so Docker-image /dev symlinks are hidden.
|
|
18
|
+
mkdir -p /dev
|
|
19
|
+
ln -sfn /proc/self/fd /dev/fd 2>/dev/null || true
|
|
20
|
+
ln -sfn /proc/self/fd/0 /dev/stdin 2>/dev/null || true
|
|
21
|
+
ln -sfn /proc/self/fd/1 /dev/stdout 2>/dev/null || true
|
|
22
|
+
ln -sfn /proc/self/fd/2 /dev/stderr 2>/dev/null || true
|
|
23
|
+
if [ ! -e /dev/ptmx ] && [ -e /dev/pts/ptmx ]; then
|
|
24
|
+
ln -sfn pts/ptmx /dev/ptmx 2>/dev/null || true
|
|
25
|
+
fi
|
|
26
|
+
`;
|
|
27
|
+
const managedGatewayInputStagingRoot = "/run/agent-vm/managed-gateway-inputs";
|
|
28
|
+
const managedGatewayInputRoot = "/run/agent-vm/managed-gateway";
|
|
29
|
+
const managedGatewayRuntimeRoot = "/run/agent-vm/gateway-runtime";
|
|
30
|
+
const managedGatewayLogRoot = "/var/log/agent-vm";
|
|
31
|
+
function frameworkBootCommand(frameworkBootEntry) {
|
|
32
|
+
switch (frameworkBootEntry) {
|
|
33
|
+
case "hermes-framework-service": return "/usr/local/bin/agent-vm-hermes-gateway";
|
|
34
|
+
case "openclaw-framework-service": return "/usr/local/bin/openclaw gateway --port 18789";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function frameworkLogFileName(frameworkBootEntry) {
|
|
38
|
+
return frameworkBootEntry === "openclaw-framework-service" ? "openclaw-service.log" : "hermes-service.log";
|
|
39
|
+
}
|
|
40
|
+
function renderManagedGatewayRootfsInitScript(projection) {
|
|
41
|
+
const selectedFrameworkBootCommand = frameworkBootCommand(projection.frameworkBootEntry);
|
|
42
|
+
const selectedFrameworkLogPath = `${managedGatewayLogRoot}/${frameworkLogFileName(projection.frameworkBootEntry)}`;
|
|
43
|
+
return `# Fixed managed Gateway sibling boot entries.
|
|
44
|
+
managed_gateway_input_staging_root=${managedGatewayInputStagingRoot}
|
|
45
|
+
managed_gateway_input_root=${managedGatewayInputRoot}
|
|
46
|
+
mkdir -p ${managedGatewayLogRoot}
|
|
47
|
+
install -d -m 0700 "$managed_gateway_input_root"
|
|
48
|
+
install -d -m 0700 ${managedGatewayRuntimeRoot}
|
|
49
|
+
install -m 0600 /dev/null ${managedGatewayLogRoot}/tool-portal-service.log
|
|
50
|
+
install -m 0600 /dev/null ${selectedFrameworkLogPath}
|
|
51
|
+
(
|
|
52
|
+
for managed_gateway_input_name in tool-portal.environment.sh tool-portal-service.json mcp.config.json; do
|
|
53
|
+
if [ ! -f "$managed_gateway_input_staging_root/$managed_gateway_input_name" ]; then exit 78; fi
|
|
54
|
+
install -m 0600 "$managed_gateway_input_staging_root/$managed_gateway_input_name" "$managed_gateway_input_root/$managed_gateway_input_name" || exit 78
|
|
55
|
+
done
|
|
56
|
+
exec /bin/sh -c 'set -a; . ${managedGatewayInputRoot}/tool-portal.environment.sh; set +a; exec /usr/local/bin/agent-vm-gateway-runtime --config ${managedGatewayInputRoot}/tool-portal-service.json'
|
|
57
|
+
) >> ${managedGatewayLogRoot}/tool-portal-service.log 2>&1 &
|
|
58
|
+
(
|
|
59
|
+
for managed_gateway_input_name in ${projection.frameworkBootEntry === "hermes-framework-service" ? "framework.environment.sh framework-service.json config.yaml" : "framework.environment.sh framework-service.json"}; do
|
|
60
|
+
if [ ! -f "$managed_gateway_input_staging_root/$managed_gateway_input_name" ]; then exit 78; fi
|
|
61
|
+
install -m 0600 "$managed_gateway_input_staging_root/$managed_gateway_input_name" "$managed_gateway_input_root/$managed_gateway_input_name" || exit 78
|
|
62
|
+
done
|
|
63
|
+
exec /bin/sh -c 'set -a; . ${managedGatewayInputRoot}/framework.environment.sh; set +a; exec ${selectedFrameworkBootCommand}'
|
|
64
|
+
) >> ${selectedFrameworkLogPath} 2>&1 &
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
function resolveBuildConfigPath(filePath, configDir) {
|
|
68
|
+
return path.isAbsolute(filePath) ? filePath : path.resolve(configDir ?? process.cwd(), filePath);
|
|
69
|
+
}
|
|
70
|
+
async function readExistingRootfsInitExtra(buildConfig, configDir) {
|
|
71
|
+
const existingRootfsInitExtra = buildConfig.init?.rootfsInitExtra;
|
|
72
|
+
if (!existingRootfsInitExtra) return;
|
|
73
|
+
const resolvedRootfsInitExtra = resolveBuildConfigPath(existingRootfsInitExtra, configDir);
|
|
74
|
+
try {
|
|
75
|
+
return await fs.readFile(resolvedRootfsInitExtra, "utf8");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
throw new Error(`Failed to read Gondolin rootfs init extra '${resolvedRootfsInitExtra}': ${message}`, { cause: error });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function composeRootfsInitExtra(existingRootfsInitExtra) {
|
|
82
|
+
return existingRootfsInitExtra ? `${agentVmRootfsInitExtraScript.trimEnd()}\n\n${existingRootfsInitExtra}` : agentVmRootfsInitExtraScript;
|
|
83
|
+
}
|
|
84
|
+
async function resolveRootfsInitExtra(options) {
|
|
85
|
+
if (options.managedGatewayBoot !== void 0 && options.buildConfig.init?.rootfsInitExtra) throw new Error("Managed Gateway images cannot compose deployment-authored rootfs init authority.");
|
|
86
|
+
const existingRootfsInitExtra = await readExistingRootfsInitExtra(options.buildConfig, options.configDir);
|
|
87
|
+
const managedGatewayRootfsInitExtra = options.managedGatewayBoot === void 0 ? void 0 : renderManagedGatewayRootfsInitScript(options.managedGatewayBoot);
|
|
88
|
+
const composedRootfsInitExtra = composeRootfsInitExtra(existingRootfsInitExtra);
|
|
89
|
+
return {
|
|
90
|
+
content: managedGatewayRootfsInitExtra === void 0 ? composedRootfsInitExtra : `${composedRootfsInitExtra.trimEnd()}\n\n${managedGatewayRootfsInitExtra}`,
|
|
91
|
+
fingerprintInput: {
|
|
92
|
+
agentVmRootfsInitExtra: agentVmRootfsInitExtraScript,
|
|
93
|
+
...existingRootfsInitExtra === void 0 ? {} : { deploymentRootfsInitExtra: existingRootfsInitExtra },
|
|
94
|
+
...options.managedGatewayBoot === void 0 ? {} : { managedGatewayBoot: options.managedGatewayBoot }
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function prepareBuildConfigWithAgentVmRootfsInitExtra(options) {
|
|
99
|
+
const rootfsInitExtraPath = path.join(options.imagePath, "agent-vm-rootfs-init-extra.sh");
|
|
100
|
+
await fs.writeFile(rootfsInitExtraPath, options.rootfsInitExtraContent, {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
mode: 493
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
...options.buildConfig,
|
|
106
|
+
init: {
|
|
107
|
+
...options.buildConfig.init,
|
|
108
|
+
rootfsInitExtra: rootfsInitExtraPath
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/build-pipeline.ts
|
|
114
|
+
const buildImageAssetFileNames = [
|
|
115
|
+
"manifest.json",
|
|
116
|
+
"rootfs.ext4",
|
|
117
|
+
"initramfs.cpio.lz4",
|
|
118
|
+
"vmlinuz-virt"
|
|
119
|
+
];
|
|
120
|
+
function requireValidBuildConfig(buildConfig) {
|
|
121
|
+
if (!validateBuildConfig(buildConfig)) throw new Error("Managed VM image recipe has an invalid build shape.");
|
|
122
|
+
return buildConfig;
|
|
123
|
+
}
|
|
124
|
+
function createGondolinImageBuildTooling() {
|
|
125
|
+
return {
|
|
126
|
+
async buildImage(options, dependencies) {
|
|
127
|
+
return await buildImage({
|
|
128
|
+
...options,
|
|
129
|
+
buildConfig: requireValidBuildConfig(options.buildConfig)
|
|
130
|
+
}, dependencies);
|
|
131
|
+
},
|
|
132
|
+
async computeFingerprint(options) {
|
|
133
|
+
return (await computeEffectiveBuildFingerprint({
|
|
134
|
+
...options,
|
|
135
|
+
buildConfig: requireValidBuildConfig(options.buildConfig)
|
|
136
|
+
})).fingerprint;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const inFlightImageBuilds = /* @__PURE__ */ new Map();
|
|
141
|
+
const gondolinWorkDirectoryName = ".agent-vm-gondolin-work";
|
|
142
|
+
function isRecord$1(value) {
|
|
143
|
+
return typeof value === "object" && value !== null;
|
|
144
|
+
}
|
|
145
|
+
function stableSerialize(value) {
|
|
146
|
+
if (Array.isArray(value)) return `[${value.map((entry) => stableSerialize(entry)).join(",")}]`;
|
|
147
|
+
if (isRecord$1(value)) return `{${Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).toSorted(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([entryKey, entryValue]) => `${JSON.stringify(entryKey)}:${stableSerialize(entryValue)}`).join(",")}}`;
|
|
148
|
+
return JSON.stringify(value);
|
|
149
|
+
}
|
|
150
|
+
function isMissingPathError(error) {
|
|
151
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
152
|
+
}
|
|
153
|
+
async function pathExists(filePath) {
|
|
154
|
+
try {
|
|
155
|
+
await fs.access(filePath);
|
|
156
|
+
return true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (!isMissingPathError(error)) throw error;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function hasBuiltImageAssets(outputDirectoryPath) {
|
|
163
|
+
for (const fileName of buildImageAssetFileNames) if (!await pathExists(path.join(outputDirectoryPath, fileName))) return false;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
async function loadBuildAssets() {
|
|
167
|
+
const gondolinModule = await import("@earendil-works/gondolin");
|
|
168
|
+
return async (buildConfig, outputDirectory, configDir, workDir, verbose) => await gondolinModule.buildAssets(buildConfig, {
|
|
169
|
+
outputDir: outputDirectory,
|
|
170
|
+
verbose: verbose ?? false,
|
|
171
|
+
...configDir ? { configDir } : {},
|
|
172
|
+
...workDir ? { workDir } : {}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function createRedirectedWrite(output) {
|
|
176
|
+
return ((chunk, encodingOrCallback, callback) => {
|
|
177
|
+
const writeCallback = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
178
|
+
const wrote = output.write(chunk);
|
|
179
|
+
writeCallback?.();
|
|
180
|
+
return wrote;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function withCapturedBuildOutput(output, fn) {
|
|
184
|
+
if (!output) return await fn();
|
|
185
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
186
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
187
|
+
const originalCi = process.env.CI;
|
|
188
|
+
const redirectedWrite = createRedirectedWrite(output);
|
|
189
|
+
process.stderr.write = redirectedWrite;
|
|
190
|
+
process.stdout.write = redirectedWrite;
|
|
191
|
+
process.env.CI = "true";
|
|
192
|
+
try {
|
|
193
|
+
return await fn();
|
|
194
|
+
} finally {
|
|
195
|
+
process.stderr.write = originalStderrWrite;
|
|
196
|
+
process.stdout.write = originalStdoutWrite;
|
|
197
|
+
if (originalCi === void 0) delete process.env.CI;
|
|
198
|
+
else process.env.CI = originalCi;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function computeBuildFingerprint(buildConfig, gondolinVersion = "unknown", fingerprintInput) {
|
|
202
|
+
const payload = fingerprintInput === void 0 ? `${stableSerialize(buildConfig)}|${gondolinVersion}` : `${stableSerialize(buildConfig)}|${gondolinVersion}|${stableSerialize(fingerprintInput)}`;
|
|
203
|
+
return crypto.createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
204
|
+
}
|
|
205
|
+
async function computeEffectiveBuildFingerprint(options) {
|
|
206
|
+
const resolvedRootfsInitExtra = await resolveRootfsInitExtra({
|
|
207
|
+
buildConfig: options.buildConfig,
|
|
208
|
+
...options.configDir ? { configDir: options.configDir } : {},
|
|
209
|
+
...options.managedGatewayBoot === void 0 ? {} : { managedGatewayBoot: options.managedGatewayBoot }
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
fingerprint: computeBuildFingerprint(options.buildConfig, options.gondolinVersion, {
|
|
213
|
+
agentVmRootfsInitExtra: resolvedRootfsInitExtra.fingerprintInput,
|
|
214
|
+
...options.fingerprintInput === void 0 ? {} : { callerFingerprintInput: options.fingerprintInput }
|
|
215
|
+
}),
|
|
216
|
+
rootfsInitExtraContent: resolvedRootfsInitExtra.content
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async function buildImage(options, dependencies = {}) {
|
|
220
|
+
const effectiveBuildFingerprint = await computeEffectiveBuildFingerprint({
|
|
221
|
+
buildConfig: options.buildConfig,
|
|
222
|
+
...options.configDir ? { configDir: options.configDir } : {},
|
|
223
|
+
...options.fingerprintInput === void 0 ? {} : { fingerprintInput: options.fingerprintInput },
|
|
224
|
+
...options.managedGatewayBoot === void 0 ? {} : { managedGatewayBoot: options.managedGatewayBoot },
|
|
225
|
+
...dependencies.gondolinVersion ? { gondolinVersion: dependencies.gondolinVersion } : {}
|
|
226
|
+
});
|
|
227
|
+
const fingerprint = effectiveBuildFingerprint.fingerprint;
|
|
228
|
+
const imagePath = path.join(options.cacheDir, fingerprint);
|
|
229
|
+
const buildImageForFingerprint = async () => {
|
|
230
|
+
if (options.fullReset) await fs.rm(imagePath, {
|
|
231
|
+
recursive: true,
|
|
232
|
+
force: true
|
|
233
|
+
});
|
|
234
|
+
if (await hasBuiltImageAssets(imagePath)) return {
|
|
235
|
+
built: false,
|
|
236
|
+
fingerprint,
|
|
237
|
+
imagePath
|
|
238
|
+
};
|
|
239
|
+
await fs.mkdir(imagePath, { recursive: true });
|
|
240
|
+
const buildAssetsImplementation = dependencies.buildAssets ?? await loadBuildAssets();
|
|
241
|
+
const effectiveBuildConfig = await prepareBuildConfigWithAgentVmRootfsInitExtra({
|
|
242
|
+
buildConfig: options.buildConfig,
|
|
243
|
+
imagePath,
|
|
244
|
+
rootfsInitExtraContent: effectiveBuildFingerprint.rootfsInitExtraContent
|
|
245
|
+
});
|
|
246
|
+
const gondolinWorkDir = path.join(imagePath, gondolinWorkDirectoryName);
|
|
247
|
+
await fs.rm(gondolinWorkDir, {
|
|
248
|
+
recursive: true,
|
|
249
|
+
force: true
|
|
250
|
+
});
|
|
251
|
+
try {
|
|
252
|
+
await withCapturedBuildOutput(options.output, async () => {
|
|
253
|
+
await buildAssetsImplementation(effectiveBuildConfig, imagePath, options.configDir, gondolinWorkDir, options.output !== void 0);
|
|
254
|
+
});
|
|
255
|
+
} finally {
|
|
256
|
+
await fs.rm(gondolinWorkDir, {
|
|
257
|
+
recursive: true,
|
|
258
|
+
force: true
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (!await hasBuiltImageAssets(imagePath)) throw new Error(`Expected Gondolin assets to be written to ${imagePath}.`);
|
|
262
|
+
return {
|
|
263
|
+
built: true,
|
|
264
|
+
fingerprint,
|
|
265
|
+
imagePath
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
if (options.output) return await buildImageForFingerprint();
|
|
269
|
+
const inFlightKey = path.resolve(imagePath);
|
|
270
|
+
const existingBuild = inFlightImageBuilds.get(inFlightKey);
|
|
271
|
+
if (existingBuild) return await existingBuild;
|
|
272
|
+
const buildPromise = buildImageForFingerprint();
|
|
273
|
+
inFlightImageBuilds.set(inFlightKey, buildPromise);
|
|
274
|
+
try {
|
|
275
|
+
return await buildPromise;
|
|
276
|
+
} finally {
|
|
277
|
+
if (inFlightImageBuilds.get(inFlightKey) === buildPromise) inFlightImageBuilds.delete(inFlightKey);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/exact-recorded-process-termination.ts
|
|
282
|
+
const execFileAsync = promisify(execFile);
|
|
283
|
+
const terminationStageTimeoutMs = 2e3;
|
|
284
|
+
const processIdentityPollIntervalMs = 100;
|
|
285
|
+
const darwinProcessCommandNameLimit = 16;
|
|
286
|
+
const linuxTaskCommandNameLimit = 15;
|
|
287
|
+
function processErrorCode(error) {
|
|
288
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
289
|
+
}
|
|
290
|
+
function isManagedVmHostCommand(command) {
|
|
291
|
+
return /\b(qemu-system|krun)\b/u.test(command);
|
|
292
|
+
}
|
|
293
|
+
function isMatchingDarwinFallbackCommand(options) {
|
|
294
|
+
const fallbackCommandMatch = /^\(([^()]+)\)$/u.exec(options.currentCommand);
|
|
295
|
+
if (fallbackCommandMatch === null) return false;
|
|
296
|
+
const recordedExecutable = options.recordedCommand.split(/[ \t]/u, 1)[0];
|
|
297
|
+
if (recordedExecutable === void 0 || recordedExecutable.length === 0) return false;
|
|
298
|
+
return fallbackCommandMatch[1] === basename(recordedExecutable).slice(0, darwinProcessCommandNameLimit);
|
|
299
|
+
}
|
|
300
|
+
function isMatchingLinuxTaskFallbackCommand(options) {
|
|
301
|
+
const fallbackCommandMatch = /^\[([^\r\n]+)\]$/u.exec(options.currentCommand);
|
|
302
|
+
if (fallbackCommandMatch === null) return false;
|
|
303
|
+
const recordedExecutable = options.recordedCommand.split(/[ \t]/u, 1)[0];
|
|
304
|
+
if (recordedExecutable === void 0 || recordedExecutable.length === 0) return false;
|
|
305
|
+
return fallbackCommandMatch[1] === basename(recordedExecutable).slice(0, linuxTaskCommandNameLimit);
|
|
306
|
+
}
|
|
307
|
+
function findProcessCommandBoundary(processDescription) {
|
|
308
|
+
let tokenCount = 0;
|
|
309
|
+
let insideToken = false;
|
|
310
|
+
for (let index = 0; index < processDescription.length; index += 1) {
|
|
311
|
+
const character = processDescription[index];
|
|
312
|
+
const isWhitespace = character === " " || character === " ";
|
|
313
|
+
if (!isWhitespace && !insideToken) insideToken = true;
|
|
314
|
+
else if (isWhitespace && insideToken) {
|
|
315
|
+
insideToken = false;
|
|
316
|
+
tokenCount += 1;
|
|
317
|
+
if (tokenCount === 5) return index;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function parseProcessIdentityOutput(hostProcessId, processDescription) {
|
|
323
|
+
const trimmedDescription = processDescription.trim();
|
|
324
|
+
const processStateBoundary = trimmedDescription.search(/[ \t]/u);
|
|
325
|
+
if (processStateBoundary === -1) throw new Error(`Unable to confirm host process identity for pid ${String(hostProcessId)} because ps returned malformed identity output.`);
|
|
326
|
+
const processState = trimmedDescription.slice(0, processStateBoundary).trim();
|
|
327
|
+
const startAndCommandDescription = trimmedDescription.slice(processStateBoundary).trim();
|
|
328
|
+
const commandBoundary = findProcessCommandBoundary(startAndCommandDescription);
|
|
329
|
+
if (commandBoundary === null) throw new Error(`Unable to confirm host process identity for pid ${String(hostProcessId)} because ps returned malformed identity output.`);
|
|
330
|
+
const processStartIdentity = startAndCommandDescription.slice(0, commandBoundary).trim();
|
|
331
|
+
const command = startAndCommandDescription.slice(commandBoundary).trim();
|
|
332
|
+
if (processState.length === 0 || processStartIdentity.length === 0 || command.length === 0) throw new Error(`Unable to confirm host process identity for pid ${String(hostProcessId)} because ps omitted the process state, start, or command.`);
|
|
333
|
+
return {
|
|
334
|
+
command,
|
|
335
|
+
processStartIdentity,
|
|
336
|
+
processState
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async function readGondolinHostProcessIdentity(hostProcessId) {
|
|
340
|
+
try {
|
|
341
|
+
const { stdout } = await execFileAsync("ps", [
|
|
342
|
+
"-p",
|
|
343
|
+
String(hostProcessId),
|
|
344
|
+
"-o",
|
|
345
|
+
"state=",
|
|
346
|
+
"-o",
|
|
347
|
+
"lstart=",
|
|
348
|
+
"-o",
|
|
349
|
+
"command="
|
|
350
|
+
]);
|
|
351
|
+
if (stdout.trim().length === 0) throw new Error(`Unable to confirm host process identity for pid ${String(hostProcessId)} because ps returned empty output.`);
|
|
352
|
+
return parseProcessIdentityOutput(hostProcessId, stdout);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (processErrorCode(error) === 1) return null;
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function sendHostProcessSignal(hostProcessId, signal) {
|
|
359
|
+
process.kill(hostProcessId, signal);
|
|
360
|
+
}
|
|
361
|
+
async function sleep(delayMs) {
|
|
362
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
363
|
+
}
|
|
364
|
+
const defaultDependencies = {
|
|
365
|
+
now: Date.now,
|
|
366
|
+
readProcessIdentity: readGondolinHostProcessIdentity,
|
|
367
|
+
sendSignal: sendHostProcessSignal,
|
|
368
|
+
sleep
|
|
369
|
+
};
|
|
370
|
+
function assertTerminationRequest(request) {
|
|
371
|
+
if (request.contextLabel.trim().length === 0) throw new Error("Exact managed VM process termination requires a non-empty context label.");
|
|
372
|
+
if (!Number.isSafeInteger(request.identity.hostProcessId) || request.identity.hostProcessId <= 0) throw new Error("Exact managed VM process termination requires a positive safe host pid.");
|
|
373
|
+
if (request.identity.vmId.length === 0 || request.identity.processStartIdentity.length === 0 || request.identity.command.length === 0) throw new Error("Exact managed VM process termination requires complete recorded identity.");
|
|
374
|
+
}
|
|
375
|
+
async function observeRecordedProcess(options) {
|
|
376
|
+
const currentIdentity = await options.dependencies.readProcessIdentity(options.identity.hostProcessId);
|
|
377
|
+
if (currentIdentity === null) return "absent";
|
|
378
|
+
if (currentIdentity.processStartIdentity !== options.identity.processStartIdentity) return "absent";
|
|
379
|
+
if (currentIdentity.processState.startsWith("Z")) return "absent";
|
|
380
|
+
if (options.mode === "after-signal" && currentIdentity.processState.includes("E")) return "exact";
|
|
381
|
+
if (options.mode === "after-signal" && (currentIdentity.processState.startsWith("U") || currentIdentity.processState.startsWith("R")) && (isMatchingDarwinFallbackCommand({
|
|
382
|
+
currentCommand: currentIdentity.command,
|
|
383
|
+
recordedCommand: options.identity.command
|
|
384
|
+
}) || isMatchingLinuxTaskFallbackCommand({
|
|
385
|
+
currentCommand: currentIdentity.command,
|
|
386
|
+
recordedCommand: options.identity.command
|
|
387
|
+
}))) return "exact";
|
|
388
|
+
if (currentIdentity.command !== options.identity.command) throw new Error(`${options.contextLabel} refusing ${options.action} pid ${String(options.identity.hostProcessId)}: same process start identity was observed with state ${JSON.stringify(currentIdentity.processState)} but command changed (recorded ${JSON.stringify(options.identity.command)}, current ${JSON.stringify(currentIdentity.command)}).`);
|
|
389
|
+
if (!isManagedVmHostCommand(currentIdentity.command)) throw new Error(`${options.contextLabel} refusing ${options.action} pid ${String(options.identity.hostProcessId)} because the exact command is not a managed VM process.`);
|
|
390
|
+
return "exact";
|
|
391
|
+
}
|
|
392
|
+
async function waitForRecordedProcessAbsence(options) {
|
|
393
|
+
const deadline = options.dependencies.now() + terminationStageTimeoutMs;
|
|
394
|
+
while (options.dependencies.now() < deadline) {
|
|
395
|
+
if (await observeRecordedProcess({
|
|
396
|
+
action: `continued containment after ${options.afterSignal} for`,
|
|
397
|
+
contextLabel: options.contextLabel,
|
|
398
|
+
dependencies: options.dependencies,
|
|
399
|
+
identity: options.identity,
|
|
400
|
+
mode: "after-signal"
|
|
401
|
+
}) === "absent") return true;
|
|
402
|
+
await options.dependencies.sleep(processIdentityPollIntervalMs);
|
|
403
|
+
}
|
|
404
|
+
return await observeRecordedProcess({
|
|
405
|
+
action: `continued containment after ${options.afterSignal} for`,
|
|
406
|
+
contextLabel: options.contextLabel,
|
|
407
|
+
dependencies: options.dependencies,
|
|
408
|
+
identity: options.identity,
|
|
409
|
+
mode: "after-signal"
|
|
410
|
+
}) === "absent";
|
|
411
|
+
}
|
|
412
|
+
function signalRecordedProcess(options) {
|
|
413
|
+
try {
|
|
414
|
+
options.dependencies.sendSignal(options.hostProcessId, options.signal);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (processErrorCode(error) !== "ESRCH") throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function terminateExactRecordedManagedVmHostProcess(options) {
|
|
420
|
+
assertTerminationRequest(options);
|
|
421
|
+
if (await observeRecordedProcess({
|
|
422
|
+
action: "SIGTERM to",
|
|
423
|
+
contextLabel: options.contextLabel,
|
|
424
|
+
dependencies: options.dependencies,
|
|
425
|
+
identity: options.identity,
|
|
426
|
+
mode: "before-signal"
|
|
427
|
+
}) === "absent") return {
|
|
428
|
+
hostProcessId: options.identity.hostProcessId,
|
|
429
|
+
kind: "already-absent"
|
|
430
|
+
};
|
|
431
|
+
signalRecordedProcess({
|
|
432
|
+
dependencies: options.dependencies,
|
|
433
|
+
hostProcessId: options.identity.hostProcessId,
|
|
434
|
+
signal: "SIGTERM"
|
|
435
|
+
});
|
|
436
|
+
if (await waitForRecordedProcessAbsence({
|
|
437
|
+
afterSignal: "SIGTERM",
|
|
438
|
+
contextLabel: options.contextLabel,
|
|
439
|
+
dependencies: options.dependencies,
|
|
440
|
+
identity: options.identity
|
|
441
|
+
})) return {
|
|
442
|
+
hostProcessId: options.identity.hostProcessId,
|
|
443
|
+
kind: "terminated"
|
|
444
|
+
};
|
|
445
|
+
if (await observeRecordedProcess({
|
|
446
|
+
action: "SIGKILL to",
|
|
447
|
+
contextLabel: options.contextLabel,
|
|
448
|
+
dependencies: options.dependencies,
|
|
449
|
+
identity: options.identity,
|
|
450
|
+
mode: "after-signal"
|
|
451
|
+
}) === "absent") return {
|
|
452
|
+
hostProcessId: options.identity.hostProcessId,
|
|
453
|
+
kind: "terminated"
|
|
454
|
+
};
|
|
455
|
+
signalRecordedProcess({
|
|
456
|
+
dependencies: options.dependencies,
|
|
457
|
+
hostProcessId: options.identity.hostProcessId,
|
|
458
|
+
signal: "SIGKILL"
|
|
459
|
+
});
|
|
460
|
+
if (await waitForRecordedProcessAbsence({
|
|
461
|
+
afterSignal: "SIGKILL",
|
|
462
|
+
contextLabel: options.contextLabel,
|
|
463
|
+
dependencies: options.dependencies,
|
|
464
|
+
identity: options.identity
|
|
465
|
+
})) return {
|
|
466
|
+
hostProcessId: options.identity.hostProcessId,
|
|
467
|
+
kind: "terminated"
|
|
468
|
+
};
|
|
469
|
+
throw new Error(`Failed to terminate exact recorded managed VM process ${String(options.identity.hostProcessId)} (${options.contextLabel}).`);
|
|
470
|
+
}
|
|
471
|
+
function createGondolinExactProcessTerminationCapability(dependencies = defaultDependencies) {
|
|
472
|
+
return { async terminateRecordedHostProcess(request) {
|
|
473
|
+
return await terminateExactRecordedManagedVmHostProcess({
|
|
474
|
+
contextLabel: request.contextLabel,
|
|
475
|
+
dependencies,
|
|
476
|
+
identity: request.identity
|
|
477
|
+
});
|
|
478
|
+
} };
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/gondolin-package.ts
|
|
482
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
483
|
+
const gondolinPackageJsonSchema = z.object({ version: z.string().min(1) });
|
|
484
|
+
function isMissingFileError(error) {
|
|
485
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
486
|
+
}
|
|
487
|
+
function getErrorMessage(error) {
|
|
488
|
+
return error instanceof Error ? error.message : String(error);
|
|
489
|
+
}
|
|
490
|
+
function parseMinimumZigVersion(rawContents) {
|
|
491
|
+
const match = rawContents.match(/\.minimum_zig_version\s*=\s*"([^"]*)"/u);
|
|
492
|
+
if (!match) throw new Error("minimum_zig_version declaration not found. Expected a line like `.minimum_zig_version = \"0.15.2\"`.");
|
|
493
|
+
const version = match[1];
|
|
494
|
+
if (!version) throw new Error("minimum_zig_version is empty.");
|
|
495
|
+
return version;
|
|
496
|
+
}
|
|
497
|
+
function resolveGondolinPackageJsonPath() {
|
|
498
|
+
return requireFromHere.resolve("@earendil-works/gondolin/package.json");
|
|
499
|
+
}
|
|
500
|
+
async function resolveGondolinPackageSpec() {
|
|
501
|
+
const packageJsonPath = resolveGondolinPackageJsonPath();
|
|
502
|
+
const parsed = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
503
|
+
return `@earendil-works/gondolin@${gondolinPackageJsonSchema.parse(parsed).version}`;
|
|
504
|
+
}
|
|
505
|
+
async function resolveDefaultBuildZigZonPath() {
|
|
506
|
+
const packageJsonPath = resolveGondolinPackageJsonPath();
|
|
507
|
+
return path.join(path.dirname(packageJsonPath), "dist", "guest", "build.zig.zon");
|
|
508
|
+
}
|
|
509
|
+
async function resolveGondolinMinimumZigVersion(options = {}) {
|
|
510
|
+
const zonPath = options.buildZigZonPath ?? await resolveDefaultBuildZigZonPath();
|
|
511
|
+
let rawContents;
|
|
512
|
+
try {
|
|
513
|
+
rawContents = await fs.readFile(zonPath, "utf8");
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (isMissingFileError(error)) throw new Error(`Missing Gondolin build.zig.zon at '${zonPath}'.`, { cause: error });
|
|
516
|
+
throw new Error(`Failed to read Gondolin build.zig.zon at '${zonPath}': ${getErrorMessage(error)}`, { cause: error });
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
return parseMinimumZigVersion(rawContents);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
throw new Error(`Failed to parse Gondolin build.zig.zon at '${zonPath}': ${getErrorMessage(error)}`, { cause: error });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/pinned-realfs.ts
|
|
526
|
+
const terminallyClosedPinnedRoots = /* @__PURE__ */ new WeakSet();
|
|
527
|
+
function formatRootIdentity(root) {
|
|
528
|
+
return `${root.device}:${root.inode}`;
|
|
529
|
+
}
|
|
530
|
+
function openDirectoryNoFollow(candidatePath) {
|
|
531
|
+
return fs$1.openSync(candidatePath, fs$1.constants.O_RDONLY | fs$1.constants.O_DIRECTORY | fs$1.constants.O_NOFOLLOW);
|
|
532
|
+
}
|
|
533
|
+
function pinRealFsRoot(hostPath) {
|
|
534
|
+
if (!hostPath || !path.isAbsolute(hostPath)) throw new Error(`Pinned RealFS root must be a non-empty absolute path: ${hostPath}`);
|
|
535
|
+
const resolvedHostPath = path.resolve(hostPath);
|
|
536
|
+
const fd = openDirectoryNoFollow(resolvedHostPath);
|
|
537
|
+
try {
|
|
538
|
+
const stats = fs$1.fstatSync(fd);
|
|
539
|
+
if (!stats.isDirectory()) throw new Error(`Pinned RealFS root is not a directory: ${resolvedHostPath}`);
|
|
540
|
+
const realPath = fs$1.realpathSync(resolvedHostPath);
|
|
541
|
+
const realPathStats = fs$1.statSync(realPath);
|
|
542
|
+
if (realPathStats.dev !== stats.dev || realPathStats.ino !== stats.ino) throw new Error(`Pinned RealFS root changed while opening: ${resolvedHostPath} opened ${stats.dev}:${stats.ino} but resolved to ${realPathStats.dev}:${realPathStats.ino}`);
|
|
543
|
+
return {
|
|
544
|
+
device: stats.dev,
|
|
545
|
+
fd,
|
|
546
|
+
hostPath: resolvedHostPath,
|
|
547
|
+
inode: stats.ino,
|
|
548
|
+
realPath
|
|
549
|
+
};
|
|
550
|
+
} catch (error) {
|
|
551
|
+
fs$1.closeSync(fd);
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function closePinnedRealFsRoot(root) {
|
|
556
|
+
if (terminallyClosedPinnedRoots.has(root)) return;
|
|
557
|
+
fs$1.closeSync(root.fd);
|
|
558
|
+
terminallyClosedPinnedRoots.add(root);
|
|
559
|
+
}
|
|
560
|
+
function assertPinnedRealFsRoot(root) {
|
|
561
|
+
const pinnedStats = fs$1.fstatSync(root.fd);
|
|
562
|
+
const currentStats = fs$1.statSync(root.realPath);
|
|
563
|
+
if (pinnedStats.dev !== root.device || pinnedStats.ino !== root.inode || currentStats.dev !== root.device || currentStats.ino !== root.inode) throw new Error(`Pinned RealFS root changed before mount access: ${root.realPath} expected ${formatRootIdentity(root)} got ${currentStats.dev}:${currentStats.ino}`);
|
|
564
|
+
}
|
|
565
|
+
function createPinnedRealFsProvider(options) {
|
|
566
|
+
assertPinnedRealFsRoot(options.root);
|
|
567
|
+
const provider = options.createRealFsProvider(options.root.realPath);
|
|
568
|
+
return new Proxy(provider, { get(target, property, receiver) {
|
|
569
|
+
const value = Reflect.get(target, property, receiver);
|
|
570
|
+
if (typeof value !== "function") return value;
|
|
571
|
+
return (...methodArguments) => {
|
|
572
|
+
assertPinnedRealFsRoot(options.root);
|
|
573
|
+
return Reflect.apply(value, target, methodArguments);
|
|
574
|
+
};
|
|
575
|
+
} });
|
|
576
|
+
}
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/filtered-workspace-provider.ts
|
|
579
|
+
function toAbsoluteProviderPath(relativePath) {
|
|
580
|
+
return `/${relativePath}`;
|
|
581
|
+
}
|
|
582
|
+
function normalizeProviderPath(providerPath) {
|
|
583
|
+
const absolutePath = providerPath.startsWith("/") ? providerPath : `/${providerPath}`;
|
|
584
|
+
return path.posix.normalize(absolutePath);
|
|
585
|
+
}
|
|
586
|
+
function toRelativeProviderPath(providerPath) {
|
|
587
|
+
const normalizedPath = normalizeProviderPath(providerPath);
|
|
588
|
+
return normalizedPath === "/" ? "" : normalizedPath.slice(1);
|
|
589
|
+
}
|
|
590
|
+
function isEqualOrDescendant(candidatePath, ancestorPath) {
|
|
591
|
+
return candidatePath === ancestorPath || candidatePath.startsWith(`${ancestorPath}/`);
|
|
592
|
+
}
|
|
593
|
+
function pathsOverlap(firstPath, secondPath) {
|
|
594
|
+
return isEqualOrDescendant(firstPath, secondPath) || isEqualOrDescendant(secondPath, firstPath);
|
|
595
|
+
}
|
|
596
|
+
function createFilesystemPolicyError(code, syscall, providerPath) {
|
|
597
|
+
return Object.assign(/* @__PURE__ */ new Error(`${code}: ${syscall} '${providerPath}'`), {
|
|
598
|
+
code,
|
|
599
|
+
errno: ERRNO[code],
|
|
600
|
+
path: providerPath,
|
|
601
|
+
syscall
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function invokeProviderMethod(provider, property, methodArguments) {
|
|
605
|
+
const method = Reflect.get(provider, property, provider);
|
|
606
|
+
if (typeof method !== "function") return method;
|
|
607
|
+
return Reflect.apply(method, provider, methodArguments);
|
|
608
|
+
}
|
|
609
|
+
const singlePathMutationMethods = new Set([
|
|
610
|
+
"appendFile",
|
|
611
|
+
"appendFileSync",
|
|
612
|
+
"mkdir",
|
|
613
|
+
"mkdirSync",
|
|
614
|
+
"rmdir",
|
|
615
|
+
"rmdirSync",
|
|
616
|
+
"symlink",
|
|
617
|
+
"symlinkSync",
|
|
618
|
+
"truncate",
|
|
619
|
+
"truncateSync",
|
|
620
|
+
"unlink",
|
|
621
|
+
"unlinkSync",
|
|
622
|
+
"writeFile",
|
|
623
|
+
"writeFileSync"
|
|
624
|
+
]);
|
|
625
|
+
function mutationPathArgumentIndex(property) {
|
|
626
|
+
return property === "symlink" || property === "symlinkSync" ? 1 : 0;
|
|
627
|
+
}
|
|
628
|
+
function failProviderMutation(property, providerPath) {
|
|
629
|
+
return failProviderOperation(property, providerPath, "EROFS");
|
|
630
|
+
}
|
|
631
|
+
function failProviderOperation(property, providerPath, code) {
|
|
632
|
+
const operationError = createFilesystemPolicyError(code, property, providerPath);
|
|
633
|
+
if (property.endsWith("Sync")) throw operationError;
|
|
634
|
+
return Promise.reject(operationError);
|
|
635
|
+
}
|
|
636
|
+
function createRelativeRootProvider(provider, sourceRelativePath) {
|
|
637
|
+
const sourceProviderPath = toAbsoluteProviderPath(sourceRelativePath);
|
|
638
|
+
const translatePath = (providerPath) => {
|
|
639
|
+
const suffix = toRelativeProviderPath(providerPath);
|
|
640
|
+
return toAbsoluteProviderPath(suffix.length === 0 ? sourceRelativePath : `${sourceRelativePath}/${suffix}`);
|
|
641
|
+
};
|
|
642
|
+
const translateArguments = (property, methodArguments) => {
|
|
643
|
+
const translatedArguments = [...methodArguments];
|
|
644
|
+
if (property === "rename" || property === "renameSync" || property === "link" || property === "linkSync" || property === "copyFile" || property === "copyFileSync") {
|
|
645
|
+
translatedArguments[0] = translatePath(String(methodArguments[0]));
|
|
646
|
+
translatedArguments[1] = translatePath(String(methodArguments[1]));
|
|
647
|
+
return translatedArguments;
|
|
648
|
+
}
|
|
649
|
+
translatedArguments[mutationPathArgumentIndex(property)] = translatePath(String(methodArguments[mutationPathArgumentIndex(property)]));
|
|
650
|
+
return translatedArguments;
|
|
651
|
+
};
|
|
652
|
+
const assertResolvedPathWithinSource = (resolvedPath, canonicalSourcePath, property, providerPath) => {
|
|
653
|
+
if (!isEqualOrDescendant(toRelativeProviderPath(resolvedPath), toRelativeProviderPath(canonicalSourcePath))) throw createFilesystemPolicyError("ENOENT", property, providerPath);
|
|
654
|
+
};
|
|
655
|
+
const resolveConfinedPath = async (property, providerPath, translatedPath) => {
|
|
656
|
+
if (!provider.realpath) throw createFilesystemPolicyError("ENOENT", property, providerPath);
|
|
657
|
+
const [canonicalSourcePath, resolvedPath] = await Promise.all([provider.realpath(sourceProviderPath), provider.realpath(translatedPath)]);
|
|
658
|
+
assertResolvedPathWithinSource(resolvedPath, canonicalSourcePath, property, providerPath);
|
|
659
|
+
return {
|
|
660
|
+
canonicalSourcePath,
|
|
661
|
+
resolvedPath
|
|
662
|
+
};
|
|
663
|
+
};
|
|
664
|
+
const resolveConfinedPathSync = (property, providerPath, translatedPath) => {
|
|
665
|
+
if (!provider.realpathSync) throw createFilesystemPolicyError("ENOENT", property, providerPath);
|
|
666
|
+
const canonicalSourcePath = provider.realpathSync(sourceProviderPath);
|
|
667
|
+
const resolvedPath = provider.realpathSync(translatedPath);
|
|
668
|
+
assertResolvedPathWithinSource(resolvedPath, canonicalSourcePath, property, providerPath);
|
|
669
|
+
return {
|
|
670
|
+
canonicalSourcePath,
|
|
671
|
+
resolvedPath
|
|
672
|
+
};
|
|
673
|
+
};
|
|
674
|
+
const parentProviderPath = (providerPath) => {
|
|
675
|
+
const normalizedPath = normalizeProviderPath(providerPath);
|
|
676
|
+
return normalizedPath === "/" ? "/" : path.posix.dirname(normalizedPath);
|
|
677
|
+
};
|
|
678
|
+
const remapResolvedPath = (resolvedPath, canonicalSourcePath) => {
|
|
679
|
+
const resolvedRelativePath = toRelativeProviderPath(resolvedPath);
|
|
680
|
+
const canonicalSourceRelativePath = toRelativeProviderPath(canonicalSourcePath);
|
|
681
|
+
if (resolvedRelativePath === canonicalSourceRelativePath) return "/";
|
|
682
|
+
return toAbsoluteProviderPath(canonicalSourceRelativePath.length === 0 ? resolvedRelativePath : resolvedRelativePath.slice(`${canonicalSourceRelativePath}/`.length));
|
|
683
|
+
};
|
|
684
|
+
const asyncFollowMethods = new Set([
|
|
685
|
+
"access",
|
|
686
|
+
"open",
|
|
687
|
+
"readdir",
|
|
688
|
+
"readFile",
|
|
689
|
+
"stat",
|
|
690
|
+
"statfs"
|
|
691
|
+
]);
|
|
692
|
+
const syncFollowMethods = new Set([
|
|
693
|
+
"accessSync",
|
|
694
|
+
"openSync",
|
|
695
|
+
"readdirSync",
|
|
696
|
+
"readFileSync",
|
|
697
|
+
"statSync",
|
|
698
|
+
"watch",
|
|
699
|
+
"watchAsync",
|
|
700
|
+
"watchFile",
|
|
701
|
+
"unwatchFile"
|
|
702
|
+
]);
|
|
703
|
+
return new Proxy(provider, { get(target, property) {
|
|
704
|
+
const value = Reflect.get(target, property, target);
|
|
705
|
+
if (typeof property !== "string" || typeof value !== "function") return value;
|
|
706
|
+
return (...methodArguments) => {
|
|
707
|
+
const translatedArguments = translateArguments(property, methodArguments);
|
|
708
|
+
if (property === "realpath") return resolveConfinedPath(property, String(methodArguments[0]), String(translatedArguments[0])).then(({ canonicalSourcePath, resolvedPath }) => remapResolvedPath(resolvedPath, canonicalSourcePath));
|
|
709
|
+
if (property === "realpathSync") {
|
|
710
|
+
const { canonicalSourcePath, resolvedPath } = resolveConfinedPathSync(property, String(methodArguments[0]), String(translatedArguments[0]));
|
|
711
|
+
return remapResolvedPath(resolvedPath, canonicalSourcePath);
|
|
712
|
+
}
|
|
713
|
+
if (property === "lstat" || property === "readlink") {
|
|
714
|
+
const providerPath = String(methodArguments[0]);
|
|
715
|
+
return resolveConfinedPath(property, providerPath, translatePath(parentProviderPath(providerPath))).then(() => Reflect.apply(value, target, translatedArguments));
|
|
716
|
+
}
|
|
717
|
+
if (property === "lstatSync" || property === "readlinkSync") {
|
|
718
|
+
const providerPath = String(methodArguments[0]);
|
|
719
|
+
resolveConfinedPathSync(property, providerPath, translatePath(parentProviderPath(providerPath)));
|
|
720
|
+
return Reflect.apply(value, target, translatedArguments);
|
|
721
|
+
}
|
|
722
|
+
if (property === "exists") return resolveConfinedPath(property, String(methodArguments[0]), String(translatedArguments[0])).then(() => Reflect.apply(value, target, translatedArguments)).catch(() => false);
|
|
723
|
+
if (property === "existsSync" || property === "internalModuleStat") {
|
|
724
|
+
const providerPath = String(methodArguments[0]);
|
|
725
|
+
try {
|
|
726
|
+
resolveConfinedPathSync(property, providerPath, String(translatedArguments[0]));
|
|
727
|
+
return Reflect.apply(value, target, translatedArguments);
|
|
728
|
+
} catch {
|
|
729
|
+
return property === "existsSync" ? false : -2;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (asyncFollowMethods.has(property)) return resolveConfinedPath(property, String(methodArguments[0]), String(translatedArguments[0])).then(() => Reflect.apply(value, target, translatedArguments));
|
|
733
|
+
if (syncFollowMethods.has(property)) resolveConfinedPathSync(property, String(methodArguments[0]), String(translatedArguments[0]));
|
|
734
|
+
return Reflect.apply(value, target, translatedArguments);
|
|
735
|
+
};
|
|
736
|
+
} });
|
|
737
|
+
}
|
|
738
|
+
function createPositiveWorkspaceProjectionProvider(provider, policy, dependencies) {
|
|
739
|
+
const isVisible = (providerPath) => {
|
|
740
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
741
|
+
return relativePath.length === 0 || policy.visiblePaths.some((visiblePath) => pathsOverlap(relativePath, visiblePath));
|
|
742
|
+
};
|
|
743
|
+
const isWritable = (providerPath) => {
|
|
744
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
745
|
+
return policy.writablePaths.some((writablePath) => isEqualOrDescendant(relativePath, writablePath));
|
|
746
|
+
};
|
|
747
|
+
const projectedProvider = dependencies.createShadowProvider(provider, {
|
|
748
|
+
shouldShadow: ({ path: providerPath }) => !isVisible(providerPath),
|
|
749
|
+
writeMode: "deny"
|
|
750
|
+
});
|
|
751
|
+
const readonlyProvider = dependencies.createReadonlyProvider(projectedProvider);
|
|
752
|
+
return new Proxy(projectedProvider, { get(target, property) {
|
|
753
|
+
const value = Reflect.get(target, property, target);
|
|
754
|
+
if (typeof property !== "string" || typeof value !== "function") return value;
|
|
755
|
+
if (property === "open" || property === "openSync") return (...methodArguments) => {
|
|
756
|
+
const providerPath = String(methodArguments[0]);
|
|
757
|
+
return invokeProviderMethod(isVisible(providerPath) && !isWritable(providerPath) ? readonlyProvider : target, property, methodArguments);
|
|
758
|
+
};
|
|
759
|
+
if (singlePathMutationMethods.has(property)) return (...methodArguments) => {
|
|
760
|
+
const pathIndex = mutationPathArgumentIndex(property);
|
|
761
|
+
const providerPath = String(methodArguments[pathIndex]);
|
|
762
|
+
return invokeProviderMethod(isVisible(providerPath) && !isWritable(providerPath) ? readonlyProvider : target, property, methodArguments);
|
|
763
|
+
};
|
|
764
|
+
if (property === "rename" || property === "renameSync") return (...methodArguments) => {
|
|
765
|
+
const affectedPaths = [String(methodArguments[0]), String(methodArguments[1])];
|
|
766
|
+
return invokeProviderMethod(affectedPaths.every(isVisible) && affectedPaths.some((affectedPath) => !isWritable(affectedPath)) ? readonlyProvider : target, property, methodArguments);
|
|
767
|
+
};
|
|
768
|
+
if (property === "copyFile" || property === "copyFileSync") return (...methodArguments) => {
|
|
769
|
+
const sourcePath = String(methodArguments[0]);
|
|
770
|
+
const destinationPath = String(methodArguments[1]);
|
|
771
|
+
return invokeProviderMethod(isVisible(sourcePath) && isVisible(destinationPath) && !isWritable(destinationPath) ? readonlyProvider : target, property, methodArguments);
|
|
772
|
+
};
|
|
773
|
+
return (...methodArguments) => Reflect.apply(value, target, methodArguments);
|
|
774
|
+
} });
|
|
775
|
+
}
|
|
776
|
+
function createWorkspaceHardlinkProvider(provider, baseProvider, policy) {
|
|
777
|
+
const hidden = (providerPath) => {
|
|
778
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
779
|
+
return policy.hiddenPaths.some((hiddenPath) => isEqualOrDescendant(relativePath, hiddenPath));
|
|
780
|
+
};
|
|
781
|
+
const temporary = (providerPath) => {
|
|
782
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
783
|
+
return policy.temporaryPaths.some((temporaryPath) => isEqualOrDescendant(relativePath, temporaryPath));
|
|
784
|
+
};
|
|
785
|
+
const visible = (providerPath) => {
|
|
786
|
+
if (policy.visibility.kind === "whole-root-writable") return true;
|
|
787
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
788
|
+
return relativePath.length === 0 || policy.visibility.visiblePaths.some((visiblePath) => pathsOverlap(relativePath, visiblePath));
|
|
789
|
+
};
|
|
790
|
+
const writable = (providerPath) => {
|
|
791
|
+
if (temporary(providerPath)) return false;
|
|
792
|
+
if (policy.visibility.kind === "whole-root-writable") return true;
|
|
793
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
794
|
+
return policy.visibility.writablePaths.some((writablePath) => isEqualOrDescendant(relativePath, writablePath));
|
|
795
|
+
};
|
|
796
|
+
return new Proxy(provider, { get(target, property) {
|
|
797
|
+
const value = Reflect.get(target, property, target);
|
|
798
|
+
if (property !== "link" && property !== "linkSync") return typeof value === "function" ? (...methodArguments) => Reflect.apply(value, target, methodArguments) : value;
|
|
799
|
+
const underlyingLinkMethod = Reflect.get(baseProvider, property, baseProvider);
|
|
800
|
+
if (typeof underlyingLinkMethod !== "function") return;
|
|
801
|
+
return (...methodArguments) => {
|
|
802
|
+
const sourcePath = String(methodArguments[0]);
|
|
803
|
+
const destinationPath = String(methodArguments[1]);
|
|
804
|
+
if (hidden(sourcePath) || !visible(sourcePath)) return failProviderOperation(property, sourcePath, "ENOENT");
|
|
805
|
+
if (hidden(destinationPath) || !visible(destinationPath)) return failProviderOperation(property, destinationPath, "EACCES");
|
|
806
|
+
if (!writable(sourcePath)) return failProviderOperation(property, sourcePath, "EROFS");
|
|
807
|
+
if (!writable(destinationPath)) return failProviderOperation(property, destinationPath, "EROFS");
|
|
808
|
+
return Reflect.apply(underlyingLinkMethod, baseProvider, methodArguments);
|
|
809
|
+
};
|
|
810
|
+
} });
|
|
811
|
+
}
|
|
812
|
+
function directReadonlyDestinationChildren(providerPath, readonlyInputs) {
|
|
813
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
814
|
+
const prefix = relativePath.length === 0 ? "" : `${relativePath}/`;
|
|
815
|
+
const children = /* @__PURE__ */ new Set();
|
|
816
|
+
for (const readonlyInput of readonlyInputs) {
|
|
817
|
+
if (!readonlyInput.destinationRelativePath.startsWith(prefix)) continue;
|
|
818
|
+
const childName = readonlyInput.destinationRelativePath.slice(prefix.length).split("/")[0];
|
|
819
|
+
if (childName) children.add(childName);
|
|
820
|
+
}
|
|
821
|
+
return [...children].toSorted();
|
|
822
|
+
}
|
|
823
|
+
function entryName(entry) {
|
|
824
|
+
return typeof entry === "string" ? entry : entry.name;
|
|
825
|
+
}
|
|
826
|
+
function mergeDirectoryEntries(entries, virtualChildren, withFileTypes) {
|
|
827
|
+
const presentNames = new Set(entries.map(entryName));
|
|
828
|
+
const missingChildren = virtualChildren.filter((childName) => !presentNames.has(childName));
|
|
829
|
+
return [...entries, ...missingChildren.map((childName) => withFileTypes ? new VirtualDirent(childName) : childName)];
|
|
830
|
+
}
|
|
831
|
+
function isNoEntryError(error) {
|
|
832
|
+
return typeof error === "object" && error !== null && ("code" in error && (error.code === "ENOENT" || error.code === "ERRNO_2") || "errno" in error && (error.errno === ERRNO.ENOENT || error.errno === -ERRNO.ENOENT));
|
|
833
|
+
}
|
|
834
|
+
function createNestedReadonlyWorkspaceProvider(provider, sourceProvider, policy, dependencies) {
|
|
835
|
+
const readonlyInputs = [];
|
|
836
|
+
for (const readonlyInput of policy.readonlyInputs) {
|
|
837
|
+
if (policy.hiddenPaths.some((hiddenPath) => isEqualOrDescendant(readonlyInput.destinationRelativePath, hiddenPath))) continue;
|
|
838
|
+
let inputProvider = createRelativeRootProvider(sourceProvider, readonlyInput.sourceRelativePath);
|
|
839
|
+
const nestedHiddenPaths = policy.hiddenPaths.flatMap((hiddenPath) => {
|
|
840
|
+
const destinationPrefix = `${readonlyInput.destinationRelativePath}/`;
|
|
841
|
+
return hiddenPath.startsWith(destinationPrefix) ? [toAbsoluteProviderPath(hiddenPath.slice(destinationPrefix.length))] : [];
|
|
842
|
+
});
|
|
843
|
+
if (nestedHiddenPaths.length > 0) inputProvider = dependencies.createShadowProvider(inputProvider, {
|
|
844
|
+
shouldShadow: dependencies.createShadowPathPredicate(nestedHiddenPaths),
|
|
845
|
+
writeMode: "deny"
|
|
846
|
+
});
|
|
847
|
+
readonlyInputs.push({
|
|
848
|
+
destinationRelativePath: readonlyInput.destinationRelativePath,
|
|
849
|
+
provider: dependencies.createReadonlyProvider(inputProvider)
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (readonlyInputs.length === 0) return provider;
|
|
853
|
+
const findInput = (providerPath) => {
|
|
854
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
855
|
+
return readonlyInputs.find((readonlyInput) => isEqualOrDescendant(relativePath, readonlyInput.destinationRelativePath));
|
|
856
|
+
};
|
|
857
|
+
const mapInputPath = (providerPath, readonlyInput) => {
|
|
858
|
+
const suffix = toRelativeProviderPath(providerPath).slice(readonlyInput.destinationRelativePath.length);
|
|
859
|
+
return suffix.length === 0 ? "/" : suffix;
|
|
860
|
+
};
|
|
861
|
+
const conflictsWithReadonlyInput = (providerPath) => {
|
|
862
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
863
|
+
return readonlyInputs.some((readonlyInput) => pathsOverlap(relativePath, readonlyInput.destinationRelativePath));
|
|
864
|
+
};
|
|
865
|
+
const isHidden = (providerPath) => {
|
|
866
|
+
const relativePath = toRelativeProviderPath(providerPath);
|
|
867
|
+
return policy.hiddenPaths.some((hiddenPath) => isEqualOrDescendant(relativePath, hiddenPath));
|
|
868
|
+
};
|
|
869
|
+
return new Proxy(provider, { get(target, property) {
|
|
870
|
+
const value = Reflect.get(target, property, target);
|
|
871
|
+
if (typeof property !== "string" || typeof value !== "function") return value;
|
|
872
|
+
if (property === "readdir" || property === "readdirSync") return (...methodArguments) => {
|
|
873
|
+
const providerPath = String(methodArguments[0]);
|
|
874
|
+
const readdirOptions = methodArguments[1];
|
|
875
|
+
const withFileTypes = typeof readdirOptions === "object" && readdirOptions !== null && "withFileTypes" in readdirOptions && readdirOptions.withFileTypes === true;
|
|
876
|
+
const readonlyInput = findInput(providerPath);
|
|
877
|
+
if (readonlyInput && !isHidden(providerPath)) return invokeProviderMethod(readonlyInput.provider, property, [mapInputPath(providerPath, readonlyInput), ...methodArguments.slice(1)]);
|
|
878
|
+
const virtualChildren = directReadonlyDestinationChildren(providerPath, readonlyInputs);
|
|
879
|
+
if (virtualChildren.length === 0) return Reflect.apply(value, target, methodArguments);
|
|
880
|
+
if (property === "readdir") return Promise.resolve(Reflect.apply(value, target, methodArguments)).catch((error) => {
|
|
881
|
+
if (isNoEntryError(error)) return [];
|
|
882
|
+
throw error;
|
|
883
|
+
}).then((entries) => mergeDirectoryEntries(entries, virtualChildren, withFileTypes));
|
|
884
|
+
try {
|
|
885
|
+
return mergeDirectoryEntries(Reflect.apply(value, target, methodArguments), virtualChildren, withFileTypes);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (isNoEntryError(error)) return virtualChildren;
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
if (property === "stat" || property === "statSync" || property === "lstat" || property === "lstatSync") return (...methodArguments) => {
|
|
892
|
+
const providerPath = String(methodArguments[0]);
|
|
893
|
+
const readonlyInput = findInput(providerPath);
|
|
894
|
+
if (readonlyInput && !isHidden(providerPath)) return invokeProviderMethod(readonlyInput.provider, property, [mapInputPath(providerPath, readonlyInput), ...methodArguments.slice(1)]);
|
|
895
|
+
if (directReadonlyDestinationChildren(providerPath, readonlyInputs).length === 0) return Reflect.apply(value, target, methodArguments);
|
|
896
|
+
if (property === "stat" || property === "lstat") return Promise.resolve(Reflect.apply(value, target, methodArguments)).catch((error) => {
|
|
897
|
+
if (isNoEntryError(error)) return createVirtualDirStats();
|
|
898
|
+
throw error;
|
|
899
|
+
});
|
|
900
|
+
try {
|
|
901
|
+
return Reflect.apply(value, target, methodArguments);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (isNoEntryError(error)) return createVirtualDirStats();
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
if (property === "open" || property === "openSync") return (...methodArguments) => {
|
|
908
|
+
const providerPath = String(methodArguments[0]);
|
|
909
|
+
const readonlyInput = findInput(providerPath);
|
|
910
|
+
return readonlyInput && !isHidden(providerPath) ? invokeProviderMethod(readonlyInput.provider, property, [mapInputPath(providerPath, readonlyInput), ...methodArguments.slice(1)]) : Reflect.apply(value, target, methodArguments);
|
|
911
|
+
};
|
|
912
|
+
if (singlePathMutationMethods.has(property)) return (...methodArguments) => {
|
|
913
|
+
const providerPath = String(methodArguments[mutationPathArgumentIndex(property)]);
|
|
914
|
+
if (!isHidden(providerPath) && conflictsWithReadonlyInput(providerPath)) return failProviderMutation(property, providerPath);
|
|
915
|
+
return Reflect.apply(value, target, methodArguments);
|
|
916
|
+
};
|
|
917
|
+
if (property === "rename" || property === "renameSync" || property === "link" || property === "linkSync") return (...methodArguments) => {
|
|
918
|
+
const conflictingPath = [String(methodArguments[0]), String(methodArguments[1])].find((affectedPath) => !isHidden(affectedPath) && conflictsWithReadonlyInput(affectedPath));
|
|
919
|
+
if (conflictingPath) return failProviderMutation(property, conflictingPath);
|
|
920
|
+
return Reflect.apply(value, target, methodArguments);
|
|
921
|
+
};
|
|
922
|
+
if (property === "copyFile" || property === "copyFileSync") return (...methodArguments) => {
|
|
923
|
+
const sourcePath = String(methodArguments[0]);
|
|
924
|
+
const destinationPath = String(methodArguments[1]);
|
|
925
|
+
if (!isHidden(destinationPath) && conflictsWithReadonlyInput(destinationPath)) return failProviderMutation(property, destinationPath);
|
|
926
|
+
const sourceInput = findInput(sourcePath);
|
|
927
|
+
if (!sourceInput || isHidden(sourcePath)) return Reflect.apply(value, target, methodArguments);
|
|
928
|
+
const mappedSourcePath = mapInputPath(sourcePath, sourceInput);
|
|
929
|
+
if (property === "copyFile") return Promise.resolve(invokeProviderMethod(sourceInput.provider, "readFile", [mappedSourcePath])).then((contents) => invokeProviderMethod(target, "writeFile", [destinationPath, contents]));
|
|
930
|
+
return invokeProviderMethod(target, "writeFileSync", [destinationPath, invokeProviderMethod(sourceInput.provider, "readFileSync", [mappedSourcePath])]);
|
|
931
|
+
};
|
|
932
|
+
return (...methodArguments) => {
|
|
933
|
+
const providerPath = String(methodArguments[0]);
|
|
934
|
+
const readonlyInput = findInput(providerPath);
|
|
935
|
+
const result = readonlyInput && !isHidden(providerPath) ? invokeProviderMethod(readonlyInput.provider, property, [mapInputPath(providerPath, readonlyInput), ...methodArguments.slice(1)]) : Reflect.apply(value, target, methodArguments);
|
|
936
|
+
if (!readonlyInput || isHidden(providerPath)) return result;
|
|
937
|
+
if (property === "realpath") return Promise.resolve(result).then((resolvedPath) => {
|
|
938
|
+
const resolvedRelativePath = toRelativeProviderPath(String(resolvedPath));
|
|
939
|
+
return toAbsoluteProviderPath(resolvedRelativePath.length === 0 ? readonlyInput.destinationRelativePath : `${readonlyInput.destinationRelativePath}/${resolvedRelativePath}`);
|
|
940
|
+
});
|
|
941
|
+
if (property === "realpathSync") {
|
|
942
|
+
const resolvedRelativePath = toRelativeProviderPath(String(result));
|
|
943
|
+
return toAbsoluteProviderPath(resolvedRelativePath.length === 0 ? readonlyInput.destinationRelativePath : `${readonlyInput.destinationRelativePath}/${resolvedRelativePath}`);
|
|
944
|
+
}
|
|
945
|
+
return result;
|
|
946
|
+
};
|
|
947
|
+
} });
|
|
948
|
+
}
|
|
949
|
+
function createFilteredWorkspaceProvider(options) {
|
|
950
|
+
let workspaceProvider = options.baseProvider;
|
|
951
|
+
if (options.policy.visibility.kind === "positive-paths") workspaceProvider = createPositiveWorkspaceProjectionProvider(workspaceProvider, options.policy.visibility, options.dependencies);
|
|
952
|
+
if (options.policy.hiddenPaths.length > 0) workspaceProvider = options.dependencies.createShadowProvider(workspaceProvider, {
|
|
953
|
+
shouldShadow: options.dependencies.createShadowPathPredicate(options.policy.hiddenPaths.map(toAbsoluteProviderPath)),
|
|
954
|
+
writeMode: "deny"
|
|
955
|
+
});
|
|
956
|
+
if (options.policy.temporaryPaths.length > 0) {
|
|
957
|
+
const hiddenPredicate = options.dependencies.createShadowPathPredicate(options.policy.hiddenPaths.map(toAbsoluteProviderPath));
|
|
958
|
+
const temporaryPredicate = options.dependencies.createShadowPathPredicate(options.policy.temporaryPaths.map(toAbsoluteProviderPath));
|
|
959
|
+
workspaceProvider = options.dependencies.createShadowProvider(workspaceProvider, {
|
|
960
|
+
shouldShadow: (context) => !hiddenPredicate(context) && temporaryPredicate(context),
|
|
961
|
+
tmpfs: options.dependencies.createMemoryProvider(),
|
|
962
|
+
writeMode: "tmpfs"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
workspaceProvider = createWorkspaceHardlinkProvider(workspaceProvider, options.baseProvider, options.policy);
|
|
966
|
+
return createNestedReadonlyWorkspaceProvider(workspaceProvider, options.baseProvider, options.policy, options.dependencies);
|
|
967
|
+
}
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region src/host-network-defaults.ts
|
|
970
|
+
/**
|
|
971
|
+
* Gondolin raw tcpHosts passthrough sockets are opened by the host-side Node
|
|
972
|
+
* process, not by guest Node processes. VM NODE_OPTIONS cannot affect those
|
|
973
|
+
* sockets, so host processes that create Gondolin VMs also force deterministic
|
|
974
|
+
* IPv4-first behavior before network state is constructed.
|
|
975
|
+
*/
|
|
976
|
+
function configureHostNetworkDefaults(dependencies = {}) {
|
|
977
|
+
const setDefaultResultOrder = "setDefaultResultOrder" in dependencies ? dependencies.setDefaultResultOrder : dns.setDefaultResultOrder;
|
|
978
|
+
const setDefaultAutoSelectFamily = "setDefaultAutoSelectFamily" in dependencies ? dependencies.setDefaultAutoSelectFamily : net$1.setDefaultAutoSelectFamily;
|
|
979
|
+
let dnsResultOrder = "unavailable";
|
|
980
|
+
if (typeof setDefaultResultOrder === "function") {
|
|
981
|
+
setDefaultResultOrder("ipv4first");
|
|
982
|
+
dnsResultOrder = "ipv4first";
|
|
983
|
+
}
|
|
984
|
+
let autoSelectFamily = "unavailable";
|
|
985
|
+
if (typeof setDefaultAutoSelectFamily === "function") {
|
|
986
|
+
setDefaultAutoSelectFamily(false);
|
|
987
|
+
autoSelectFamily = false;
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
autoSelectFamily,
|
|
991
|
+
dnsResultOrder
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/vm-adapter.ts
|
|
996
|
+
const SYNTHETIC_DNS_IPV4_BENCHMARK = "198.18.0.1";
|
|
997
|
+
const SYNTHETIC_DNS_IPV6_IPV4_MAPPED_BENCHMARK = "::ffff:198.18.0.1";
|
|
998
|
+
const MANAGED_VM_DEFAULT_INGRESS_OPTIONS = {
|
|
999
|
+
allowWebSockets: true,
|
|
1000
|
+
bufferResponseBody: false,
|
|
1001
|
+
maxBufferedResponseBodyBytes: 512 * 1024 * 1024,
|
|
1002
|
+
upstreamHeaderTimeoutMs: 12e4,
|
|
1003
|
+
upstreamResponseTimeoutMs: 12e4
|
|
1004
|
+
};
|
|
1005
|
+
function createReadonlyMutationError(syscall, filePath) {
|
|
1006
|
+
const pathSuffix = filePath === void 0 ? "" : ` '${filePath}'`;
|
|
1007
|
+
return Object.assign(/* @__PURE__ */ new Error(`EROFS: ${syscall}${pathSuffix}`), {
|
|
1008
|
+
code: "EROFS",
|
|
1009
|
+
errno: ERRNO.EROFS,
|
|
1010
|
+
...filePath === void 0 ? {} : { path: filePath },
|
|
1011
|
+
syscall
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
function createHardenedReadonlyFileHandle(handle) {
|
|
1015
|
+
return new Proxy(handle, { get(target, property, receiver) {
|
|
1016
|
+
const value = Reflect.get(target, property, receiver);
|
|
1017
|
+
if (property === "write" || property === "writeFile" || property === "truncate") return async () => {
|
|
1018
|
+
throw createReadonlyMutationError(String(property), target.path);
|
|
1019
|
+
};
|
|
1020
|
+
if (property === "writeSync" || property === "writeFileSync" || property === "truncateSync") return () => {
|
|
1021
|
+
throw createReadonlyMutationError(String(property), target.path);
|
|
1022
|
+
};
|
|
1023
|
+
return typeof value === "function" ? (...methodArguments) => Reflect.apply(value, target, methodArguments) : value;
|
|
1024
|
+
} });
|
|
1025
|
+
}
|
|
1026
|
+
function createHardenedReadonlyProvider(provider) {
|
|
1027
|
+
const readonlyProvider = new ReadonlyProvider(provider);
|
|
1028
|
+
return new Proxy(readonlyProvider, { get(target, property, receiver) {
|
|
1029
|
+
const value = Reflect.get(target, property, receiver);
|
|
1030
|
+
if (property === "open") return async (entryPath, flags, mode) => {
|
|
1031
|
+
if (isWriteFlag(flags)) throw createReadonlyMutationError("open", entryPath);
|
|
1032
|
+
return createHardenedReadonlyFileHandle(await target.open(entryPath, flags, mode));
|
|
1033
|
+
};
|
|
1034
|
+
if (property === "openSync") return (entryPath, flags, mode) => {
|
|
1035
|
+
if (isWriteFlag(flags)) throw createReadonlyMutationError("openSync", entryPath);
|
|
1036
|
+
return createHardenedReadonlyFileHandle(target.openSync(entryPath, flags, mode));
|
|
1037
|
+
};
|
|
1038
|
+
if (property === "mkdir" || property === "rmdir" || property === "unlink" || property === "rename" || property === "link" || property === "writeFile" || property === "appendFile" || property === "copyFile" || property === "symlink") return async (entryPath) => {
|
|
1039
|
+
throw createReadonlyMutationError(String(property), entryPath);
|
|
1040
|
+
};
|
|
1041
|
+
if (property === "mkdirSync" || property === "rmdirSync" || property === "unlinkSync" || property === "renameSync" || property === "linkSync" || property === "writeFileSync" || property === "appendFileSync" || property === "copyFileSync" || property === "symlinkSync") return (entryPath) => {
|
|
1042
|
+
throw createReadonlyMutationError(String(property), entryPath);
|
|
1043
|
+
};
|
|
1044
|
+
return typeof value === "function" ? (...methodArguments) => Reflect.apply(value, target, methodArguments) : value;
|
|
1045
|
+
} });
|
|
1046
|
+
}
|
|
1047
|
+
function isSshServerHostKey(value) {
|
|
1048
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1049
|
+
if (!("algorithm" in value) || !("publicKeyBase64" in value)) return false;
|
|
1050
|
+
const algorithm = value.algorithm;
|
|
1051
|
+
const publicKeyBase64 = value.publicKeyBase64;
|
|
1052
|
+
if (algorithm !== "ssh-ed25519" || typeof publicKeyBase64 !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/u.test(publicKeyBase64)) return false;
|
|
1053
|
+
try {
|
|
1054
|
+
const decodedPublicKey = Buffer.from(publicKeyBase64, "base64");
|
|
1055
|
+
if (decodedPublicKey.toString("base64") !== publicKeyBase64 || decodedPublicKey.length < 4) return false;
|
|
1056
|
+
const algorithmLength = decodedPublicKey.readUInt32BE(0);
|
|
1057
|
+
const algorithmStart = 4;
|
|
1058
|
+
const algorithmEnd = algorithmStart + algorithmLength;
|
|
1059
|
+
const publicKeyLengthOffset = algorithmEnd;
|
|
1060
|
+
const publicKeyStart = publicKeyLengthOffset + 4;
|
|
1061
|
+
return algorithmEnd + 4 <= decodedPublicKey.length && decodedPublicKey.subarray(algorithmStart, algorithmEnd).toString("utf8") === "ssh-ed25519" && decodedPublicKey.readUInt32BE(publicKeyLengthOffset) === 32 && decodedPublicKey.length === publicKeyStart + 32;
|
|
1062
|
+
} catch {
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function parseSshServerHostKey(publicKeyText) {
|
|
1067
|
+
const publicKeyLines = publicKeyText.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1068
|
+
if (publicKeyLines.length !== 1) throw new Error("Tool VM did not expose exactly one ssh-ed25519 server host key.");
|
|
1069
|
+
const publicKeyFields = publicKeyLines[0]?.split(/\s+/u);
|
|
1070
|
+
const serverHostKey = {
|
|
1071
|
+
algorithm: publicKeyFields?.[0],
|
|
1072
|
+
publicKeyBase64: publicKeyFields?.[1]
|
|
1073
|
+
};
|
|
1074
|
+
if (!isSshServerHostKey(serverHostKey)) throw new Error("Tool VM did not expose a valid ssh-ed25519 server host key.");
|
|
1075
|
+
return serverHostKey;
|
|
1076
|
+
}
|
|
1077
|
+
async function readSshServerHostKey(vmInstance) {
|
|
1078
|
+
const publicKeyResult = await vmInstance.exec(["/bin/cat", "/etc/ssh/ssh_host_ed25519_key.pub"]);
|
|
1079
|
+
if (publicKeyResult.exitCode !== 0) throw new Error(`Tool VM server host key read failed with exit ${String(publicKeyResult.exitCode)}.`);
|
|
1080
|
+
return parseSshServerHostKey(publicKeyResult.stdout);
|
|
1081
|
+
}
|
|
1082
|
+
async function closeSshAccessAfterIdentityFailure(sshAccess, identityError) {
|
|
1083
|
+
try {
|
|
1084
|
+
await sshAccess.close();
|
|
1085
|
+
} catch (closeError) {
|
|
1086
|
+
throw new AggregateError([identityError, closeError], "Tool VM SSH server identity validation and SSH access cleanup both failed.", { cause: identityError });
|
|
1087
|
+
}
|
|
1088
|
+
throw identityError;
|
|
1089
|
+
}
|
|
1090
|
+
function createDefaultDependencies() {
|
|
1091
|
+
const createDefaultRealFsProvider = (hostPath) => new RealFSProvider(hostPath);
|
|
1092
|
+
return {
|
|
1093
|
+
configureHostNetworkDefaults,
|
|
1094
|
+
createVm: async (vmOptions) => await VM.create(vmOptions),
|
|
1095
|
+
createHttpHooks: (hookOptions) => createHttpHooks({
|
|
1096
|
+
allowedHosts: [...hookOptions.allowedHosts],
|
|
1097
|
+
...hookOptions.allowedInternalHosts === void 0 ? {} : { allowedInternalHosts: [...hookOptions.allowedInternalHosts] },
|
|
1098
|
+
...hookOptions.isIpAllowed ? { isIpAllowed: hookOptions.isIpAllowed } : {},
|
|
1099
|
+
secrets: Object.fromEntries(Object.entries(hookOptions.secrets).map(([secretName, secretSpec]) => [secretName, {
|
|
1100
|
+
hosts: [...secretSpec.hosts],
|
|
1101
|
+
...secretSpec.placeholder === void 0 ? {} : { placeholder: secretSpec.placeholder },
|
|
1102
|
+
value: secretSpec.value
|
|
1103
|
+
}])),
|
|
1104
|
+
...hookOptions.onRequest ? { onRequest: hookOptions.onRequest } : {},
|
|
1105
|
+
...hookOptions.onResponse ? { onResponse: hookOptions.onResponse } : {}
|
|
1106
|
+
}),
|
|
1107
|
+
closePinnedRealFsRoot,
|
|
1108
|
+
createPinnedRealFsProvider: (root) => createPinnedRealFsProvider({
|
|
1109
|
+
createRealFsProvider: createDefaultRealFsProvider,
|
|
1110
|
+
root
|
|
1111
|
+
}),
|
|
1112
|
+
createRealFsProvider: createDefaultRealFsProvider,
|
|
1113
|
+
createReadonlyProvider: createHardenedReadonlyProvider,
|
|
1114
|
+
createMemoryProvider: () => new MemoryProvider(),
|
|
1115
|
+
createShadowProvider: (provider, shadowOptions) => new ShadowProvider(provider, shadowOptions),
|
|
1116
|
+
createShadowPathPredicate: (paths) => createShadowPathPredicate([...paths])
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function normalizeShadowPath(pathValue) {
|
|
1120
|
+
const trimmedPath = pathValue.trim();
|
|
1121
|
+
if (trimmedPath.startsWith("/")) return trimmedPath;
|
|
1122
|
+
return `/${trimmedPath.startsWith("./") ? trimmedPath.slice(2) : trimmedPath}`;
|
|
1123
|
+
}
|
|
1124
|
+
function createRealFsProviderForSpec(mountSpec, dependencies, mountKind) {
|
|
1125
|
+
if (mountSpec.pinnedHostRoot) return dependencies.createPinnedRealFsProvider(mountSpec.pinnedHostRoot);
|
|
1126
|
+
if (mountSpec.hostPath) return dependencies.createRealFsProvider(mountSpec.hostPath);
|
|
1127
|
+
throw new Error(`${mountKind} mounts require hostPath or pinnedHostRoot`);
|
|
1128
|
+
}
|
|
1129
|
+
function createProviderFromSpec(mountSpec, dependencies) {
|
|
1130
|
+
switch (mountSpec.kind) {
|
|
1131
|
+
case "memory": return dependencies.createMemoryProvider();
|
|
1132
|
+
case "realfs": return createRealFsProviderForSpec(mountSpec, dependencies, "realfs");
|
|
1133
|
+
case "realfs-readonly": return dependencies.createReadonlyProvider(createRealFsProviderForSpec(mountSpec, dependencies, "realfs-readonly"));
|
|
1134
|
+
case "filtered-workspace": return createFilteredWorkspaceProvider({
|
|
1135
|
+
baseProvider: dependencies.createPinnedRealFsProvider(mountSpec.pinnedHostRoot),
|
|
1136
|
+
dependencies,
|
|
1137
|
+
policy: mountSpec.policy
|
|
1138
|
+
});
|
|
1139
|
+
case "shadow": {
|
|
1140
|
+
let shadowProvider = mountSpec.hostPath || mountSpec.pinnedHostRoot ? createRealFsProviderForSpec(mountSpec, dependencies, "shadow") : dependencies.createMemoryProvider();
|
|
1141
|
+
const shadowConfig = mountSpec.shadowConfig;
|
|
1142
|
+
if (shadowConfig?.deny.length) shadowProvider = dependencies.createShadowProvider(shadowProvider, {
|
|
1143
|
+
shouldShadow: dependencies.createShadowPathPredicate(shadowConfig.deny.map((shadowPath) => normalizeShadowPath(shadowPath))),
|
|
1144
|
+
writeMode: "deny"
|
|
1145
|
+
});
|
|
1146
|
+
if (shadowConfig?.tmpfs.length) shadowProvider = dependencies.createShadowProvider(shadowProvider, {
|
|
1147
|
+
shouldShadow: dependencies.createShadowPathPredicate(shadowConfig.tmpfs.map((shadowPath) => normalizeShadowPath(shadowPath))),
|
|
1148
|
+
writeMode: "tmpfs"
|
|
1149
|
+
});
|
|
1150
|
+
return shadowProvider;
|
|
1151
|
+
}
|
|
1152
|
+
default: throw new Error("Unsupported VFS mount kind.");
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function createVfsMounts(vfsMounts, dependencies) {
|
|
1156
|
+
const mountMap = {};
|
|
1157
|
+
for (const [guestPath, mountSpec] of Object.entries(vfsMounts)) mountMap[guestPath] = createProviderFromSpec(mountSpec, dependencies);
|
|
1158
|
+
return mountMap;
|
|
1159
|
+
}
|
|
1160
|
+
function collectPinnedRealFsRoots(vfsMounts) {
|
|
1161
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const mountSpec of Object.values(vfsMounts)) if ("pinnedHostRoot" in mountSpec && mountSpec.pinnedHostRoot) roots.set(mountSpec.pinnedHostRoot.fd, mountSpec.pinnedHostRoot);
|
|
1163
|
+
return [...roots.values()];
|
|
1164
|
+
}
|
|
1165
|
+
function closePinnedRealFsRoots(roots, dependencies) {
|
|
1166
|
+
const closeErrors = [];
|
|
1167
|
+
for (const root of roots) try {
|
|
1168
|
+
dependencies.closePinnedRealFsRoot(root);
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
closeErrors.push(error);
|
|
1171
|
+
}
|
|
1172
|
+
if (closeErrors.length === 1) throw closeErrors[0];
|
|
1173
|
+
if (closeErrors.length > 1) throw new AggregateError(closeErrors, "Multiple pinned RealFS roots failed to close.");
|
|
1174
|
+
}
|
|
1175
|
+
function closePinnedRealFsRootsAfterFailure(roots, dependencies) {
|
|
1176
|
+
try {
|
|
1177
|
+
closePinnedRealFsRoots(roots, dependencies);
|
|
1178
|
+
} catch {}
|
|
1179
|
+
}
|
|
1180
|
+
function resolveManagedVmIngressOptions(ingressOptions = {}) {
|
|
1181
|
+
const resolvedOptions = { ...MANAGED_VM_DEFAULT_INGRESS_OPTIONS };
|
|
1182
|
+
if (ingressOptions.listenHost !== void 0) resolvedOptions.listenHost = ingressOptions.listenHost;
|
|
1183
|
+
if (ingressOptions.listenPort !== void 0) resolvedOptions.listenPort = ingressOptions.listenPort;
|
|
1184
|
+
if (ingressOptions.allowWebSockets !== void 0) resolvedOptions.allowWebSockets = ingressOptions.allowWebSockets;
|
|
1185
|
+
if (ingressOptions.hooks !== void 0) resolvedOptions.hooks = ingressOptions.hooks;
|
|
1186
|
+
if (ingressOptions.bufferResponseBody !== void 0) resolvedOptions.bufferResponseBody = ingressOptions.bufferResponseBody;
|
|
1187
|
+
if (ingressOptions.maxBufferedResponseBodyBytes !== void 0) resolvedOptions.maxBufferedResponseBodyBytes = ingressOptions.maxBufferedResponseBodyBytes;
|
|
1188
|
+
if (ingressOptions.upstreamHeaderTimeoutMs !== void 0) resolvedOptions.upstreamHeaderTimeoutMs = ingressOptions.upstreamHeaderTimeoutMs;
|
|
1189
|
+
if (ingressOptions.upstreamResponseTimeoutMs !== void 0) resolvedOptions.upstreamResponseTimeoutMs = ingressOptions.upstreamResponseTimeoutMs;
|
|
1190
|
+
return resolvedOptions;
|
|
1191
|
+
}
|
|
1192
|
+
function normalizePolicyHostname(hostname) {
|
|
1193
|
+
return hostname.toLowerCase();
|
|
1194
|
+
}
|
|
1195
|
+
function parseTcpHostEndpoint(endpoint) {
|
|
1196
|
+
if (endpoint.startsWith("[")) {
|
|
1197
|
+
const closingBracketIndex = endpoint.indexOf("]");
|
|
1198
|
+
if (closingBracketIndex > 1) {
|
|
1199
|
+
const portValue = Number.parseInt(endpoint.slice(closingBracketIndex + 2), 10);
|
|
1200
|
+
if (!Number.isFinite(portValue)) return;
|
|
1201
|
+
return {
|
|
1202
|
+
hostname: normalizePolicyHostname(endpoint.slice(1, closingBracketIndex)),
|
|
1203
|
+
port: portValue
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const portSeparatorIndex = endpoint.lastIndexOf(":");
|
|
1208
|
+
if (portSeparatorIndex <= 0) return;
|
|
1209
|
+
const portValue = Number.parseInt(endpoint.slice(portSeparatorIndex + 1), 10);
|
|
1210
|
+
if (!Number.isFinite(portValue)) return;
|
|
1211
|
+
return {
|
|
1212
|
+
hostname: normalizePolicyHostname(endpoint.slice(0, portSeparatorIndex)),
|
|
1213
|
+
port: portValue
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function ipv4AddressIsInternal(ipAddress) {
|
|
1217
|
+
const octets = ipAddress.split(".").map((segment) => Number.parseInt(segment, 10));
|
|
1218
|
+
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet))) return false;
|
|
1219
|
+
const firstOctet = octets[0];
|
|
1220
|
+
const secondOctet = octets[1];
|
|
1221
|
+
if (firstOctet === void 0 || secondOctet === void 0) return false;
|
|
1222
|
+
return firstOctet === 10 || firstOctet === 127 || firstOctet === 169 && secondOctet === 254 || firstOctet === 172 && secondOctet >= 16 && secondOctet <= 31 || firstOctet === 192 && secondOctet === 168 || firstOctet === 100 && secondOctet >= 64 && secondOctet <= 127;
|
|
1223
|
+
}
|
|
1224
|
+
function ipAddressIsInternal(ipAddress) {
|
|
1225
|
+
if (net.isIP(ipAddress) === 4) return ipv4AddressIsInternal(ipAddress);
|
|
1226
|
+
const normalizedIpAddress = ipAddress.toLowerCase();
|
|
1227
|
+
if (normalizedIpAddress.startsWith("::ffff:")) return ipv4AddressIsInternal(normalizedIpAddress.slice(7));
|
|
1228
|
+
return normalizedIpAddress === "::1" || normalizedIpAddress.startsWith("fc") || normalizedIpAddress.startsWith("fd") || normalizedIpAddress.startsWith("fe80:");
|
|
1229
|
+
}
|
|
1230
|
+
function endpointHostnameIsInternal(hostname) {
|
|
1231
|
+
const normalizedHostname = normalizePolicyHostname(hostname);
|
|
1232
|
+
return normalizedHostname === "localhost" || normalizedHostname === "host.docker.internal" || ipAddressIsInternal(normalizedHostname);
|
|
1233
|
+
}
|
|
1234
|
+
function deriveInternalTcpHostRules(tcpHosts) {
|
|
1235
|
+
if (!tcpHosts) return [];
|
|
1236
|
+
const rules = [];
|
|
1237
|
+
for (const [tcpHostKey, tcpHostTarget] of Object.entries(tcpHosts)) {
|
|
1238
|
+
const exposedEndpoint = parseTcpHostEndpoint(tcpHostKey);
|
|
1239
|
+
const targetEndpoint = parseTcpHostEndpoint(tcpHostTarget);
|
|
1240
|
+
if (!exposedEndpoint || !targetEndpoint || !endpointHostnameIsInternal(targetEndpoint.hostname)) continue;
|
|
1241
|
+
if (!rules.some((rule) => rule.hostname === exposedEndpoint.hostname && rule.port === exposedEndpoint.port)) rules.push(exposedEndpoint);
|
|
1242
|
+
}
|
|
1243
|
+
return rules;
|
|
1244
|
+
}
|
|
1245
|
+
function mergeUniqueHosts(hosts, additionalHosts) {
|
|
1246
|
+
const mergedHosts = [...hosts];
|
|
1247
|
+
for (const host of additionalHosts) if (!mergedHosts.includes(host)) mergedHosts.push(host);
|
|
1248
|
+
return mergedHosts;
|
|
1249
|
+
}
|
|
1250
|
+
function normalizeGitRepoPath(repoPath) {
|
|
1251
|
+
return repoPath.replace(/^\/+/u, "").replace(/\.git$/u, "");
|
|
1252
|
+
}
|
|
1253
|
+
function createGitReadOnlySshEgressOptions(options) {
|
|
1254
|
+
const allowedRepos = options.allowedRepos === void 0 ? void 0 : new Set(options.allowedRepos.map((repoPath) => normalizeGitRepoPath(repoPath)));
|
|
1255
|
+
return {
|
|
1256
|
+
allowedHosts: [...options.allowedHosts],
|
|
1257
|
+
...options.agent ? { agent: options.agent } : {},
|
|
1258
|
+
...options.knownHostsFile ? { knownHostsFile: options.knownHostsFile } : {},
|
|
1259
|
+
execPolicy: (request) => {
|
|
1260
|
+
const gitExec = getInfoFromSshExecRequest(request);
|
|
1261
|
+
if (!gitExec) return {
|
|
1262
|
+
allow: false,
|
|
1263
|
+
message: "agent-vm: non-git SSH exec is denied"
|
|
1264
|
+
};
|
|
1265
|
+
if (gitExec.service === "git-receive-pack") return {
|
|
1266
|
+
allow: false,
|
|
1267
|
+
message: "agent-vm: git push over guest SSH is denied"
|
|
1268
|
+
};
|
|
1269
|
+
if (gitExec.service !== "git-upload-pack") return {
|
|
1270
|
+
allow: false,
|
|
1271
|
+
message: "agent-vm: unsupported git SSH service is denied"
|
|
1272
|
+
};
|
|
1273
|
+
if (allowedRepos && !allowedRepos.has(normalizeGitRepoPath(gitExec.repo))) return {
|
|
1274
|
+
allow: false,
|
|
1275
|
+
message: "agent-vm: git repository is not allowlisted for guest SSH reads"
|
|
1276
|
+
};
|
|
1277
|
+
return { allow: true };
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
function createInternalTcpHostPolicy(rules) {
|
|
1282
|
+
if (rules.length === 0) return;
|
|
1283
|
+
const ruleHostnames = new Set(rules.map((rule) => rule.hostname));
|
|
1284
|
+
return (info) => {
|
|
1285
|
+
const hostname = normalizePolicyHostname(info.hostname);
|
|
1286
|
+
const exactRuleMatched = rules.some((rule) => rule.hostname === hostname && rule.port === info.port);
|
|
1287
|
+
if (ruleHostnames.has(hostname)) return exactRuleMatched;
|
|
1288
|
+
if (ipAddressIsInternal(info.ip)) return false;
|
|
1289
|
+
return true;
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
async function createManagedVm(options, dependencies = createDefaultDependencies()) {
|
|
1293
|
+
dependencies.configureHostNetworkDefaults?.();
|
|
1294
|
+
const hasTcpHosts = options.tcpHosts && Object.keys(options.tcpHosts).length > 0;
|
|
1295
|
+
const hasSshEgress = options.sshEgress !== void 0 && options.sshEgress.allowedHosts.length > 0;
|
|
1296
|
+
const internalTcpHostRules = deriveInternalTcpHostRules(options.tcpHosts);
|
|
1297
|
+
const allowedInternalHosts = mergeUniqueHosts([], internalTcpHostRules.map((rule) => rule.hostname));
|
|
1298
|
+
const isIpAllowed = createInternalTcpHostPolicy(internalTcpHostRules);
|
|
1299
|
+
const pinnedRealFsRoots = collectPinnedRealFsRoots(options.vfsMounts);
|
|
1300
|
+
let vmInstance;
|
|
1301
|
+
try {
|
|
1302
|
+
const hookBundle = dependencies.createHttpHooks({
|
|
1303
|
+
allowedHosts: options.allowedHosts,
|
|
1304
|
+
...allowedInternalHosts.length > 0 ? { allowedInternalHosts } : {},
|
|
1305
|
+
...isIpAllowed ? { isIpAllowed } : {},
|
|
1306
|
+
secrets: options.secrets,
|
|
1307
|
+
...options.onRequest ? { onRequest: options.onRequest } : {},
|
|
1308
|
+
...options.onResponse ? { onResponse: options.onResponse } : {}
|
|
1309
|
+
});
|
|
1310
|
+
vmInstance = await dependencies.createVm({
|
|
1311
|
+
...options.imagePath.length > 0 ? { sandbox: { imagePath: options.imagePath } } : {},
|
|
1312
|
+
...options.sessionLabel ? { sessionLabel: options.sessionLabel } : {},
|
|
1313
|
+
rootfs: {
|
|
1314
|
+
mode: options.rootfsMode,
|
|
1315
|
+
...options.runtimeRootfsSize === void 0 ? {} : { size: options.runtimeRootfsSize }
|
|
1316
|
+
},
|
|
1317
|
+
memory: options.memory,
|
|
1318
|
+
cpus: options.cpus,
|
|
1319
|
+
env: {
|
|
1320
|
+
...hookBundle.env,
|
|
1321
|
+
...options.env
|
|
1322
|
+
},
|
|
1323
|
+
httpHooks: hookBundle.httpHooks,
|
|
1324
|
+
vfs: {
|
|
1325
|
+
fuseMount: "/data",
|
|
1326
|
+
mounts: createVfsMounts(options.vfsMounts, dependencies)
|
|
1327
|
+
},
|
|
1328
|
+
...hasTcpHosts || hasSshEgress ? {
|
|
1329
|
+
dns: {
|
|
1330
|
+
mode: "synthetic",
|
|
1331
|
+
syntheticIPv4: SYNTHETIC_DNS_IPV4_BENCHMARK,
|
|
1332
|
+
syntheticIPv6: SYNTHETIC_DNS_IPV6_IPV4_MAPPED_BENCHMARK,
|
|
1333
|
+
syntheticHostMapping: "per-host"
|
|
1334
|
+
},
|
|
1335
|
+
...hasSshEgress ? { ssh: options.sshEgress } : {},
|
|
1336
|
+
...hasTcpHosts ? { tcp: { hosts: options.tcpHosts } } : {}
|
|
1337
|
+
} : {}
|
|
1338
|
+
});
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
closePinnedRealFsRootsAfterFailure(pinnedRealFsRoots, dependencies);
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
return {
|
|
1344
|
+
fs: vmInstance.fs,
|
|
1345
|
+
id: vmInstance.id,
|
|
1346
|
+
exec(command, execOptions) {
|
|
1347
|
+
const normalizedCommand = typeof command === "string" ? command : [...command];
|
|
1348
|
+
return vmInstance.exec(normalizedCommand, execOptions);
|
|
1349
|
+
},
|
|
1350
|
+
async enableSsh(sshOptions) {
|
|
1351
|
+
const sshAccess = await vmInstance.enableSsh(sshOptions);
|
|
1352
|
+
let serverHostKey;
|
|
1353
|
+
try {
|
|
1354
|
+
serverHostKey = await readSshServerHostKey(vmInstance);
|
|
1355
|
+
} catch (identityError) {
|
|
1356
|
+
return await closeSshAccessAfterIdentityFailure(sshAccess, identityError);
|
|
1357
|
+
}
|
|
1358
|
+
return {
|
|
1359
|
+
close: async () => await sshAccess.close(),
|
|
1360
|
+
command: sshAccess.command,
|
|
1361
|
+
host: sshAccess.host,
|
|
1362
|
+
identityFile: sshAccess.identityFile,
|
|
1363
|
+
port: sshAccess.port,
|
|
1364
|
+
serverHostKey,
|
|
1365
|
+
user: sshAccess.user
|
|
1366
|
+
};
|
|
1367
|
+
},
|
|
1368
|
+
async enableIngress(ingressOptions) {
|
|
1369
|
+
return await vmInstance.enableIngress(resolveManagedVmIngressOptions(ingressOptions));
|
|
1370
|
+
},
|
|
1371
|
+
getHostPid() {
|
|
1372
|
+
return vmInstance.getHostPid?.() ?? null;
|
|
1373
|
+
},
|
|
1374
|
+
getVmInstance() {
|
|
1375
|
+
return vmInstance;
|
|
1376
|
+
},
|
|
1377
|
+
setIngressRoutes(routes) {
|
|
1378
|
+
vmInstance.setIngressRoutes(routes);
|
|
1379
|
+
},
|
|
1380
|
+
async start() {
|
|
1381
|
+
await vmInstance.start();
|
|
1382
|
+
},
|
|
1383
|
+
async close() {
|
|
1384
|
+
const closeErrors = [];
|
|
1385
|
+
try {
|
|
1386
|
+
await vmInstance.close();
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
closeErrors.push(error);
|
|
1389
|
+
}
|
|
1390
|
+
try {
|
|
1391
|
+
closePinnedRealFsRoots(pinnedRealFsRoots, dependencies);
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
if (error instanceof AggregateError) closeErrors.push(...error.errors);
|
|
1394
|
+
else closeErrors.push(error);
|
|
1395
|
+
}
|
|
1396
|
+
if (closeErrors.length === 1) throw closeErrors[0];
|
|
1397
|
+
if (closeErrors.length > 1) throw new AggregateError(closeErrors, "Managed Gondolin VM cleanup failed.");
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
//#endregion
|
|
1402
|
+
//#region src/managed-vm-provider.ts
|
|
1403
|
+
function isRecord(value) {
|
|
1404
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1405
|
+
}
|
|
1406
|
+
function formatUnknownValue(value) {
|
|
1407
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1408
|
+
}
|
|
1409
|
+
function assertCreateRequestSupported(request) {
|
|
1410
|
+
if (![
|
|
1411
|
+
"readonly",
|
|
1412
|
+
"memory",
|
|
1413
|
+
"cow"
|
|
1414
|
+
].includes(request.rootfsMode)) throw new Error(`Unsupported managed VM rootfs mode: ${formatUnknownValue(request.rootfsMode)}`);
|
|
1415
|
+
if (!Number.isSafeInteger(request.resources.cpuCount) || request.resources.cpuCount <= 0) throw new Error("Managed VM CPU count must be a positive safe integer.");
|
|
1416
|
+
if (request.imageReference.length === 0 || request.resources.memory.length === 0) throw new Error("Managed VM image reference and memory must be non-empty.");
|
|
1417
|
+
if (request.sshEgress !== void 0 && request.sshEgress.kind !== "git-read-only") throw new Error(`Unsupported managed VM SSH egress kind: ${formatUnknownValue(request.sshEgress.kind)}`);
|
|
1418
|
+
for (const [guestPath, mount] of Object.entries(request.mounts)) {
|
|
1419
|
+
if (!guestPath.startsWith("/")) throw new Error(`Managed VM mount path must be absolute: ${guestPath}`);
|
|
1420
|
+
if (![
|
|
1421
|
+
"host-directory",
|
|
1422
|
+
"owned-host-directory",
|
|
1423
|
+
"owned-filtered-workspace",
|
|
1424
|
+
"memory",
|
|
1425
|
+
"shadow"
|
|
1426
|
+
].includes(mount.kind)) throw new Error(`Unsupported managed VM mount kind: ${formatUnknownValue(mount.kind)}`);
|
|
1427
|
+
}
|
|
1428
|
+
const mediatedSecretNames = /* @__PURE__ */ new Set();
|
|
1429
|
+
for (const secret of request.mediatedSecrets) {
|
|
1430
|
+
if (mediatedSecretNames.has(secret.environmentVariable)) throw new Error(`Duplicate managed VM mediated-secret environment variable: ${secret.environmentVariable}`);
|
|
1431
|
+
mediatedSecretNames.add(secret.environmentVariable);
|
|
1432
|
+
}
|
|
1433
|
+
const tcpGuestHosts = /* @__PURE__ */ new Set();
|
|
1434
|
+
for (const mapping of request.tcpHosts) {
|
|
1435
|
+
if (tcpGuestHosts.has(mapping.guestHost)) throw new Error(`Duplicate managed VM TCP guest host: ${mapping.guestHost}`);
|
|
1436
|
+
tcpGuestHosts.add(mapping.guestHost);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function closeOwnedDirectoryTransfers(transfers) {
|
|
1440
|
+
const closeErrors = [];
|
|
1441
|
+
for (const transfer of transfers) try {
|
|
1442
|
+
transfer.close();
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
closeErrors.push(error);
|
|
1445
|
+
}
|
|
1446
|
+
return closeErrors;
|
|
1447
|
+
}
|
|
1448
|
+
function isReadonlyStringArray(value) {
|
|
1449
|
+
return Array.isArray(value);
|
|
1450
|
+
}
|
|
1451
|
+
function translateExecEnvironment(environment) {
|
|
1452
|
+
return isReadonlyStringArray(environment) ? [...environment] : { ...environment };
|
|
1453
|
+
}
|
|
1454
|
+
function translateExecStdin(stdin) {
|
|
1455
|
+
if (typeof stdin === "string") return stdin;
|
|
1456
|
+
if (stdin instanceof Uint8Array) return Buffer.from(stdin);
|
|
1457
|
+
return (async function* () {
|
|
1458
|
+
for await (const chunk of stdin) yield Buffer.from(chunk);
|
|
1459
|
+
})();
|
|
1460
|
+
}
|
|
1461
|
+
const MANAGED_VM_EXEC_OUTPUT_WINDOW_MIN_BYTES = 4 * 1024;
|
|
1462
|
+
const MANAGED_VM_EXEC_OUTPUT_WINDOW_MAX_BYTES = 16 * 1024 * 1024;
|
|
1463
|
+
function translateExecStreamMode(mode) {
|
|
1464
|
+
if (typeof mode !== "object" || mode === null || !("kind" in mode)) throw new Error("Unsupported managed VM exec output mode.");
|
|
1465
|
+
if (mode.kind === "pipe") return "pipe";
|
|
1466
|
+
if (mode.kind === "discard") return "ignore";
|
|
1467
|
+
throw new Error("Unsupported managed VM exec output mode.");
|
|
1468
|
+
}
|
|
1469
|
+
function translateExecStreamingOptions(output) {
|
|
1470
|
+
if (!Number.isInteger(output.windowBytes) || output.windowBytes < MANAGED_VM_EXEC_OUTPUT_WINDOW_MIN_BYTES || output.windowBytes > MANAGED_VM_EXEC_OUTPUT_WINDOW_MAX_BYTES) throw new Error(`Managed VM exec output window must be an integer between ${String(MANAGED_VM_EXEC_OUTPUT_WINDOW_MIN_BYTES)} and ${String(MANAGED_VM_EXEC_OUTPUT_WINDOW_MAX_BYTES)} bytes.`);
|
|
1471
|
+
return {
|
|
1472
|
+
stderr: translateExecStreamMode(output.stderr),
|
|
1473
|
+
stdout: translateExecStreamMode(output.stdout),
|
|
1474
|
+
windowBytes: output.windowBytes
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function translateExecOptions(options) {
|
|
1478
|
+
return {
|
|
1479
|
+
...options.argv ? { argv: [...options.argv] } : {},
|
|
1480
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
1481
|
+
...options.env ? { env: translateExecEnvironment(options.env) } : {},
|
|
1482
|
+
...options.output === void 0 ? {} : translateExecStreamingOptions(options.output),
|
|
1483
|
+
...options.pty === void 0 ? {} : { pty: options.pty },
|
|
1484
|
+
...options.signal ? { signal: options.signal } : {},
|
|
1485
|
+
...options.stdin === void 0 ? {} : { stdin: translateExecStdin(options.stdin) }
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function wrapExecProcess(nativeProcess) {
|
|
1489
|
+
const result = nativeProcess.result;
|
|
1490
|
+
return Object.assign(result, {
|
|
1491
|
+
[Symbol.asyncIterator]() {
|
|
1492
|
+
return nativeProcess[Symbol.asyncIterator]();
|
|
1493
|
+
},
|
|
1494
|
+
end() {
|
|
1495
|
+
nativeProcess.end();
|
|
1496
|
+
},
|
|
1497
|
+
lines() {
|
|
1498
|
+
return nativeProcess.lines();
|
|
1499
|
+
},
|
|
1500
|
+
output() {
|
|
1501
|
+
return nativeProcess.output();
|
|
1502
|
+
},
|
|
1503
|
+
resize(rows, columns) {
|
|
1504
|
+
nativeProcess.resize(rows, columns);
|
|
1505
|
+
},
|
|
1506
|
+
result,
|
|
1507
|
+
write(data) {
|
|
1508
|
+
nativeProcess.write(typeof data === "string" ? data : Buffer.from(data));
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
async function assertReadonlyInputSourcesStayWithinOwnedRoot(pinnedRoot, policy) {
|
|
1513
|
+
const canonicalRootPrefix = pinnedRoot.realPath.endsWith(path.sep) ? pinnedRoot.realPath : `${pinnedRoot.realPath}${path.sep}`;
|
|
1514
|
+
await Promise.all(policy.readonlyInputs.map(async (readonlyInput) => {
|
|
1515
|
+
const sourcePath = path.join(pinnedRoot.realPath, ...readonlyInput.sourceRelativePath.split("/"));
|
|
1516
|
+
const canonicalSourcePath = await fs.realpath(sourcePath);
|
|
1517
|
+
if (canonicalSourcePath !== pinnedRoot.realPath && !canonicalSourcePath.startsWith(canonicalRootPrefix)) throw new Error(`Managed filtered workspace read-only source crosses the owned root boundary: ${readonlyInput.sourceRelativePath}`);
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1520
|
+
async function translateMounts(mounts, ownedDirectoryRoots) {
|
|
1521
|
+
const translatedMounts = {};
|
|
1522
|
+
const transfers = [];
|
|
1523
|
+
const sourceValidations = [];
|
|
1524
|
+
try {
|
|
1525
|
+
for (const [guestPath, mount] of Object.entries(mounts)) switch (mount.kind) {
|
|
1526
|
+
case "host-directory":
|
|
1527
|
+
translatedMounts[guestPath] = {
|
|
1528
|
+
hostPath: mount.hostPath,
|
|
1529
|
+
kind: mount.access === "read-only" ? "realfs-readonly" : "realfs"
|
|
1530
|
+
};
|
|
1531
|
+
break;
|
|
1532
|
+
case "owned-host-directory": {
|
|
1533
|
+
const pinnedRoot = ownedDirectoryRoots.get(mount.directory);
|
|
1534
|
+
if (!pinnedRoot) throw new Error("Owned host directory was not acquired by this Gondolin provider.");
|
|
1535
|
+
assertPinnedRealFsRoot(pinnedRoot);
|
|
1536
|
+
transfers.push(mount.directory.consume());
|
|
1537
|
+
ownedDirectoryRoots.delete(mount.directory);
|
|
1538
|
+
translatedMounts[guestPath] = {
|
|
1539
|
+
kind: mount.access === "read-only" ? "realfs-readonly" : "realfs",
|
|
1540
|
+
pinnedHostRoot: pinnedRoot
|
|
1541
|
+
};
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
case "owned-filtered-workspace": {
|
|
1545
|
+
const pinnedRoot = ownedDirectoryRoots.get(mount.directory);
|
|
1546
|
+
if (!pinnedRoot) throw new Error("Owned host directory was not acquired by this Gondolin provider.");
|
|
1547
|
+
assertPinnedRealFsRoot(pinnedRoot);
|
|
1548
|
+
transfers.push(mount.directory.consume());
|
|
1549
|
+
ownedDirectoryRoots.delete(mount.directory);
|
|
1550
|
+
const policy = validateManagedVmFilteredWorkspacePolicy(mount.policy);
|
|
1551
|
+
sourceValidations.push({
|
|
1552
|
+
pinnedRoot,
|
|
1553
|
+
policy
|
|
1554
|
+
});
|
|
1555
|
+
translatedMounts[guestPath] = {
|
|
1556
|
+
kind: "filtered-workspace",
|
|
1557
|
+
pinnedHostRoot: pinnedRoot,
|
|
1558
|
+
policy
|
|
1559
|
+
};
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
case "memory":
|
|
1563
|
+
translatedMounts[guestPath] = { kind: "memory" };
|
|
1564
|
+
break;
|
|
1565
|
+
case "shadow":
|
|
1566
|
+
translatedMounts[guestPath] = {
|
|
1567
|
+
hostPath: mount.hostPath,
|
|
1568
|
+
kind: "shadow",
|
|
1569
|
+
shadowConfig: {
|
|
1570
|
+
deny: mount.deny,
|
|
1571
|
+
tmpfs: mount.temporaryFilesystems
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
await Promise.all(sourceValidations.map(({ pinnedRoot, policy }) => assertReadonlyInputSourcesStayWithinOwnedRoot(pinnedRoot, policy)));
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
const closeErrors = closeOwnedDirectoryTransfers(transfers);
|
|
1579
|
+
if (closeErrors.length > 0) throw new AggregateError([error, ...closeErrors], "Managed VM mount translation and owned-directory cleanup both failed.", { cause: error });
|
|
1580
|
+
throw error;
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
mounts: translatedMounts,
|
|
1584
|
+
transfers
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function wrapManagedVm(nativeVm, ownedDirectoryTransfers) {
|
|
1588
|
+
let closePromise;
|
|
1589
|
+
let hostProcessId = null;
|
|
1590
|
+
let lifecycleState = "created";
|
|
1591
|
+
let startPromise;
|
|
1592
|
+
const closeOnce = async () => {
|
|
1593
|
+
if (startPromise) try {
|
|
1594
|
+
await startPromise;
|
|
1595
|
+
} catch {}
|
|
1596
|
+
const closeErrors = [];
|
|
1597
|
+
try {
|
|
1598
|
+
await nativeVm.close();
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
closeErrors.push(error);
|
|
1601
|
+
}
|
|
1602
|
+
closeErrors.push(...closeOwnedDirectoryTransfers(ownedDirectoryTransfers));
|
|
1603
|
+
try {
|
|
1604
|
+
if (closeErrors.length === 1) throw closeErrors[0];
|
|
1605
|
+
if (closeErrors.length > 1) throw new AggregateError(closeErrors, "Managed VM cleanup failed.");
|
|
1606
|
+
} finally {
|
|
1607
|
+
hostProcessId = null;
|
|
1608
|
+
lifecycleState = "closed";
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
return {
|
|
1612
|
+
async close() {
|
|
1613
|
+
if (!closePromise) {
|
|
1614
|
+
lifecycleState = "closing";
|
|
1615
|
+
closePromise = closeOnce();
|
|
1616
|
+
}
|
|
1617
|
+
await closePromise;
|
|
1618
|
+
},
|
|
1619
|
+
configureIngressRoutes(routes) {
|
|
1620
|
+
nativeVm.setIngressRoutes(routes.map((route) => ({
|
|
1621
|
+
port: route.port,
|
|
1622
|
+
prefix: route.prefix,
|
|
1623
|
+
stripPrefix: route.stripPrefix
|
|
1624
|
+
})));
|
|
1625
|
+
},
|
|
1626
|
+
async enableIngress(options) {
|
|
1627
|
+
return await nativeVm.enableIngress(options);
|
|
1628
|
+
},
|
|
1629
|
+
async enableSsh(options) {
|
|
1630
|
+
const access = await nativeVm.enableSsh(options);
|
|
1631
|
+
if (!access.command || !access.identityFile || !access.user) {
|
|
1632
|
+
await access.close();
|
|
1633
|
+
throw new Error("Gondolin SSH access omitted required neutral connection fields.");
|
|
1634
|
+
}
|
|
1635
|
+
return {
|
|
1636
|
+
close: async () => await access.close(),
|
|
1637
|
+
command: access.command,
|
|
1638
|
+
host: access.host,
|
|
1639
|
+
identityFile: access.identityFile,
|
|
1640
|
+
port: access.port,
|
|
1641
|
+
serverHostKey: access.serverHostKey,
|
|
1642
|
+
user: access.user
|
|
1643
|
+
};
|
|
1644
|
+
},
|
|
1645
|
+
exec(command, options) {
|
|
1646
|
+
const normalizedCommand = typeof command === "string" ? command : [...command];
|
|
1647
|
+
return wrapExecProcess(nativeVm.exec(normalizedCommand, options ? translateExecOptions(options) : void 0));
|
|
1648
|
+
},
|
|
1649
|
+
getHostProcessId() {
|
|
1650
|
+
if (hostProcessId === null) return null;
|
|
1651
|
+
const currentHostProcessId = nativeVm.getHostPid();
|
|
1652
|
+
if (currentHostProcessId === null) return null;
|
|
1653
|
+
if (currentHostProcessId !== hostProcessId) throw new Error(`Gondolin runner identity changed for managed VM '${nativeVm.id}': expected pid ${String(hostProcessId)}, observed ${String(currentHostProcessId)}.`);
|
|
1654
|
+
return hostProcessId;
|
|
1655
|
+
},
|
|
1656
|
+
id: nativeVm.id,
|
|
1657
|
+
async start() {
|
|
1658
|
+
if (lifecycleState === "closing" || lifecycleState === "closed") throw new Error("Managed VM cannot start after close has been requested.");
|
|
1659
|
+
if (lifecycleState === "started") return;
|
|
1660
|
+
if (!startPromise) {
|
|
1661
|
+
lifecycleState = "starting";
|
|
1662
|
+
startPromise = (async () => {
|
|
1663
|
+
try {
|
|
1664
|
+
await nativeVm.start();
|
|
1665
|
+
const startedHostProcessId = assertPositiveHostProcessId(nativeVm.getHostPid());
|
|
1666
|
+
if (lifecycleState !== "starting") throw new Error("Managed VM closed while startup was settling.");
|
|
1667
|
+
hostProcessId = startedHostProcessId;
|
|
1668
|
+
lifecycleState = "started";
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
if (lifecycleState === "starting") lifecycleState = "start-failed";
|
|
1671
|
+
throw error;
|
|
1672
|
+
}
|
|
1673
|
+
})();
|
|
1674
|
+
}
|
|
1675
|
+
await startPromise;
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function openOwnedHostDirectory(hostPath, ownedDirectoryRoots) {
|
|
1680
|
+
const pinnedRoot = pinRealFsRoot(hostPath);
|
|
1681
|
+
const directory = createOwnedHostDirectoryController({
|
|
1682
|
+
identity: {
|
|
1683
|
+
canonicalPath: pinnedRoot.realPath,
|
|
1684
|
+
device: pinnedRoot.device,
|
|
1685
|
+
inode: pinnedRoot.inode
|
|
1686
|
+
},
|
|
1687
|
+
onClose: () => closePinnedRealFsRoot(pinnedRoot),
|
|
1688
|
+
onConsume: () => assertPinnedRealFsRoot(pinnedRoot)
|
|
1689
|
+
});
|
|
1690
|
+
ownedDirectoryRoots.set(directory, pinnedRoot);
|
|
1691
|
+
return directory;
|
|
1692
|
+
}
|
|
1693
|
+
function createGondolinManagedVmProvider() {
|
|
1694
|
+
const ownedDirectoryRoots = /* @__PURE__ */ new WeakMap();
|
|
1695
|
+
return {
|
|
1696
|
+
diagnostics: { async checkCompatibility() {
|
|
1697
|
+
const diagnostics = [];
|
|
1698
|
+
try {
|
|
1699
|
+
await Promise.all([resolveGondolinPackageSpec(), resolveGondolinMinimumZigVersion()]);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
diagnostics.push({
|
|
1702
|
+
code: "gondolin-toolchain-unavailable",
|
|
1703
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1704
|
+
severity: "error"
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
return diagnostics;
|
|
1708
|
+
} },
|
|
1709
|
+
exactProcessTermination: createGondolinExactProcessTerminationCapability(),
|
|
1710
|
+
factory: { async createManagedVm(request) {
|
|
1711
|
+
assertCreateRequestSupported(request);
|
|
1712
|
+
const secrets = Object.fromEntries(request.mediatedSecrets.map((secret) => [secret.environmentVariable, {
|
|
1713
|
+
hosts: [...secret.allowedHosts],
|
|
1714
|
+
...secret.guestPlaceholder === void 0 ? {} : { placeholder: secret.guestPlaceholder },
|
|
1715
|
+
value: secret.value
|
|
1716
|
+
}]));
|
|
1717
|
+
const tcpHosts = Object.fromEntries(request.tcpHosts.map((mapping) => [mapping.guestHost, mapping.target]));
|
|
1718
|
+
const translatedMounts = await translateMounts(request.mounts, ownedDirectoryRoots);
|
|
1719
|
+
let nativeVm;
|
|
1720
|
+
try {
|
|
1721
|
+
nativeVm = await createManagedVm({
|
|
1722
|
+
allowedHosts: [...request.allowedHosts],
|
|
1723
|
+
cpus: request.resources.cpuCount,
|
|
1724
|
+
env: { ...request.environment },
|
|
1725
|
+
imagePath: request.imageReference,
|
|
1726
|
+
memory: request.resources.memory,
|
|
1727
|
+
...request.mediation?.onRequest ? { onRequest: request.mediation.onRequest } : {},
|
|
1728
|
+
...request.mediation?.onResponse ? { onResponse: request.mediation.onResponse } : {},
|
|
1729
|
+
rootfsMode: request.rootfsMode,
|
|
1730
|
+
...request.runtimeRootfsSize ? { runtimeRootfsSize: request.runtimeRootfsSize } : {},
|
|
1731
|
+
secrets,
|
|
1732
|
+
sessionLabel: request.sessionLabel,
|
|
1733
|
+
...request.sshEgress ? { sshEgress: createGitReadOnlySshEgressOptions({
|
|
1734
|
+
allowedHosts: request.sshEgress.allowedHosts,
|
|
1735
|
+
...request.sshEgress.agentSocket ? { agent: request.sshEgress.agentSocket } : {},
|
|
1736
|
+
...request.sshEgress.allowedRepositories ? { allowedRepos: request.sshEgress.allowedRepositories } : {},
|
|
1737
|
+
...request.sshEgress.knownHostsFile ? { knownHostsFile: request.sshEgress.knownHostsFile } : {}
|
|
1738
|
+
}) } : {},
|
|
1739
|
+
tcpHosts,
|
|
1740
|
+
vfsMounts: translatedMounts.mounts
|
|
1741
|
+
});
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
const closeErrors = closeOwnedDirectoryTransfers(translatedMounts.transfers);
|
|
1744
|
+
if (closeErrors.length > 0) throw new AggregateError([error, ...closeErrors], "Managed VM construction and owned-directory cleanup both failed.", { cause: error });
|
|
1745
|
+
throw error;
|
|
1746
|
+
}
|
|
1747
|
+
return wrapManagedVm(nativeVm, translatedMounts.transfers);
|
|
1748
|
+
} },
|
|
1749
|
+
images: { async prepareImage(request) {
|
|
1750
|
+
const parseErrors = [];
|
|
1751
|
+
const parsedRecipe = parse(await fs.readFile(request.recipePath, "utf8"), parseErrors, { allowTrailingComma: true });
|
|
1752
|
+
if (parseErrors.length > 0) throw new Error(`Invalid managed VM image recipe '${request.recipePath}': ${parseErrors.map((parseError) => printParseErrorCode(parseError.error)).join(", ")}`);
|
|
1753
|
+
if (!isRecord(parsedRecipe) || !validateBuildConfig(parsedRecipe)) throw new Error(`Managed VM image recipe has an invalid build shape: ${request.recipePath}`);
|
|
1754
|
+
const result = await buildImage({
|
|
1755
|
+
buildConfig: parsedRecipe,
|
|
1756
|
+
cacheDir: request.cacheDirectory,
|
|
1757
|
+
configDir: path.dirname(request.recipePath),
|
|
1758
|
+
...request.forceRebuild === void 0 ? {} : { fullReset: request.forceRebuild }
|
|
1759
|
+
});
|
|
1760
|
+
return {
|
|
1761
|
+
built: result.built,
|
|
1762
|
+
fingerprint: result.fingerprint,
|
|
1763
|
+
imageReference: result.imagePath
|
|
1764
|
+
};
|
|
1765
|
+
} },
|
|
1766
|
+
ownedDirectories: { openHostDirectory: (hostPath) => openOwnedHostDirectory(hostPath, ownedDirectoryRoots) }
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
//#endregion
|
|
1770
|
+
export { buildImageAssetFileNames, configureHostNetworkDefaults, createGondolinImageBuildTooling, createGondolinManagedVmProvider, hasBuiltImageAssets, resolveGondolinMinimumZigVersion, resolveGondolinPackageSpec };
|
|
1771
|
+
|
|
1772
|
+
//# sourceMappingURL=index.js.map
|