nebula-notebook 0.2.15 → 0.2.17

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 (56) hide show
  1. package/dist/assets/{errorwidget-B2XZ3hoa.js → errorwidget-t1RLHQYw.js} +1 -1
  2. package/dist/assets/{index-DqHzn4vg.js → index-BMuaZuus.js} +1 -1
  3. package/dist/assets/index-Bvhz0ltE.css +32 -0
  4. package/dist/assets/{index-NfxQwPBq.js → index-Cq1BG1_T.js} +205 -200
  5. package/dist/assets/{index-a9gKLTlv.js → index-DTGJj27s.js} +1 -1
  6. package/dist/assets/{index-CV0ZPmTH.js → index-DzeeB-u3.js} +1 -1
  7. package/dist/assets/{services-shim-D_SeYcB9.js → services-shim-Dg9Zi6xv.js} +1 -1
  8. package/dist/index.html +2 -2
  9. package/node-server/dist/auth/auth-middleware.js +3 -1
  10. package/node-server/dist/cluster/kernel-proxy.d.ts +15 -0
  11. package/node-server/dist/cluster/kernel-proxy.js +75 -0
  12. package/node-server/dist/fs/fs-service.js +104 -8
  13. package/node-server/dist/fs/index.d.ts +1 -0
  14. package/node-server/dist/fs/index.js +1 -0
  15. package/node-server/dist/fs/sealed-path.d.ts +41 -0
  16. package/node-server/dist/fs/sealed-path.js +228 -0
  17. package/node-server/dist/fs/types.d.ts +4 -2
  18. package/node-server/dist/fs/types.js +2 -2
  19. package/node-server/dist/index.js +24 -13
  20. package/node-server/dist/kernel/kernel-service.d.ts +2 -2
  21. package/node-server/dist/kernel/kernel-service.js +15 -5
  22. package/node-server/dist/kernel/types.d.ts +16 -0
  23. package/node-server/dist/notebook/headless-handler.d.ts +10 -0
  24. package/node-server/dist/notebook/headless-handler.js +64 -10
  25. package/node-server/dist/notebook/operation-router.js +13 -2
  26. package/node-server/dist/provenance/canonical-json.d.ts +36 -0
  27. package/node-server/dist/provenance/canonical-json.js +218 -0
  28. package/node-server/dist/provenance/provenance-store.d.ts +63 -0
  29. package/node-server/dist/provenance/provenance-store.js +598 -0
  30. package/node-server/dist/provenance/replay-seal-service.d.ts +223 -0
  31. package/node-server/dist/provenance/replay-seal-service.js +1702 -0
  32. package/node-server/dist/provenance/types.d.ts +65 -0
  33. package/node-server/dist/provenance/types.js +2 -0
  34. package/node-server/dist/routes/fs.js +27 -0
  35. package/node-server/dist/routes/kernel.js +15 -0
  36. package/node-server/dist/routes/notebook.js +54 -0
  37. package/node-server/dist/routes/replay-seal.d.ts +9 -0
  38. package/node-server/dist/routes/replay-seal.js +66 -0
  39. package/node-server/dist/scheduler/allocation-service.d.ts +3 -2
  40. package/node-server/dist/scheduler/allocation-service.js +45 -2
  41. package/node-server/dist/scheduler/arch.d.ts +30 -0
  42. package/node-server/dist/scheduler/arch.js +113 -0
  43. package/node-server/dist/scheduler/job-template.d.ts +11 -0
  44. package/node-server/dist/scheduler/mock-scheduler.d.ts +1 -0
  45. package/node-server/dist/scheduler/mock-scheduler.js +3 -0
  46. package/node-server/dist/scheduler/slurm-scheduler.d.ts +1 -0
  47. package/node-server/dist/scheduler/slurm-scheduler.js +34 -0
  48. package/node-server/dist/scheduler/types.d.ts +9 -0
  49. package/node-server/dist/scheduler/util.d.ts +6 -0
  50. package/node-server/dist/scheduler/util.js +16 -0
  51. package/node-server/dist/server/bind-host.d.ts +12 -0
  52. package/node-server/dist/server/bind-host.js +56 -0
  53. package/node-server/dist/server/cors-origin.d.ts +1 -0
  54. package/node-server/dist/server/cors-origin.js +32 -0
  55. package/package.json +1 -1
  56. package/dist/assets/index-Czch8hB-.css +0 -32
@@ -0,0 +1,65 @@
1
+ import type { CanonicalJsonValue } from './canonical-json';
2
+ export interface ProvenanceActor {
3
+ /** Authenticated principal category, for example user, agent, or system. */
4
+ kind: string;
5
+ /** Stable principal identifier within the actor category. */
6
+ id?: string;
7
+ /** Optional human-readable client/principal label. */
8
+ name?: string;
9
+ attributes?: Record<string, CanonicalJsonValue>;
10
+ }
11
+ export interface ProvenanceEventInput {
12
+ type: string;
13
+ actor: ProvenanceActor;
14
+ payload?: CanonicalJsonValue;
15
+ runId?: string;
16
+ taskId?: string;
17
+ sourceId?: string;
18
+ executionId?: string;
19
+ inputManifestSha256?: string;
20
+ environmentManifestSha256?: string;
21
+ idempotencyKey?: string;
22
+ }
23
+ export interface ProvenanceEvent {
24
+ schemaVersion: 1;
25
+ eventId: string;
26
+ seq: number;
27
+ timestamp: string;
28
+ notebookPath: string;
29
+ type: string;
30
+ actor: ProvenanceActor;
31
+ payload: CanonicalJsonValue;
32
+ runId?: string;
33
+ taskId?: string;
34
+ sourceId?: string;
35
+ executionId?: string;
36
+ inputManifestSha256?: string;
37
+ environmentManifestSha256?: string;
38
+ idempotencyKey?: string;
39
+ idempotencyRequestHash?: string;
40
+ prevHash: string | null;
41
+ eventHash: string;
42
+ }
43
+ export interface ProvenanceAppendResult {
44
+ event: ProvenanceEvent;
45
+ appended: boolean;
46
+ }
47
+ export interface ProvenanceVerification {
48
+ valid: boolean;
49
+ eventCount: number;
50
+ lastSeq: number;
51
+ headHash: string | null;
52
+ error?: string;
53
+ failedLine?: number;
54
+ }
55
+ export interface ProvenanceBlobReference {
56
+ sha256: string;
57
+ sizeBytes: number;
58
+ }
59
+ export interface ProvenanceBlobVerification {
60
+ valid: boolean;
61
+ expectedSha256: string;
62
+ actualSha256?: string;
63
+ sizeBytes?: number;
64
+ error?: string;
65
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -43,6 +43,12 @@ Object.defineProperty(exports, "fsService", { enumerable: true, get: function ()
43
43
  const path = __importStar(require("path"));
44
44
  const private_tmp_1 = require("../private-tmp");
45
45
  const nodeFs = __importStar(require("fs"));
46
+ const sealed_path_1 = require("../fs/sealed-path");
47
+ function sendSealedError(reply, error) {
48
+ if (!(error instanceof sealed_path_1.SealedPathLockedError))
49
+ return null;
50
+ return reply.code(error.statusCode).send((0, sealed_path_1.sealedErrorBody)(error));
51
+ }
46
52
  async function fsRoutes(fastify) {
47
53
  /**
48
54
  * List directory contents
@@ -176,6 +182,9 @@ async function fsRoutes(fastify) {
176
182
  return reply.send({ status: 'ok', path: filePath });
177
183
  }
178
184
  catch (err) {
185
+ const sealed = sendSealedError(reply, err);
186
+ if (sealed)
187
+ return sealed;
179
188
  const message = err instanceof Error ? err.message : 'Unknown error';
180
189
  return reply.code(500).send({ detail: message });
181
190
  }
@@ -193,6 +202,9 @@ async function fsRoutes(fastify) {
193
202
  return reply.send({ status: 'ok', file: info });
194
203
  }
195
204
  catch (err) {
205
+ const sealed = sendSealedError(reply, err);
206
+ if (sealed)
207
+ return sealed;
196
208
  if (err instanceof Error && err.message.includes('exists')) {
197
209
  return reply.code(409).send({ detail: err.message });
198
210
  }
@@ -215,6 +227,9 @@ async function fsRoutes(fastify) {
215
227
  return reply.send({ status: 'ok' });
216
228
  }
217
229
  catch (err) {
230
+ const sealed = sendSealedError(reply, err);
231
+ if (sealed)
232
+ return sealed;
218
233
  if (err instanceof Error && err.message.includes('not found')) {
219
234
  return reply.code(404).send({ detail: err.message });
220
235
  }
@@ -237,6 +252,9 @@ async function fsRoutes(fastify) {
237
252
  return reply.send({ status: 'ok', file: info });
238
253
  }
239
254
  catch (err) {
255
+ const sealed = sendSealedError(reply, err);
256
+ if (sealed)
257
+ return sealed;
240
258
  if (err instanceof Error) {
241
259
  if (err.message.includes('not found')) {
242
260
  return reply.code(404).send({ detail: err.message });
@@ -266,6 +284,9 @@ async function fsRoutes(fastify) {
266
284
  return reply.send({ status: 'ok', file: info });
267
285
  }
268
286
  catch (err) {
287
+ const sealed = sendSealedError(reply, err);
288
+ if (sealed)
289
+ return sealed;
269
290
  if (err instanceof Error) {
270
291
  if (err.message.includes('not found')) {
271
292
  return reply.code(404).send({ detail: err.message });
@@ -373,6 +394,9 @@ async function fsRoutes(fastify) {
373
394
  if (!destPath) {
374
395
  return reply.code(400).send({ detail: 'path is required' });
375
396
  }
397
+ // Reject sealed destinations before buffering the multipart payload to a
398
+ // temp file. FilesystemService repeats this at the actual write boundary.
399
+ (0, sealed_path_1.assertPathMutable)(fs_service_1.fsService.normalizePath(destPath), { operation: 'upload into' });
376
400
  // Save the uploaded file to a temp location first
377
401
  const tmpPath = path.join((0, private_tmp_1.privateTmpDir)('uploads'), `upload-${Date.now()}-${path.basename(data.filename || 'file')}`);
378
402
  const writeStream = nodeFs.createWriteStream(tmpPath);
@@ -395,6 +419,9 @@ async function fsRoutes(fastify) {
395
419
  return reply.send({ status: 'ok', file: info });
396
420
  }
397
421
  catch (err) {
422
+ const sealed = sendSealedError(reply, err);
423
+ if (sealed)
424
+ return sealed;
398
425
  if (err instanceof Error) {
399
426
  if (err.message.includes('already exists')) {
400
427
  return reply.code(409).send({ detail: err.message });
@@ -52,6 +52,7 @@ const operation_router_1 = require("../notebook/operation-router");
52
52
  const kernel_proxy_1 = require("../cluster/kernel-proxy");
53
53
  const server_registry_1 = require("../cluster/server-registry");
54
54
  const allocation_service_1 = require("../scheduler/allocation-service");
55
+ const sealed_path_1 = require("../fs/sealed-path");
55
56
  // Track all WebSocket connections per kernel session for broadcasting
56
57
  const sessionWebSockets = new Map();
57
58
  // Only send streaming outputs to sockets after they've performed an initial output sync.
@@ -138,6 +139,12 @@ function sendProvisionError(reply, err) {
138
139
  reply.code(status).send({ detail: err.message, code: err.code, install_hint: err.installHint });
139
140
  return true;
140
141
  }
142
+ function sendSealedError(reply, err) {
143
+ if (!(err instanceof sealed_path_1.SealedPathLockedError))
144
+ return false;
145
+ reply.code(err.statusCode).send((0, sealed_path_1.sealedErrorBody)(err));
146
+ return true;
147
+ }
141
148
  async function persistNotebookKernelMetadata(filePath, kernelName) {
142
149
  // Best-effort: persisting the kernel choice into the notebook's metadata is a
143
150
  // secondary side-effect. If it fails (e.g. the notebook file doesn't exist yet,
@@ -484,6 +491,9 @@ async function kernelRoutes(fastify) {
484
491
  fastify.post('/kernels/start', async (request, reply) => {
485
492
  try {
486
493
  const { kernel_name = 'python3', cwd, file_path, server_id, client_origin } = request.body;
494
+ if (file_path) {
495
+ (0, sealed_path_1.assertPathMutable)(kernelService.normalizeNotebookPath(file_path), { operation: 'start a kernel for' });
496
+ }
487
497
  const localServerId = server_registry_1.serverRegistry.getLocalServerId();
488
498
  // Check if we should start on a remote server
489
499
  if (server_id && server_id !== localServerId && server_id !== 'local') {
@@ -518,6 +528,8 @@ async function kernelRoutes(fastify) {
518
528
  return reply.send({ session_id: sessionId, kernel_name, server_id: localServerId, mtime: notebookMtime });
519
529
  }
520
530
  catch (err) {
531
+ if (sendSealedError(reply, err))
532
+ return reply;
521
533
  if (sendProvisionError(reply, err))
522
534
  return reply;
523
535
  const message = err instanceof Error ? err.message : 'Unknown error';
@@ -553,6 +565,7 @@ async function kernelRoutes(fastify) {
553
565
  return reply.code(400).send({ detail: 'file_path is required' });
554
566
  }
555
567
  const normalizedFilePath = kernelService.normalizeNotebookPath(file_path);
568
+ (0, sealed_path_1.assertPathMutable)(normalizedFilePath, { operation: 'start a kernel for' });
556
569
  let effectiveKernelName = kernel_name;
557
570
  let effectiveServerId = server_id;
558
571
  const localServerId = server_registry_1.serverRegistry.getLocalServerId();
@@ -635,6 +648,8 @@ async function kernelRoutes(fastify) {
635
648
  });
636
649
  }
637
650
  catch (err) {
651
+ if (sendSealedError(reply, err))
652
+ return reply;
638
653
  if (sendProvisionError(reply, err))
639
654
  return reply;
640
655
  const message = err instanceof Error ? err.message : 'Unknown error';
@@ -2,6 +2,9 @@
2
2
  /**
3
3
  * Notebook API Routes
4
4
  */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
5
8
  Object.defineProperty(exports, "__esModule", { value: true });
6
9
  exports.headlessHandler = exports.operationRouter = exports.fsService = void 0;
7
10
  exports.default = notebookRoutes;
@@ -11,11 +14,19 @@ const operation_router_1 = require("../notebook/operation-router");
11
14
  Object.defineProperty(exports, "operationRouter", { enumerable: true, get: function () { return operation_router_1.operationRouter; } });
12
15
  const headless_handler_1 = require("../notebook/headless-handler");
13
16
  const kernel_1 = require("./kernel");
17
+ const sealed_path_1 = require("../fs/sealed-path");
18
+ const replay_seal_1 = __importDefault(require("./replay-seal"));
14
19
  // Initialize headless handler with kernel service for cell execution
15
20
  const headlessHandler = new headless_handler_1.HeadlessOperationHandler(fs_service_1.fsService, operation_router_1.operationRouter, kernel_1.kernelService);
16
21
  exports.headlessHandler = headlessHandler;
17
22
  operation_router_1.operationRouter.setHeadlessHandler(headlessHandler);
23
+ function sendSealedError(reply, error) {
24
+ if (!(error instanceof sealed_path_1.SealedPathLockedError))
25
+ return null;
26
+ return reply.code(error.statusCode).send((0, sealed_path_1.sealedErrorBody)(error));
27
+ }
18
28
  async function notebookRoutes(fastify) {
29
+ await fastify.register(replay_seal_1.default);
19
30
  /**
20
31
  * Get cell metadata schema
21
32
  */
@@ -55,6 +66,16 @@ async function notebookRoutes(fastify) {
55
66
  return reply.code(400).send({ detail: 'path query parameter is required' });
56
67
  }
57
68
  const normalizedPath = fs_service_1.fsService.normalizePath(filePath);
69
+ const sealInfo = (0, sealed_path_1.classifySealedPath)(normalizedPath);
70
+ const sealHint = request.query.seal;
71
+ if (sealHint && (!sealInfo.sealed || sealInfo.sealId !== sealHint)) {
72
+ return reply.code(409).send({
73
+ code: 'seal_mismatch',
74
+ detail: sealInfo.sealed
75
+ ? `Seal hint ${sealHint} does not match canonical path seal ${sealInfo.sealId}`
76
+ : `Seal hint ${sealHint} does not identify the requested notebook path`,
77
+ });
78
+ }
58
79
  const result = await fs_service_1.fsService.getNotebookCellsWithKernel(filePath);
59
80
  return reply.send({
60
81
  path: normalizedPath,
@@ -62,6 +83,9 @@ async function notebookRoutes(fastify) {
62
83
  metadata: result.metadata,
63
84
  kernelspec: result.kernelspec,
64
85
  mtime: result.mtime,
86
+ access: sealInfo.sealed
87
+ ? { read_only: true, reason: 'sealed', seal_id: sealInfo.sealId }
88
+ : { read_only: false },
65
89
  });
66
90
  }
67
91
  catch (err) {
@@ -102,6 +126,9 @@ async function notebookRoutes(fastify) {
102
126
  return reply.send({ status: 'ok', path: filePath, mtime: result.mtime });
103
127
  }
104
128
  catch (err) {
129
+ const sealed = sendSealedError(reply, err);
130
+ if (sealed)
131
+ return sealed;
105
132
  const message = err instanceof Error ? err.message : 'Unknown error';
106
133
  return reply.code(500).send({ detail: message });
107
134
  }
@@ -119,6 +146,9 @@ async function notebookRoutes(fastify) {
119
146
  return reply.send({ notebook_path: notebookPath, history });
120
147
  }
121
148
  catch (err) {
149
+ const sealed = sendSealedError(reply, err);
150
+ if (sealed)
151
+ return sealed;
122
152
  const message = err instanceof Error ? err.message : 'Unknown error';
123
153
  return reply.code(500).send({ detail: message });
124
154
  }
@@ -136,6 +166,9 @@ async function notebookRoutes(fastify) {
136
166
  return reply.send({ status: 'ok', notebook_path });
137
167
  }
138
168
  catch (err) {
169
+ const sealed = sendSealedError(reply, err);
170
+ if (sealed)
171
+ return sealed;
139
172
  const message = err instanceof Error ? err.message : 'Unknown error';
140
173
  return reply.code(500).send({ detail: message });
141
174
  }
@@ -170,6 +203,9 @@ async function notebookRoutes(fastify) {
170
203
  return reply.send({ status: 'ok', notebook_path });
171
204
  }
172
205
  catch (err) {
206
+ const sealed = sendSealedError(reply, err);
207
+ if (sealed)
208
+ return sealed;
173
209
  const message = err instanceof Error ? err.message : 'Unknown error';
174
210
  return reply.code(500).send({ detail: message });
175
211
  }
@@ -195,6 +231,9 @@ async function notebookRoutes(fastify) {
195
231
  });
196
232
  }
197
233
  catch (err) {
234
+ const sealed = sendSealedError(reply, err);
235
+ if (sealed)
236
+ return sealed;
198
237
  const message = err instanceof Error ? err.message : 'Unknown error';
199
238
  return reply.code(500).send({ detail: message });
200
239
  }
@@ -272,9 +311,15 @@ async function notebookRoutes(fastify) {
272
311
  return reply.code(400).send({ detail: 'Operation with type is required' });
273
312
  }
274
313
  const result = await operation_router_1.operationRouter.applyOperation(operation);
314
+ if (result.code === 'sealed_read_only') {
315
+ return reply.code(403).send(result);
316
+ }
275
317
  return reply.send(result);
276
318
  }
277
319
  catch (err) {
320
+ const sealed = sendSealedError(reply, err);
321
+ if (sealed)
322
+ return sealed;
278
323
  const message = err instanceof Error ? err.message : 'Unknown error';
279
324
  return reply.send({ success: false, error: message });
280
325
  }
@@ -297,6 +342,9 @@ async function notebookRoutes(fastify) {
297
342
  });
298
343
  }
299
344
  catch (err) {
345
+ const sealed = sendSealedError(reply, err);
346
+ if (sealed)
347
+ return sealed;
300
348
  const message = err instanceof Error ? err.message : 'Unknown error';
301
349
  return reply.code(500).send({ detail: message });
302
350
  }
@@ -341,6 +389,9 @@ async function notebookRoutes(fastify) {
341
389
  });
342
390
  }
343
391
  catch (err) {
392
+ const sealed = sendSealedError(reply, err);
393
+ if (sealed)
394
+ return sealed;
344
395
  const message = err instanceof Error ? err.message : 'Unknown error';
345
396
  return reply.code(500).send({ detail: message });
346
397
  }
@@ -368,6 +419,9 @@ async function notebookRoutes(fastify) {
368
419
  return reply.send({ status: 'ok', notebook_path: filePath, mtime: updateResult.mtime });
369
420
  }
370
421
  catch (err) {
422
+ const sealed = sendSealedError(reply, err);
423
+ if (sealed)
424
+ return sealed;
371
425
  const message = err instanceof Error ? err.message : 'Unknown error';
372
426
  return reply.code(500).send({ detail: message });
373
427
  }
@@ -0,0 +1,9 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { type ReplaySealRequest, type ReplaySealStatusResponse } from '../provenance/replay-seal-service';
3
+ export interface ReplaySealRouteService {
4
+ submit(request: ReplaySealRequest, idempotencyKey: string): Promise<ReplaySealStatusResponse>;
5
+ getStatus(sealId: string): Promise<ReplaySealStatusResponse | null>;
6
+ }
7
+ /** Build the protected KOSMOS replay/seal routes with an injectable service. */
8
+ export declare function createReplaySealRoutes(service: ReplaySealRouteService): (fastify: FastifyInstance) => Promise<void>;
9
+ export default function replaySealRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createReplaySealRoutes = createReplaySealRoutes;
4
+ exports.default = replaySealRoutes;
5
+ const fs_service_1 = require("../fs/fs-service");
6
+ const replay_seal_service_1 = require("../provenance/replay-seal-service");
7
+ const provenance_store_1 = require("../provenance/provenance-store");
8
+ const kernel_1 = require("./kernel");
9
+ let productionService;
10
+ function getProductionService() {
11
+ productionService ??= new replay_seal_service_1.ReplaySealService({
12
+ rootDirectory: fs_service_1.fsService.getRootDirectory(),
13
+ kernelService: kernel_1.kernelService,
14
+ provenanceStore: new provenance_store_1.ProvenanceStore(),
15
+ });
16
+ return productionService;
17
+ }
18
+ function errorResponse(reply, error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ const code = error && typeof error === 'object' ? error.code : undefined;
21
+ if (error instanceof replay_seal_service_1.ReplaySealConflictError || code === 'IDEMPOTENCY_CONFLICT') {
22
+ return reply.code(409).send({ detail: message, code: 'idempotency_conflict' });
23
+ }
24
+ if (error instanceof replay_seal_service_1.ReplaySealValidationError || code === 'REPLAY_SEAL_VALIDATION_ERROR') {
25
+ return reply.code(400).send({ detail: message, code: 'invalid_replay_seal_request' });
26
+ }
27
+ return reply.code(500).send({ detail: message });
28
+ }
29
+ /** Build the protected KOSMOS replay/seal routes with an injectable service. */
30
+ function createReplaySealRoutes(service) {
31
+ return async function replaySealRoutes(fastify) {
32
+ fastify.post('/notebook/replay-seal', async (request, reply) => {
33
+ const header = request.headers['idempotency-key'];
34
+ const idempotencyKey = Array.isArray(header) ? header[0] : header;
35
+ if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0) {
36
+ return reply.code(400).send({ detail: 'Idempotency-Key header is required' });
37
+ }
38
+ try {
39
+ const status = await service.submit(request.body, idempotencyKey);
40
+ return reply.code(status.status === 'pending' || status.status === 'running' ? 202 : 200)
41
+ .send(status);
42
+ }
43
+ catch (error) {
44
+ return errorResponse(reply, error);
45
+ }
46
+ });
47
+ fastify.get('/notebook/replay-seal/:sealId', async (request, reply) => {
48
+ try {
49
+ const sealId = request.params.sealId;
50
+ if (typeof sealId !== 'string' || sealId.length === 0) {
51
+ return reply.code(400).send({ detail: 'sealId is required' });
52
+ }
53
+ const status = await service.getStatus(sealId);
54
+ if (!status)
55
+ return reply.code(404).send({ detail: `Replay seal not found: ${sealId}` });
56
+ return reply.send(status);
57
+ }
58
+ catch (error) {
59
+ return errorResponse(reply, error);
60
+ }
61
+ });
62
+ };
63
+ }
64
+ async function replaySealRoutes(fastify) {
65
+ await createReplaySealRoutes(getProductionService())(fastify);
66
+ }
@@ -23,7 +23,7 @@ export interface Allocation {
23
23
  createdAt: number;
24
24
  walltimeEndsAt?: number;
25
25
  }
26
- declare class AllocationService {
26
+ export declare class AllocationService {
27
27
  private scheduler;
28
28
  private ctx;
29
29
  private allocations;
@@ -57,7 +57,8 @@ declare class AllocationService {
57
57
  create(spec: JobSpec): Promise<Allocation>;
58
58
  cancel(id: string): Promise<boolean>;
59
59
  private poll;
60
+ /** Last few lines of an allocation's job log (bounded read), or null. */
61
+ private readLogTail;
60
62
  shutdown(): void;
61
63
  }
62
64
  export declare const allocationService: AllocationService;
63
- export {};
@@ -43,11 +43,13 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.allocationService = void 0;
46
+ exports.allocationService = exports.AllocationService = void 0;
47
47
  const crypto_1 = require("crypto");
48
48
  const fs = __importStar(require("fs"));
49
49
  const path = __importStar(require("path"));
50
50
  const job_template_1 = require("./job-template");
51
+ const arch_1 = require("./arch");
52
+ const util_1 = require("./util");
51
53
  const server_registry_1 = require("../cluster/server-registry");
52
54
  // Adaptive polling: fast only while a transition is imminent (job climbing
53
55
  // the queue / server booting), slow once allocations are correlated and
@@ -192,7 +194,18 @@ class AllocationService {
192
194
  const id = (0, crypto_1.randomUUID)().slice(0, 8);
193
195
  const token = (0, crypto_1.randomUUID)();
194
196
  const alloc = { id, token, spec, state: 'pending', createdAt: Date.now() };
195
- const script = (0, job_template_1.renderJobScript)(spec, this.ctx, id, token);
197
+ // Cross-arch partitions (aarch64 queues under an x86_64 server) launch a
198
+ // per-arch runtime — or refuse HERE, actionably, instead of letting the
199
+ // job die on the node with "Exec format error" in an unread log.
200
+ let targetArch = null;
201
+ try {
202
+ targetArch = await this.scheduler.partitionArch(spec.partition);
203
+ }
204
+ catch {
205
+ targetArch = null;
206
+ }
207
+ const launchCtx = (0, arch_1.pickLaunchContext)(this.ctx, targetArch, this.ctx.serverArch);
208
+ const script = (0, job_template_1.renderJobScript)(spec, launchCtx, id, token);
196
209
  const scriptPath = path.join(this.ctx.stateDir, `${id}.sh`);
197
210
  fs.writeFileSync(scriptPath, script, { mode: 0o700 });
198
211
  const { jobId } = await this.scheduler.submit(scriptPath);
@@ -266,6 +279,13 @@ class AllocationService {
266
279
  else if (['completed', 'cancelled', 'failed'].includes(status.state)) {
267
280
  alloc.state = status.state === 'failed' ? 'failed' : status.state === 'cancelled' ? 'cancelled' : 'ended';
268
281
  alloc.reason = status.reason;
282
+ // A failure (or an exit that never registered) explains itself in the
283
+ // job log; the scheduler's reason alone ("NonZeroExitCode") does not.
284
+ if (alloc.state === 'failed' || (alloc.state === 'ended' && !alloc.serverId)) {
285
+ const tail = this.readLogTail(alloc.id);
286
+ if (tail)
287
+ alloc.reason = [status.reason, tail].filter(Boolean).join(' — ');
288
+ }
269
289
  dirty = true;
270
290
  if (alloc.serverId)
271
291
  server_registry_1.serverRegistry.unregister(alloc.serverId);
@@ -275,6 +295,28 @@ class AllocationService {
275
295
  if (dirty)
276
296
  this.persist();
277
297
  }
298
+ /** Last few lines of an allocation's job log (bounded read), or null. */
299
+ readLogTail(allocId) {
300
+ if (!this.ctx)
301
+ return null;
302
+ const file = path.join(this.ctx.stateDir, `${allocId}.log`);
303
+ try {
304
+ const size = fs.statSync(file).size;
305
+ const fd = fs.openSync(file, 'r');
306
+ try {
307
+ const want = Math.min(size, 4096);
308
+ const buf = Buffer.alloc(want);
309
+ fs.readSync(fd, buf, 0, want, size - want);
310
+ return (0, util_1.summarizeLogTail)(buf.toString('utf-8'));
311
+ }
312
+ finally {
313
+ fs.closeSync(fd);
314
+ }
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ }
278
320
  shutdown() {
279
321
  if (this.pollTimer) {
280
322
  clearTimeout(this.pollTimer);
@@ -282,4 +324,5 @@ class AllocationService {
282
324
  }
283
325
  }
284
326
  }
327
+ exports.AllocationService = AllocationService;
285
328
  exports.allocationService = new AllocationService();
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Multi-arch launch support for compute allocations.
3
+ *
4
+ * An allocation re-launches THIS Nebula install on the compute node, so the
5
+ * node binary and node_modules must match the compute node's CPU arch. On
6
+ * heterogeneous clusters (CRI: x86_64 login nodes, aarch64 ghq/pearsonq) a
7
+ * per-arch runtime is configured via env:
8
+ *
9
+ * NEBULA_ARM64_NODE_BIN=/shared/node22-arm64/bin/node
10
+ * NEBULA_ARM64_DIR=/shared/nebula-notebook-arm64 # checkout with arm64 node_modules
11
+ *
12
+ * Both must live on storage the compute nodes share with the login node.
13
+ */
14
+ import type { LaunchContext } from './job-template';
15
+ /** SLURM spellings (uname -m) → node's process.arch identifiers. */
16
+ export declare function normalizeArch(raw: string): string;
17
+ /**
18
+ * Read per-arch runtime overrides from the environment. An arch is configured
19
+ * only when BOTH its vars are present — a node binary without its matching
20
+ * node_modules tree (or vice versa) would just fail later and worse.
21
+ */
22
+ export declare function archOverridesFromEnv(env?: Record<string, string | undefined>): NonNullable<LaunchContext['archOverrides']>;
23
+ /**
24
+ * Pick the launch context for a partition's arch: the server's own install
25
+ * when arches match (or the arch is unknown — the status quo), the configured
26
+ * override when they differ, and a loud, actionable refusal otherwise. The
27
+ * refusal is the point: without it the job dies on the compute node with
28
+ * "Exec format error" in a log nobody reads.
29
+ */
30
+ export declare function pickLaunchContext(ctx: LaunchContext, targetArchRaw: string | null, serverArch?: string): LaunchContext;