labgate 0.5.39 → 0.5.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +132 -248
  2. package/dist/cli.js +9 -33
  3. package/dist/cli.js.map +1 -1
  4. package/dist/lib/config.d.ts +19 -3
  5. package/dist/lib/config.js +154 -75
  6. package/dist/lib/config.js.map +1 -1
  7. package/dist/lib/container.d.ts +11 -9
  8. package/dist/lib/container.js +749 -282
  9. package/dist/lib/container.js.map +1 -1
  10. package/dist/lib/dataset-mcp.js +2 -9
  11. package/dist/lib/dataset-mcp.js.map +1 -1
  12. package/dist/lib/display-mcp.d.ts +2 -2
  13. package/dist/lib/display-mcp.js +17 -38
  14. package/dist/lib/display-mcp.js.map +1 -1
  15. package/dist/lib/doctor.js +8 -0
  16. package/dist/lib/doctor.js.map +1 -1
  17. package/dist/lib/explorer-claude.js +36 -1
  18. package/dist/lib/explorer-claude.js.map +1 -1
  19. package/dist/lib/explorer-eval.js +3 -2
  20. package/dist/lib/explorer-eval.js.map +1 -1
  21. package/dist/lib/image-pull-lock.d.ts +18 -0
  22. package/dist/lib/image-pull-lock.js +167 -0
  23. package/dist/lib/image-pull-lock.js.map +1 -0
  24. package/dist/lib/init.js +22 -19
  25. package/dist/lib/init.js.map +1 -1
  26. package/dist/lib/slurm-cli-passthrough.d.ts +12 -2
  27. package/dist/lib/slurm-cli-passthrough.js +401 -143
  28. package/dist/lib/slurm-cli-passthrough.js.map +1 -1
  29. package/dist/lib/startup-stage-lock.d.ts +21 -0
  30. package/dist/lib/startup-stage-lock.js +196 -0
  31. package/dist/lib/startup-stage-lock.js.map +1 -0
  32. package/dist/lib/ui.d.ts +40 -0
  33. package/dist/lib/ui.html +4953 -3366
  34. package/dist/lib/ui.js +1771 -297
  35. package/dist/lib/ui.js.map +1 -1
  36. package/dist/lib/web-terminal-startup-readiness.d.ts +8 -0
  37. package/dist/lib/web-terminal-startup-readiness.js +29 -0
  38. package/dist/lib/web-terminal-startup-readiness.js.map +1 -0
  39. package/dist/lib/web-terminal.d.ts +51 -0
  40. package/dist/lib/web-terminal.js +171 -1
  41. package/dist/lib/web-terminal.js.map +1 -1
  42. package/dist/mcp-bundles/dataset-mcp.bundle.mjs +144 -93
  43. package/dist/mcp-bundles/display-mcp.bundle.mjs +35 -43
  44. package/dist/mcp-bundles/explorer-mcp.bundle.mjs +263 -146
  45. package/dist/mcp-bundles/results-mcp.bundle.mjs +39 -41
  46. package/dist/mcp-bundles/slurm-mcp.bundle.mjs +19 -21
  47. package/package.json +1 -1
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.stageSlurmCliPassthrough = stageSlurmCliPassthrough;
4
4
  const child_process_1 = require("child_process");
5
+ const crypto_1 = require("crypto");
5
6
  const fs_1 = require("fs");
6
7
  const path_1 = require("path");
8
+ const os_1 = require("os");
7
9
  const config_js_1 = require("./config.js");
10
+ const startup_stage_lock_js_1 = require("./startup-stage-lock.js");
8
11
  const DEFAULT_SLURM_COMMANDS = [
9
12
  'srun',
10
13
  'sbatch',
@@ -14,6 +17,12 @@ const DEFAULT_SLURM_COMMANDS = [
14
17
  'sinfo',
15
18
  'scontrol',
16
19
  ];
20
+ const SLURM_STAGE_SCHEMA_VERSION = 3;
21
+ const SLURM_STAGE_CURRENT_LINK = 'current';
22
+ const SLURM_STAGE_STORE_DIR = '.stage-store';
23
+ const SLURM_STAGE_LOCK_DIR = 'startup-stage.lock';
24
+ const SLURM_STAGE_MANIFEST_FILE = 'manifest.json';
25
+ const SLURM_STAGE_ROOT_IN_CONTAINER = '/home/sandbox/.labgate/slurm/current';
17
26
  const SKIP_LIB_BASENAMES = new Set([
18
27
  'linux-vdso.so.1',
19
28
  'libc.so.6',
@@ -94,6 +103,77 @@ function copyFileBestEffort(src, dst) {
94
103
  // Best effort
95
104
  }
96
105
  }
106
+ function sha256File(path) {
107
+ const hash = (0, crypto_1.createHash)('sha256');
108
+ hash.update((0, fs_1.readFileSync)(path));
109
+ return hash.digest('hex');
110
+ }
111
+ function fingerprintPath(path, options = {}) {
112
+ try {
113
+ const st = (0, fs_1.statSync)(path);
114
+ if (!st.isFile())
115
+ return null;
116
+ const fingerprint = {
117
+ path,
118
+ size: st.size,
119
+ mtimeMs: Math.floor(st.mtimeMs),
120
+ };
121
+ if (options.hashContent) {
122
+ fingerprint.sha256 = sha256File(path);
123
+ }
124
+ return fingerprint;
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ function updateDirectoryHashRecursive(rootDir, relativeDir, hash) {
131
+ const dirPath = relativeDir ? (0, path_1.join)(rootDir, relativeDir) : rootDir;
132
+ let entryCount = 0;
133
+ const entries = (0, fs_1.readdirSync)(dirPath).sort((a, b) => a.localeCompare(b));
134
+ for (const entry of entries) {
135
+ const relativePath = relativeDir ? (0, path_1.join)(relativeDir, entry) : entry;
136
+ const normalizedRelativePath = relativePath.replace(/\\/g, '/');
137
+ const absPath = (0, path_1.join)(rootDir, relativePath);
138
+ const st = (0, fs_1.lstatSync)(absPath);
139
+ if (st.isDirectory()) {
140
+ hash.update(`dir:${normalizedRelativePath}\n`);
141
+ entryCount += 1 + updateDirectoryHashRecursive(rootDir, relativePath, hash);
142
+ continue;
143
+ }
144
+ if (st.isSymbolicLink()) {
145
+ hash.update(`link:${normalizedRelativePath}\n`);
146
+ hash.update((0, fs_1.readlinkSync)(absPath));
147
+ hash.update('\n');
148
+ entryCount += 1;
149
+ continue;
150
+ }
151
+ if (!st.isFile())
152
+ continue;
153
+ hash.update(`file:${normalizedRelativePath}:${st.size}:${Math.floor(st.mtimeMs)}\n`);
154
+ entryCount += 1;
155
+ }
156
+ return entryCount;
157
+ }
158
+ function fingerprintDirectory(path) {
159
+ if (!path)
160
+ return null;
161
+ try {
162
+ const st = (0, fs_1.statSync)(path);
163
+ if (!st.isDirectory())
164
+ return null;
165
+ const hash = (0, crypto_1.createHash)('sha256');
166
+ const entryCount = updateDirectoryHashRecursive(path, '', hash);
167
+ return {
168
+ path,
169
+ entryCount,
170
+ sha256: hash.digest('hex'),
171
+ };
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
97
177
  function collectLinkedLibs(filePath, out) {
98
178
  try {
99
179
  const lddOut = (0, child_process_1.execFileSync)('ldd', [filePath], { encoding: 'utf-8' });
@@ -195,9 +275,10 @@ function buildHostProxyClientScript() {
195
275
  'import { createConnection } from "net";',
196
276
  '',
197
277
  'const [socketPath, command, ...args] = process.argv.slice(2);',
278
+ 'const UNAVAILABLE_EXIT_CODE = 245;',
198
279
  'if (!socketPath || !command) {',
199
280
  ' console.error("[labgate] host proxy usage: <socket> <command> [args...]");',
200
- ' process.exit(2);',
281
+ ` process.exit(UNAVAILABLE_EXIT_CODE);`,
201
282
  '}',
202
283
  '',
203
284
  'const readStdin = async () => {',
@@ -223,7 +304,7 @@ function buildHostProxyClientScript() {
223
304
  ' sock.on("data", (d) => { raw += d; });',
224
305
  ' sock.on("error", (err) => {',
225
306
  ' console.error(`[labgate] host SLURM proxy error: ${err.message}`);',
226
- ' process.exit(127);',
307
+ ` process.exit(UNAVAILABLE_EXIT_CODE);`,
227
308
  ' });',
228
309
  ' sock.on("end", () => {',
229
310
  ' try {',
@@ -234,7 +315,7 @@ function buildHostProxyClientScript() {
234
315
  ' process.exit(code);',
235
316
  ' } catch (err) {',
236
317
  ' console.error(`[labgate] invalid proxy response: ${err?.message ?? String(err)}`);',
237
- ' process.exit(1);',
318
+ ` process.exit(UNAVAILABLE_EXIT_CODE);`,
238
319
  ' }',
239
320
  ' });',
240
321
  ' sock.write(JSON.stringify(req));',
@@ -243,17 +324,18 @@ function buildHostProxyClientScript() {
243
324
  '',
244
325
  'main().catch((err) => {',
245
326
  ' console.error(`[labgate] host SLURM proxy failure: ${err?.message ?? String(err)}`);',
246
- ' process.exit(1);',
327
+ ` process.exit(UNAVAILABLE_EXIT_CODE);`,
247
328
  '});',
248
329
  '',
249
330
  ].join('\n');
250
331
  }
251
- function buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInContainer) {
332
+ function buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInContainer, options = {}) {
333
+ const proxyOnly = options.proxyOnly ?? false;
252
334
  const real = `${stageRootInContainer}/bin/${cmd}`;
253
335
  const lib = `${stageRootInContainer}/lib`;
254
336
  const conf = `${stageRootInContainer}/etc/slurm.conf`;
255
337
  const proxyClient = `${stageRootInContainer}/host-proxy-client.mjs`;
256
- return [
338
+ const lines = [
257
339
  '#!/usr/bin/env bash',
258
340
  'set -euo pipefail',
259
341
  `REAL="${real}"`,
@@ -262,25 +344,102 @@ function buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInCon
262
344
  `PROXY_SOCKET="${hostProxySocketInContainer || ''}"`,
263
345
  `PROXY_CLIENT="${proxyClient}"`,
264
346
  'if [ -n "$PROXY_SOCKET" ] && [ -S "$PROXY_SOCKET" ] && [ -f "$PROXY_CLIENT" ] && command -v node >/dev/null 2>&1; then',
265
- ` if node "$PROXY_CLIENT" "$PROXY_SOCKET" "${cmd}" "$@"; then`,
347
+ ' if node "$PROXY_CLIENT" "$PROXY_SOCKET" "${cmd}" "$@"; then',
266
348
  ' exit 0',
349
+ ' else',
350
+ ' proxy_exit_code=$?',
351
+ ' if [ "$proxy_exit_code" -ne 245 ]; then',
352
+ ' exit "$proxy_exit_code"',
353
+ ' fi',
267
354
  ' fi',
268
- ' echo "[labgate] host SLURM proxy unavailable; falling back to staged CLI." >&2',
269
- 'fi',
270
- 'if [ ! -x "$REAL" ]; then',
271
- ` echo "[labgate] SLURM CLI '${cmd}' is not staged in this sandbox." >&2`,
272
- ' echo "[labgate] Fix: ensure SLURM is available on the host (e.g. login node), then restart the LabGate session." >&2',
273
- ' exit 127',
274
- 'fi',
275
- 'if [ -d "$LIBDIR" ]; then',
276
- ' export LD_LIBRARY_PATH="$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"',
277
- 'fi',
278
- 'if [ -z "${SLURM_CONF:-}" ] && [ -f "$CONF" ]; then',
279
- ' export SLURM_CONF="$CONF"',
280
- 'fi',
281
- 'exec "$REAL" "$@"',
282
- '',
283
- ].join('\n');
355
+ ];
356
+ if (proxyOnly) {
357
+ lines.push('fi', `echo "[labgate] host SLURM proxy unavailable for '${cmd}' in proxy-only mode." >&2`, 'echo "[labgate] Fix: ensure the LabGate host proxy is running and host SLURM commands are available, then restart the session." >&2', 'exit 127', '');
358
+ return lines.join('\n');
359
+ }
360
+ lines.push(' echo "[labgate] host SLURM proxy unavailable; falling back to staged CLI." >&2', 'fi', 'if [ ! -x "$REAL" ]; then', ` echo "[labgate] SLURM CLI '${cmd}' is not staged in this sandbox." >&2`, ' echo "[labgate] Fix: ensure SLURM is available on the host (e.g. login node), then restart the LabGate session." >&2', ' exit 127', 'fi', 'if [ -d "$LIBDIR" ]; then', ' export LD_LIBRARY_PATH="$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"', 'fi', 'if [ -z "${SLURM_CONF:-}" ] && [ -f "$CONF" ]; then', ' export SLURM_CONF="$CONF"', 'fi', 'exec "$REAL" "$@"', '');
361
+ return lines.join('\n');
362
+ }
363
+ function removeSlurmPath(path) {
364
+ try {
365
+ (0, fs_1.rmSync)(path, { recursive: true, force: true });
366
+ }
367
+ catch {
368
+ // Best effort cleanup.
369
+ }
370
+ }
371
+ function cleanupSlurmWrappers(wrapperDir, keep, candidates) {
372
+ const keepSet = new Set(keep);
373
+ for (const cmd of new Set(candidates)) {
374
+ if (keepSet.has(cmd))
375
+ continue;
376
+ removeSlurmPath((0, path_1.join)(wrapperDir, cmd));
377
+ }
378
+ }
379
+ function readSlurmStageManifest(stageRoot) {
380
+ try {
381
+ const raw = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(stageRoot, SLURM_STAGE_MANIFEST_FILE), 'utf-8'));
382
+ if (!raw || typeof raw !== 'object')
383
+ return null;
384
+ if (Number(raw.schema_version) !== SLURM_STAGE_SCHEMA_VERSION)
385
+ return null;
386
+ return raw;
387
+ }
388
+ catch {
389
+ return null;
390
+ }
391
+ }
392
+ function getSlurmStageLockPath(sandboxHome) {
393
+ return (0, path_1.join)(sandboxHome, '.labgate', 'locks', SLURM_STAGE_LOCK_DIR);
394
+ }
395
+ function computeStageFingerprint(input) {
396
+ return (0, crypto_1.createHash)('sha256').update(JSON.stringify(input)).digest('hex');
397
+ }
398
+ function ensureStageStoreLink(stageRoot, targetDir) {
399
+ const currentLink = (0, path_1.join)(stageRoot, SLURM_STAGE_CURRENT_LINK);
400
+ const tempLink = (0, path_1.join)(stageRoot, `.${SLURM_STAGE_CURRENT_LINK}.tmp-${process.pid}-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
401
+ let currentExists = false;
402
+ let currentIsSymlink = false;
403
+ try {
404
+ currentIsSymlink = (0, fs_1.lstatSync)(currentLink).isSymbolicLink();
405
+ currentExists = true;
406
+ }
407
+ catch {
408
+ currentExists = false;
409
+ currentIsSymlink = false;
410
+ }
411
+ (0, fs_1.symlinkSync)(targetDir, tempLink, 'dir');
412
+ if (currentExists && currentIsSymlink) {
413
+ try {
414
+ const currentTarget = (0, path_1.resolve)(stageRoot, (0, fs_1.readlinkSync)(currentLink));
415
+ if (currentTarget === targetDir) {
416
+ removeSlurmPath(tempLink);
417
+ return;
418
+ }
419
+ }
420
+ catch {
421
+ // Fall through to replace.
422
+ }
423
+ }
424
+ if (!currentExists || currentIsSymlink) {
425
+ (0, fs_1.renameSync)(tempLink, currentLink);
426
+ return;
427
+ }
428
+ const legacyDir = (0, path_1.join)(stageRoot, `.${SLURM_STAGE_CURRENT_LINK}.legacy-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
429
+ (0, fs_1.renameSync)(currentLink, legacyDir);
430
+ try {
431
+ (0, fs_1.renameSync)(tempLink, currentLink);
432
+ }
433
+ catch (err) {
434
+ (0, fs_1.renameSync)(legacyDir, currentLink);
435
+ throw err;
436
+ }
437
+ removeSlurmPath(legacyDir);
438
+ }
439
+ function cleanupLegacyStageLayout(stageRoot) {
440
+ for (const name of ['bin', 'lib', 'etc', 'plugins', 'host-proxy-client.mjs']) {
441
+ removeSlurmPath((0, path_1.join)(stageRoot, name));
442
+ }
284
443
  }
285
444
  function tryExtractPluginDir(slurmConfText) {
286
445
  for (const raw of slurmConfText.split('\n')) {
@@ -335,33 +494,35 @@ function rewriteDaemonUsers(slurmConfText) {
335
494
  * in $HOME/.labgate-bin so they are available inside the container even when the image
336
495
  * does not ship with SLURM.
337
496
  *
338
- * This is intentionally simple: copy binaries + ldd libraries + slurm.conf (if found).
339
- * The host-side session launcher is responsible for bind-mounting /run/munge when present.
497
+ * When host proxy mode is preferred, this writes proxy-only wrappers without paying the
498
+ * binary/plugin copy cost on every startup. Otherwise it stages binaries + ldd libraries
499
+ * + slurm.conf (if found). The host-side session launcher is responsible for bind-mounting
500
+ * /run/munge when present.
340
501
  */
341
502
  function stageSlurmCliPassthrough(options) {
342
503
  const env = options.env ?? process.env;
343
504
  const commands = options.commands ?? [...DEFAULT_SLURM_COMMANDS];
344
505
  const installMissingWrappers = options.installMissingWrappers ?? true;
345
506
  const hostProxySocketInContainer = options.hostProxySocketInContainer ?? null;
507
+ const preferHostProxy = options.preferHostProxy ?? false;
346
508
  const stageRoot = (0, path_1.join)(options.sandboxHome, '.labgate', 'slurm');
347
- const binDir = (0, path_1.join)(stageRoot, 'bin');
348
- const libDir = (0, path_1.join)(stageRoot, 'lib');
349
- const etcDir = (0, path_1.join)(stageRoot, 'etc');
509
+ const storeRoot = (0, path_1.join)(stageRoot, SLURM_STAGE_STORE_DIR);
350
510
  const wrapperDir = (0, path_1.join)(options.sandboxHome, '.labgate-bin');
351
- const errors = [];
352
- const staged = [];
511
+ const lockPath = getSlurmStageLockPath(options.sandboxHome);
353
512
  const skipped = [];
354
513
  try {
355
514
  (0, config_js_1.ensurePrivateDir)((0, path_1.join)(options.sandboxHome, '.labgate'));
515
+ (0, config_js_1.ensurePrivateDir)((0, path_1.join)(options.sandboxHome, '.labgate', 'locks'));
356
516
  (0, config_js_1.ensurePrivateDir)(stageRoot);
357
- (0, config_js_1.ensurePrivateDir)(binDir);
358
- (0, config_js_1.ensurePrivateDir)(libDir);
359
- (0, config_js_1.ensurePrivateDir)(etcDir);
517
+ (0, config_js_1.ensurePrivateDir)(storeRoot);
360
518
  (0, config_js_1.ensurePrivateDir)(wrapperDir);
361
519
  }
362
520
  catch (err) {
363
521
  return {
364
522
  ok: false,
523
+ mode: 'staged',
524
+ reused: false,
525
+ hostCommands: [],
365
526
  staged: [],
366
527
  skipped: commands,
367
528
  errors: [`Failed to initialize SLURM stage directories: ${err?.message ?? String(err)}`],
@@ -369,7 +530,7 @@ function stageSlurmCliPassthrough(options) {
369
530
  stageRoot,
370
531
  };
371
532
  }
372
- // 1) Resolve host command paths and copy binaries
533
+ // 1) Resolve host command paths.
373
534
  const hostPaths = new Map();
374
535
  for (const cmd of commands) {
375
536
  const hostPath = which(cmd, env);
@@ -378,134 +539,231 @@ function stageSlurmCliPassthrough(options) {
378
539
  continue;
379
540
  }
380
541
  hostPaths.set(cmd, hostPath);
381
- const target = (0, path_1.join)(binDir, cmd);
542
+ }
543
+ const hostCommands = Array.from(hostPaths.keys());
544
+ const proxyOnly = !!hostProxySocketInContainer && preferHostProxy && hostCommands.length > 0;
545
+ const sampleCmd = hostPaths.get('squeue') ?? hostPaths.get('scontrol') ?? hostPaths.get('sbatch') ?? null;
546
+ const slurmConfPath = proxyOnly ? null : resolveSlurmConf(sampleCmd, env);
547
+ let slurmConfText = '';
548
+ if (slurmConfPath) {
382
549
  try {
383
- (0, fs_1.copyFileSync)(hostPath, target);
384
- // Strip any unexpected setuid/setgid bits from host binaries.
385
- try {
386
- (0, fs_1.chmodSync)(target, 0o755);
387
- }
388
- catch { /* ignore */ }
389
- staged.push(cmd);
550
+ slurmConfText = (0, fs_1.readFileSync)(slurmConfPath, 'utf-8');
390
551
  }
391
- catch (err) {
392
- skipped.push(cmd);
393
- errors.push(`Failed to stage ${cmd} from ${hostPath}: ${err?.message ?? String(err)}`);
552
+ catch {
553
+ slurmConfText = '';
394
554
  }
395
555
  }
396
- // 2) Stage shared libraries for staged commands (best effort)
397
- const libsToCopy = new Set();
398
- for (const cmd of staged) {
556
+ const pluginDir = proxyOnly ? null : resolvePluginDir(sampleCmd, slurmConfText, env);
557
+ const slurmConfFingerprint = (proxyOnly || !slurmConfPath) ? null : fingerprintPath(slurmConfPath, { hashContent: true });
558
+ const pluginDirFingerprint = proxyOnly ? null : fingerprintDirectory(pluginDir);
559
+ const commandFingerprints = hostCommands
560
+ .map((cmd) => {
399
561
  const hostPath = hostPaths.get(cmd);
400
562
  if (!hostPath)
401
- continue;
402
- collectLinkedLibs(hostPath, libsToCopy);
403
- }
404
- // 3) Stage slurm.conf (best effort)
405
- const sampleCmd = hostPaths.get('squeue') ?? hostPaths.get('scontrol') ?? hostPaths.get('sbatch') ?? null;
406
- const slurmConfPath = resolveSlurmConf(sampleCmd, env);
407
- let stagedPluginDir = null;
408
- if (slurmConfPath) {
409
- try {
410
- const confText = (0, fs_1.readFileSync)(slurmConfPath, 'utf-8');
411
- const stageRootInContainer = '/home/sandbox/.labgate/slurm';
412
- const pluginDirInContainer = `${stageRootInContainer}/plugins`;
413
- let rewritten = rewritePluginDir(confText, pluginDirInContainer);
414
- rewritten = rewriteDaemonUsers(rewritten);
415
- const pluginDir = resolvePluginDir(sampleCmd, confText, env);
416
- (0, config_js_1.ensurePrivateDir)((0, path_1.join)(stageRoot, 'plugins'));
417
- if (pluginDir) {
563
+ return null;
564
+ const fp = fingerprintPath(hostPath);
565
+ if (!fp)
566
+ return null;
567
+ return { command: cmd, ...fp };
568
+ })
569
+ .filter((entry) => !!entry);
570
+ const fingerprint = computeStageFingerprint({
571
+ schema_version: SLURM_STAGE_SCHEMA_VERSION,
572
+ host: (0, os_1.hostname)(),
573
+ mode: proxyOnly ? 'proxy-only' : 'staged',
574
+ requested_commands: commands,
575
+ host_commands: commandFingerprints,
576
+ slurm_conf: slurmConfFingerprint,
577
+ plugin_dir: pluginDirFingerprint,
578
+ host_proxy_socket: hostProxySocketInContainer,
579
+ });
580
+ try {
581
+ return (0, startup_stage_lock_js_1.withStartupStageLock)(lockPath, 'slurm-stage', () => {
582
+ (0, config_js_1.ensurePrivateDir)(stageRoot);
583
+ (0, config_js_1.ensurePrivateDir)(storeRoot);
584
+ (0, config_js_1.ensurePrivateDir)(wrapperDir);
585
+ cleanupLegacyStageLayout(stageRoot);
586
+ const stageMode = proxyOnly ? 'proxy-only' : 'staged';
587
+ const storeId = fingerprint;
588
+ const storeDir = (0, path_1.join)(storeRoot, storeId);
589
+ const existingManifest = readSlurmStageManifest(stageRoot);
590
+ let reused = false;
591
+ let staged = [];
592
+ let errors = [];
593
+ let libCount = 0;
594
+ const reuseExistingStore = (existingManifest &&
595
+ existingManifest.fingerprint === fingerprint &&
596
+ existingManifest.mode === stageMode &&
597
+ existingManifest.store_id === storeId &&
598
+ (0, fs_1.existsSync)(storeDir));
599
+ if (!reuseExistingStore && !(0, fs_1.existsSync)(storeDir)) {
600
+ const tempStoreDir = (0, path_1.join)(storeRoot, `.tmp-${storeId}-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
601
+ removeSlurmPath(tempStoreDir);
602
+ (0, fs_1.mkdirSync)(tempStoreDir, { recursive: true, mode: 0o700 });
418
603
  try {
419
- // Copy plugin directory tree into the sandbox; many clusters set PluginDir in slurm.conf.
420
- const stagedPluginRoot = (0, path_1.join)(stageRoot, 'plugins');
421
- (0, fs_1.cpSync)(pluginDir, stagedPluginRoot, { recursive: true, force: true });
422
- stagedPluginDir = pluginDir;
423
- // Also stage transitive shared libs required by plugins (e.g. libmunge).
424
- for (const so of listSharedObjects(stagedPluginRoot)) {
425
- collectLinkedLibs(so, libsToCopy);
604
+ if (!proxyOnly) {
605
+ const binDir = (0, path_1.join)(tempStoreDir, 'bin');
606
+ const libDir = (0, path_1.join)(tempStoreDir, 'lib');
607
+ const etcDir = (0, path_1.join)(tempStoreDir, 'etc');
608
+ (0, fs_1.mkdirSync)(binDir, { recursive: true, mode: 0o700 });
609
+ (0, fs_1.mkdirSync)(libDir, { recursive: true, mode: 0o700 });
610
+ (0, fs_1.mkdirSync)(etcDir, { recursive: true, mode: 0o700 });
611
+ for (const [cmd, hostPath] of hostPaths) {
612
+ const target = (0, path_1.join)(binDir, cmd);
613
+ try {
614
+ (0, fs_1.copyFileSync)(hostPath, target);
615
+ try {
616
+ (0, fs_1.chmodSync)(target, 0o755);
617
+ }
618
+ catch { /* ignore */ }
619
+ staged.push(cmd);
620
+ }
621
+ catch (err) {
622
+ skipped.push(cmd);
623
+ errors.push(`Failed to stage ${cmd} from ${hostPath}: ${err?.message ?? String(err)}`);
624
+ }
625
+ }
626
+ const libsToCopy = new Set();
627
+ for (const cmd of staged) {
628
+ const hostPath = hostPaths.get(cmd);
629
+ if (!hostPath)
630
+ continue;
631
+ collectLinkedLibs(hostPath, libsToCopy);
632
+ }
633
+ if (slurmConfPath) {
634
+ try {
635
+ const pluginDirInContainer = `${SLURM_STAGE_ROOT_IN_CONTAINER}/plugins`;
636
+ let rewritten = rewritePluginDir(slurmConfText, pluginDirInContainer);
637
+ rewritten = rewriteDaemonUsers(rewritten);
638
+ const stagedPluginRoot = (0, path_1.join)(tempStoreDir, 'plugins');
639
+ (0, fs_1.mkdirSync)(stagedPluginRoot, { recursive: true, mode: 0o700 });
640
+ if (pluginDir) {
641
+ try {
642
+ (0, fs_1.cpSync)(pluginDir, stagedPluginRoot, { recursive: true, force: true });
643
+ for (const so of listSharedObjects(stagedPluginRoot)) {
644
+ collectLinkedLibs(so, libsToCopy);
645
+ }
646
+ }
647
+ catch (err) {
648
+ errors.push(`Failed to stage SLURM plugins from ${pluginDir}: ${err?.message ?? String(err)}`);
649
+ }
650
+ }
651
+ else {
652
+ errors.push('Could not detect a host SLURM plugin directory to stage. ' +
653
+ 'In-container SLURM commands may fail if plugins are required.');
654
+ }
655
+ (0, startup_stage_lock_js_1.writeTextFileAtomic)((0, path_1.join)(etcDir, 'slurm.conf'), `${rewritten.replace(/\n?$/, '\n')}`, { mode: 0o644 });
656
+ }
657
+ catch (err) {
658
+ errors.push(`Failed to stage slurm.conf from ${slurmConfPath}: ${err?.message ?? String(err)}`);
659
+ }
660
+ }
661
+ for (const lib of libsToCopy) {
662
+ const target = (0, path_1.join)(libDir, (0, path_1.basename)(lib));
663
+ if ((0, fs_1.existsSync)(target))
664
+ continue;
665
+ copyFileBestEffort(lib, target);
666
+ }
667
+ libCount = libsToCopy.size;
668
+ }
669
+ if (hostProxySocketInContainer) {
670
+ try {
671
+ (0, startup_stage_lock_js_1.writeTextFileAtomic)((0, path_1.join)(tempStoreDir, 'host-proxy-client.mjs'), `${buildHostProxyClientScript()}\n`, { mode: 0o755 });
672
+ }
673
+ catch (err) {
674
+ errors.push(`Failed to write host SLURM proxy client: ${err?.message ?? String(err)}`);
675
+ }
426
676
  }
677
+ (0, fs_1.renameSync)(tempStoreDir, storeDir);
427
678
  }
428
679
  catch (err) {
429
- errors.push(`Failed to stage SLURM plugins from ${pluginDir}: ${err?.message ?? String(err)}`);
680
+ removeSlurmPath(tempStoreDir);
681
+ throw err;
430
682
  }
431
683
  }
432
684
  else {
433
- errors.push('Could not detect a host SLURM plugin directory to stage. ' +
434
- 'In-container SLURM commands may fail if plugins are required.');
685
+ reused = true;
435
686
  }
436
- // Ensure staged slurm.conf always points to the in-container plugin path.
437
- try {
438
- if (!(0, fs_1.existsSync)((0, path_1.join)(stageRoot, 'plugins'))) {
439
- (0, config_js_1.ensurePrivateDir)((0, path_1.join)(stageRoot, 'plugins'));
687
+ if (reused && existingManifest) {
688
+ staged = [...existingManifest.commands];
689
+ errors = [...existingManifest.errors];
690
+ libCount = existingManifest.lib_count;
691
+ }
692
+ else if (reused) {
693
+ staged = proxyOnly ? [] : [...hostCommands];
694
+ }
695
+ const effectiveSkipped = reused && existingManifest
696
+ ? [...new Set([...skipped, ...existingManifest.skipped])]
697
+ : [...new Set(skipped)];
698
+ ensureStageStoreLink(stageRoot, storeDir);
699
+ const wrapperCommands = proxyOnly
700
+ ? hostCommands
701
+ : (installMissingWrappers ? commands : staged);
702
+ cleanupSlurmWrappers(wrapperDir, wrapperCommands, [...DEFAULT_SLURM_COMMANDS, ...commands]);
703
+ for (const cmd of wrapperCommands) {
704
+ const wrapperPath = (0, path_1.join)(wrapperDir, cmd);
705
+ try {
706
+ const script = buildSlurmWrapperScript(cmd, SLURM_STAGE_ROOT_IN_CONTAINER, hostProxySocketInContainer, { proxyOnly });
707
+ (0, startup_stage_lock_js_1.writeTextFileAtomic)(wrapperPath, `${script.replace(/\n?$/, '\n')}`, { mode: 0o755 });
708
+ }
709
+ catch (err) {
710
+ errors.push(`Failed to write wrapper for ${cmd}: ${err?.message ?? String(err)}`);
440
711
  }
441
712
  }
442
- catch { /* ignore */ }
443
- if (!stagedPluginDir && !pluginDir) {
444
- rewritten = rewritePluginDir(confText, pluginDirInContainer);
445
- rewritten = rewriteDaemonUsers(rewritten);
713
+ const manifest = {
714
+ schema_version: SLURM_STAGE_SCHEMA_VERSION,
715
+ fingerprint,
716
+ mode: stageMode,
717
+ host: (0, os_1.hostname)(),
718
+ requested_commands: [...commands],
719
+ host_commands: [...hostCommands],
720
+ commands: [...staged],
721
+ skipped: effectiveSkipped,
722
+ errors: [...errors],
723
+ slurm_conf: slurmConfFingerprint,
724
+ plugin_dir: pluginDirFingerprint,
725
+ host_proxy_socket: hostProxySocketInContainer,
726
+ lib_count: libCount,
727
+ store_id: storeId,
728
+ stage_root_in_container: SLURM_STAGE_ROOT_IN_CONTAINER,
729
+ staged_at: new Date().toISOString(),
730
+ notes: proxyOnly
731
+ ? 'Host SLURM CLI routed exclusively through the LabGate host proxy.'
732
+ : reused
733
+ ? 'Host SLURM CLI stage reused from cached manifest and fingerprint.'
734
+ : 'Host SLURM CLI staged by LabGate for in-container use.',
735
+ };
736
+ try {
737
+ (0, startup_stage_lock_js_1.writeTextFileAtomic)((0, path_1.join)(stageRoot, SLURM_STAGE_MANIFEST_FILE), JSON.stringify(manifest, null, 2) + '\n', { mode: 0o644 });
446
738
  }
447
- (0, fs_1.writeFileSync)((0, path_1.join)(etcDir, 'slurm.conf'), rewritten, { encoding: 'utf-8', mode: 0o644 });
448
- }
449
- catch (err) {
450
- errors.push(`Failed to stage slurm.conf from ${slurmConfPath}: ${err?.message ?? String(err)}`);
451
- }
452
- }
453
- // Copy all collected shared libraries (command + plugin deps).
454
- for (const lib of libsToCopy) {
455
- const target = (0, path_1.join)(libDir, (0, path_1.basename)(lib));
456
- if ((0, fs_1.existsSync)(target))
457
- continue;
458
- copyFileBestEffort(lib, target);
459
- }
460
- // 4) Install wrappers.
461
- // Legacy default is to install for all requested commands; optional mode
462
- // installs wrappers only for successfully staged commands.
463
- const stageRootInContainer = '/home/sandbox/.labgate/slurm';
464
- if (hostProxySocketInContainer) {
465
- try {
466
- (0, fs_1.writeFileSync)((0, path_1.join)(stageRoot, 'host-proxy-client.mjs'), buildHostProxyClientScript(), {
467
- encoding: 'utf-8',
468
- mode: 0o755,
469
- });
470
- }
471
- catch (err) {
472
- errors.push(`Failed to write host SLURM proxy client: ${err?.message ?? String(err)}`);
473
- }
474
- }
475
- const wrapperCommands = installMissingWrappers ? commands : staged;
476
- for (const cmd of wrapperCommands) {
477
- const wrapperPath = (0, path_1.join)(wrapperDir, cmd);
478
- try {
479
- const script = buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInContainer);
480
- (0, fs_1.writeFileSync)(wrapperPath, script, { encoding: 'utf-8', mode: 0o755 });
481
- }
482
- catch (err) {
483
- errors.push(`Failed to write wrapper for ${cmd}: ${err?.message ?? String(err)}`);
484
- }
739
+ catch {
740
+ // Best effort only.
741
+ }
742
+ return {
743
+ ok: proxyOnly ? hostCommands.length > 0 : staged.length > 0,
744
+ mode: stageMode,
745
+ reused,
746
+ hostCommands,
747
+ staged,
748
+ skipped: effectiveSkipped,
749
+ errors,
750
+ slurmConfPath: slurmConfFingerprint?.path ?? null,
751
+ stageRoot,
752
+ };
753
+ });
485
754
  }
486
- // 5) Write a small manifest for debugging
487
- try {
488
- const manifest = {
489
- staged_at: new Date().toISOString(),
490
- commands: staged,
491
- slurm_conf: slurmConfPath,
492
- plugin_dir: stagedPluginDir,
493
- host_proxy_socket: hostProxySocketInContainer,
494
- lib_count: libsToCopy.size,
495
- notes: 'Host SLURM CLI staged by LabGate for in-container use.',
755
+ catch (err) {
756
+ return {
757
+ ok: false,
758
+ mode: proxyOnly ? 'proxy-only' : 'staged',
759
+ reused: false,
760
+ hostCommands,
761
+ staged: [],
762
+ skipped: [...new Set(skipped)],
763
+ errors: [`SLURM passthrough staging failed: ${err?.message ?? String(err)}`],
764
+ slurmConfPath: slurmConfFingerprint?.path ?? null,
765
+ stageRoot,
496
766
  };
497
- (0, fs_1.writeFileSync)((0, path_1.join)(stageRoot, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
498
767
  }
499
- catch {
500
- // ignore
501
- }
502
- return {
503
- ok: staged.length > 0,
504
- staged,
505
- skipped,
506
- errors,
507
- slurmConfPath,
508
- stageRoot,
509
- };
510
768
  }
511
769
  //# sourceMappingURL=slurm-cli-passthrough.js.map