nebula-notebook 0.2.0 → 0.2.2

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 (100) hide show
  1. package/README.md +35 -20
  2. package/dist/assets/{errorwidget-C4r2j2DQ.js → errorwidget-Wy5ajPiM.js} +1 -1
  3. package/dist/assets/{index-Day3QcNs.js → index-B_Bs5koo.js} +1 -1
  4. package/dist/assets/index-DK4nS74r.js +829 -0
  5. package/dist/assets/{index-D5w21_Z8.js → index-DeQN03nW.js} +1 -1
  6. package/dist/assets/index-DzjpmUak.css +32 -0
  7. package/dist/assets/{index-CsHoPQy-.js → index-g6MjT-VG.js} +1 -1
  8. package/dist/assets/{services-shim-D6p_A67v.js → services-shim-BirHVsih.js} +1 -1
  9. package/dist/index.html +2 -2
  10. package/node-server/dist/auth/auth-middleware.js +22 -4
  11. package/node-server/dist/auth/auth-service.js +37 -7
  12. package/node-server/dist/cluster/client-registration.d.ts +12 -1
  13. package/node-server/dist/cluster/client-registration.js +35 -2
  14. package/node-server/dist/cluster/kernel-proxy.js +19 -3
  15. package/node-server/dist/cluster/server-registry.d.ts +9 -0
  16. package/node-server/dist/cluster/server-registry.js +26 -0
  17. package/node-server/dist/discovery/conda-locations.d.ts +73 -0
  18. package/node-server/dist/discovery/conda-locations.js +427 -0
  19. package/node-server/dist/discovery/discovery-service.d.ts +73 -4
  20. package/node-server/dist/discovery/discovery-service.js +211 -127
  21. package/node-server/dist/discovery/types.d.ts +0 -1
  22. package/node-server/dist/discovery/types.js +1 -2
  23. package/node-server/dist/environment.d.ts +12 -0
  24. package/node-server/dist/environment.js +104 -0
  25. package/node-server/dist/fs/fs-service.d.ts +31 -1
  26. package/node-server/dist/fs/fs-service.js +160 -28
  27. package/node-server/dist/fs/types.d.ts +9 -0
  28. package/node-server/dist/fs/types.js +8 -0
  29. package/node-server/dist/index.js +71 -4
  30. package/node-server/dist/kernel/kernel-service.d.ts +17 -0
  31. package/node-server/dist/kernel/kernel-service.js +72 -4
  32. package/node-server/dist/kernel/kernelspec.d.ts +27 -0
  33. package/node-server/dist/kernel/kernelspec.js +75 -4
  34. package/node-server/dist/notebook/headless-handler.d.ts +10 -0
  35. package/node-server/dist/notebook/headless-handler.js +55 -5
  36. package/node-server/dist/notebook/undoRedoManager.d.ts +8 -0
  37. package/node-server/dist/notebook/undoRedoManager.js +39 -16
  38. package/node-server/dist/private-tmp.d.ts +14 -0
  39. package/node-server/dist/private-tmp.js +70 -0
  40. package/node-server/dist/routes/autocomplete.d.ts +19 -0
  41. package/node-server/dist/routes/autocomplete.js +294 -0
  42. package/node-server/dist/routes/fs.js +21 -4
  43. package/node-server/dist/routes/kernel.js +45 -2
  44. package/node-server/dist/routes/notebook.js +8 -2
  45. package/node-server/dist/routes/python.d.ts +1 -2
  46. package/node-server/dist/routes/python.js +139 -3
  47. package/node-server/dist/scheduler/allocation-service.d.ts +20 -0
  48. package/node-server/dist/scheduler/allocation-service.js +124 -8
  49. package/node-server/dist/scheduler/mock-scheduler.js +7 -3
  50. package/node-server/dist/scheduler/slurm-scheduler.js +21 -8
  51. package/node-server/dist/scheduler/types.d.ts +3 -2
  52. package/node-server/dist/terminal/agent-registry.d.ts +87 -0
  53. package/node-server/dist/terminal/agent-registry.js +246 -0
  54. package/node-server/dist/terminal/binding-store.d.ts +57 -0
  55. package/node-server/dist/terminal/binding-store.js +0 -0
  56. package/node-server/dist/terminal/pty-manager.d.ts +23 -6
  57. package/node-server/dist/terminal/pty-manager.js +130 -19
  58. package/node-server/dist/terminal/server.js +112 -21
  59. package/node-server/dist/terminal/types.d.ts +2 -0
  60. package/node-server/package.json +1 -0
  61. package/node_modules/nebula-autocomplete/README.md +145 -0
  62. package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
  63. package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
  64. package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
  65. package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
  66. package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
  67. package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
  68. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
  69. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
  70. package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
  71. package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
  72. package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
  73. package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
  74. package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
  75. package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
  76. package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
  77. package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
  78. package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
  79. package/node_modules/nebula-autocomplete/dist/index.js +5 -0
  80. package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
  81. package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
  82. package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
  83. package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
  84. package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
  85. package/node_modules/nebula-autocomplete/dist/types.js +1 -0
  86. package/node_modules/nebula-autocomplete/package.json +58 -0
  87. package/node_modules/nebula-autocomplete/server.d.ts +3 -0
  88. package/node_modules/nebula-autocomplete/server.js +3 -0
  89. package/package.json +17 -5
  90. package/scripts/postinstall.cjs +25 -5
  91. package/dist/assets/index-7-YBurka.js +0 -716
  92. package/dist/assets/index-CFBUnxSZ.css +0 -32
  93. package/node-server/dist/llm/index.d.ts +0 -5
  94. package/node-server/dist/llm/index.js +0 -21
  95. package/node-server/dist/llm/llm-service.d.ts +0 -77
  96. package/node-server/dist/llm/llm-service.js +0 -454
  97. package/node-server/dist/llm/types.d.ts +0 -40
  98. package/node-server/dist/llm/types.js +0 -15
  99. package/node-server/dist/routes/llm.d.ts +0 -8
  100. package/node-server/dist/routes/llm.js +0 -105
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.kernelService = exports.KernelService = exports.IOPUB_QUEUE_TIMEOUT = exports.IOPUB_QUEUE_CLOSED = void 0;
43
+ exports.rewriteShellLines = rewriteShellLines;
43
44
  const fs = __importStar(require("fs"));
44
45
  const path = __importStar(require("path"));
45
46
  const os = __importStar(require("os"));
@@ -53,7 +54,10 @@ const types_1 = require("./types");
53
54
  const display_data_1 = require("../output/display-data");
54
55
  const output_limits_1 = require("../config/output-limits");
55
56
  const session_store_1 = require("./session-store");
57
+ const private_tmp_1 = require("../private-tmp");
56
58
  const kernelspec_1 = require("./kernelspec");
59
+ const discovery_service_1 = require("../discovery/discovery-service");
60
+ const types_2 = require("../discovery/types");
57
61
  /** Resolved by BoundedAsyncQueue.next() when the queue has been closed. */
58
62
  exports.IOPUB_QUEUE_CLOSED = Symbol('iopub-queue-closed');
59
63
  /** Resolved by BoundedAsyncQueue.next(timeoutMs) when the wait timed out. */
@@ -139,6 +143,41 @@ class BoundedAsyncQueue {
139
143
  }
140
144
  }
141
145
  }
146
+ /**
147
+ * Polyfill IPython-style `!command` lines for kernels whose language lacks
148
+ * them, by rewriting each such line into the language's own shell-call idiom
149
+ * before the code reaches the kernel. Because the rewritten code still runs
150
+ * in the kernel process, cwd, environment (conda env, loaded modules), and
151
+ * machine (allocation node) are all the kernel's — the same guarantees
152
+ * IPython's native `!` gives Python users.
153
+ *
154
+ * Python kernels are deliberately untouched: IPython's `!` is richer than a
155
+ * plain shell call (output capture `x = !ls`, `{var}` interpolation,
156
+ * streaming output) and a rewrite would break it.
157
+ *
158
+ * Only whole lines that start with `!` are rewritten. Known limits (v1):
159
+ * a line inside a multi-line string that happens to start with `!` would be
160
+ * rewritten too, and R's intern=TRUE buffers output until the command exits.
161
+ */
162
+ function rewriteShellLines(code, language) {
163
+ const lang = (language || '').toLowerCase();
164
+ if (lang !== 'r')
165
+ return code; // python: native support; others: no safe idiom mapped yet
166
+ if (!/^[ \t]*!/m.test(code))
167
+ return code;
168
+ return code
169
+ .split('\n')
170
+ .map((line) => {
171
+ const m = line.match(/^([ \t]*)!(.+)$/);
172
+ if (!m)
173
+ return line;
174
+ // Merge stderr at the shell level — intern=TRUE captures only stdout.
175
+ const cmd = `${m[2].trim()} 2>&1`;
176
+ const rString = cmd.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
177
+ return `${m[1]}cat(system("${rString}", intern = TRUE), sep = "\\n")`;
178
+ })
179
+ .join('\n');
180
+ }
142
181
  class KernelService {
143
182
  sessions = new Map();
144
183
  fileToSession = new Map();
@@ -573,8 +612,9 @@ class KernelService {
573
612
  hbPort,
574
613
  };
575
614
  // Write connection file
576
- const connDir = path.join(os.tmpdir(), 'nebula-kernels');
577
- fs.mkdirSync(connDir, { recursive: true });
615
+ // Per-user private dir (0700): connection files carry the HMAC key —
616
+ // world-readable means arbitrary code execution as this user.
617
+ const connDir = (0, private_tmp_1.privateTmpDir)('kernels');
578
618
  const connFile = path.join(connDir, `kernel-${sessionId}.json`);
579
619
  const connData = {
580
620
  ip: config.ip,
@@ -587,7 +627,7 @@ class KernelService {
587
627
  iopub_port: config.iopubPort,
588
628
  hb_port: config.hbPort,
589
629
  };
590
- fs.writeFileSync(connFile, JSON.stringify(connData, null, 2));
630
+ fs.writeFileSync(connFile, JSON.stringify(connData, null, 2), { mode: 0o600 });
591
631
  return { config, filePath: connFile };
592
632
  }
593
633
  /**
@@ -595,7 +635,27 @@ class KernelService {
595
635
  */
596
636
  async startKernel(options = {}) {
597
637
  const kernelName = options.kernelName || 'python3';
598
- const spec = (0, kernelspec_1.getKernelSpec)(kernelName);
638
+ // env:<pythonPath> kernels raw-launch a Python environment directly
639
+ // (VSCode-style) — no kernelspec registration involved. Preflight here so
640
+ // callers get a crisp, coded error instead of a spawn failure + ready
641
+ // timeout: python_not_found for a dead path, needs_ipykernel (with an
642
+ // ecosystem-appropriate install hint) when the env can't host a kernel yet.
643
+ const envPython = (0, kernelspec_1.envKernelPythonPath)(kernelName);
644
+ let spec;
645
+ if (envPython) {
646
+ if (!fs.existsSync(envPython)) {
647
+ throw new types_2.KernelProvisionError(`Python not found: ${envPython}`, 'python_not_found');
648
+ }
649
+ const probe = await discovery_service_1.pythonDiscovery.probeForKernel(envPython);
650
+ if (!probe.hasIpykernel) {
651
+ throw new types_2.KernelProvisionError(`ipykernel is not installed in ${envPython}`, 'needs_ipykernel', probe.installHint || undefined);
652
+ }
653
+ const cached = discovery_service_1.pythonDiscovery.getFromCache()?.[envPython];
654
+ spec = (0, kernelspec_1.makeEnvKernelSpec)(envPython, cached?.displayName);
655
+ }
656
+ else {
657
+ spec = (0, kernelspec_1.getKernelSpec)(kernelName);
658
+ }
599
659
  if (!spec) {
600
660
  throw new Error(`Kernel '${kernelName}' not found`);
601
661
  }
@@ -1349,6 +1409,14 @@ class KernelService {
1349
1409
  * Execute code in a kernel session
1350
1410
  */
1351
1411
  async executeCode(sessionId, code, onOutput, onQueueInfo, cellId) {
1412
+ // Polyfill `!command` lines for kernels whose language lacks them (see
1413
+ // rewriteShellLines) — rewritten code still runs IN the kernel process,
1414
+ // so cwd/env/node are the kernel's, exactly like IPython's native `!`.
1415
+ {
1416
+ const kernelName = this.sessions.get(sessionId)?.kernelName;
1417
+ const spec = kernelName ? (0, kernelspec_1.getKernelSpec)(kernelName) : null;
1418
+ code = rewriteShellLines(code, spec?.language);
1419
+ }
1352
1420
  const queueInfo = this.reserveExecutionSlot(sessionId);
1353
1421
  if (onQueueInfo) {
1354
1422
  onQueueInfo(queueInfo);
@@ -10,6 +10,16 @@ export type { KernelSpec } from './types';
10
10
  * Standard kernelspec search paths
11
11
  */
12
12
  export declare function getKernelSearchPaths(): string[];
13
+ /**
14
+ * Read kernelspecs from a list of `…/kernels` directories. `seen` dedupes by
15
+ * kernel name across calls (first path wins — Jupyter precedence order).
16
+ */
17
+ export declare function readKernelSpecsFromPaths(searchPaths: string[], options?: {
18
+ skipDefaultPythonSpecs?: boolean;
19
+ seen?: Set<string>;
20
+ }): KernelSpec[];
21
+ /** The `share/jupyter/kernels` dir belonging to each interpreter's prefix. */
22
+ export declare function envKernelspecDirs(envPythonPaths: string[]): string[];
13
23
  /**
14
24
  * Discover all available kernelspecs on the system
15
25
  * Uses 60-second in-memory cache to avoid repeated disk I/O
@@ -23,6 +33,23 @@ export declare function invalidateKernelspecCache(): void;
23
33
  * Get a specific kernelspec by name
24
34
  */
25
35
  export declare function getKernelSpec(name: string): KernelSpec | null;
36
+ /**
37
+ * Env kernels (VSCode-style raw launch): any Python environment can be used
38
+ * as a kernel WITHOUT registering a kernelspec on disk. The kernel name
39
+ * `env:<pythonPath>` resolves to a synthetic in-memory spec that launches
40
+ * `<python> -m ipykernel_launcher` directly — only ipykernel needs to be
41
+ * importable in the environment; jupyter itself is never required.
42
+ */
43
+ export declare const ENV_KERNEL_PREFIX = "env:";
44
+ export declare function isEnvKernelName(name: string): boolean;
45
+ /** The interpreter path encoded in an env: kernel name, or null. */
46
+ export declare function envKernelPythonPath(name: string): string | null;
47
+ export declare function makeEnvKernelSpec(pythonPath: string, displayName?: string): KernelSpec;
48
+ /**
49
+ * Resolve a kernel name to a spec: env: names produce a synthetic raw-launch
50
+ * spec, anything else falls back to registered kernelspecs on disk.
51
+ */
52
+ export declare function resolveKernelSpec(name: string, displayName?: string): KernelSpec | null;
26
53
  /**
27
54
  * Check if a kernelspec exists
28
55
  */
@@ -39,14 +39,23 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.ENV_KERNEL_PREFIX = void 0;
42
43
  exports.getKernelSearchPaths = getKernelSearchPaths;
44
+ exports.readKernelSpecsFromPaths = readKernelSpecsFromPaths;
45
+ exports.envKernelspecDirs = envKernelspecDirs;
43
46
  exports.discoverKernelSpecs = discoverKernelSpecs;
44
47
  exports.invalidateKernelspecCache = invalidateKernelspecCache;
45
48
  exports.getKernelSpec = getKernelSpec;
49
+ exports.isEnvKernelName = isEnvKernelName;
50
+ exports.envKernelPythonPath = envKernelPythonPath;
51
+ exports.makeEnvKernelSpec = makeEnvKernelSpec;
52
+ exports.resolveKernelSpec = resolveKernelSpec;
46
53
  exports.hasKernelSpec = hasKernelSpec;
47
54
  const fs = __importStar(require("fs"));
48
55
  const path = __importStar(require("path"));
49
56
  const os = __importStar(require("os"));
57
+ const discovery_service_1 = require("../discovery/discovery-service");
58
+ const conda_locations_1 = require("../discovery/conda-locations");
50
59
  // In-memory cache for kernelspecs (avoids repeated disk I/O)
51
60
  let kernelspecCache = null;
52
61
  let kernelspecCacheTime = 0;
@@ -139,12 +148,19 @@ function readKernelSpec(kernelDir) {
139
148
  }
140
149
  }
141
150
  /**
142
- * Perform actual kernelspec discovery (disk I/O)
151
+ * Auto-registered default python specs (`python3`, `python3.11`, …) inside an
152
+ * environment duplicate what the env row in the picker already offers ("run
153
+ * Python here") — showing N identical "Python 3 (ipykernel)" entries would
154
+ * drown the real kernels. VSCode hides these the same way.
143
155
  */
144
- function performKernelspecDiscovery() {
156
+ const DEFAULT_PYTHON_SPEC_RE = /^python\d*(\.\d+)*$/i;
157
+ /**
158
+ * Read kernelspecs from a list of `…/kernels` directories. `seen` dedupes by
159
+ * kernel name across calls (first path wins — Jupyter precedence order).
160
+ */
161
+ function readKernelSpecsFromPaths(searchPaths, options = {}) {
145
162
  const specs = [];
146
- const seenNames = new Set();
147
- const searchPaths = getKernelSearchPaths();
163
+ const seenNames = options.seen ?? new Set();
148
164
  for (const searchPath of searchPaths) {
149
165
  if (!fs.existsSync(searchPath)) {
150
166
  continue;
@@ -159,6 +175,9 @@ function performKernelspecDiscovery() {
159
175
  if (seenNames.has(kernelName)) {
160
176
  continue;
161
177
  }
178
+ if (options.skipDefaultPythonSpecs && DEFAULT_PYTHON_SPEC_RE.test(kernelName)) {
179
+ continue;
180
+ }
162
181
  const kernelDir = path.join(searchPath, kernelName);
163
182
  const spec = readKernelSpec(kernelDir);
164
183
  if (spec) {
@@ -173,6 +192,24 @@ function performKernelspecDiscovery() {
173
192
  }
174
193
  return specs;
175
194
  }
195
+ /** The `share/jupyter/kernels` dir belonging to each interpreter's prefix. */
196
+ function envKernelspecDirs(envPythonPaths) {
197
+ const dirs = envPythonPaths.map(p => path.join((0, conda_locations_1.prefixForPythonExe)(p), 'share', 'jupyter', 'kernels'));
198
+ return [...new Set(dirs)];
199
+ }
200
+ /**
201
+ * Perform actual kernelspec discovery (disk I/O): the standard Jupyter search
202
+ * paths first, then every discovered Python environment's own
203
+ * share/jupyter/kernels (how conda-installed kernels like R/Julia surface) —
204
+ * with each env's redundant default python spec hidden.
205
+ */
206
+ function performKernelspecDiscovery() {
207
+ const seen = new Set();
208
+ const specs = readKernelSpecsFromPaths(getKernelSearchPaths(), { seen });
209
+ const envPaths = Object.keys(discovery_service_1.pythonDiscovery.getFromCache() ?? {});
210
+ specs.push(...readKernelSpecsFromPaths(envKernelspecDirs(envPaths), { seen, skipDefaultPythonSpecs: true }));
211
+ return specs;
212
+ }
176
213
  /**
177
214
  * Discover all available kernelspecs on the system
178
215
  * Uses 60-second in-memory cache to avoid repeated disk I/O
@@ -202,6 +239,40 @@ function getKernelSpec(name) {
202
239
  const specs = discoverKernelSpecs();
203
240
  return specs.find(s => s.name === name) || null;
204
241
  }
242
+ /**
243
+ * Env kernels (VSCode-style raw launch): any Python environment can be used
244
+ * as a kernel WITHOUT registering a kernelspec on disk. The kernel name
245
+ * `env:<pythonPath>` resolves to a synthetic in-memory spec that launches
246
+ * `<python> -m ipykernel_launcher` directly — only ipykernel needs to be
247
+ * importable in the environment; jupyter itself is never required.
248
+ */
249
+ exports.ENV_KERNEL_PREFIX = 'env:';
250
+ function isEnvKernelName(name) {
251
+ return name.startsWith(exports.ENV_KERNEL_PREFIX);
252
+ }
253
+ /** The interpreter path encoded in an env: kernel name, or null. */
254
+ function envKernelPythonPath(name) {
255
+ return isEnvKernelName(name) ? name.slice(exports.ENV_KERNEL_PREFIX.length) : null;
256
+ }
257
+ function makeEnvKernelSpec(pythonPath, displayName) {
258
+ return {
259
+ name: exports.ENV_KERNEL_PREFIX + pythonPath,
260
+ displayName: displayName || `Python (${pythonPath})`,
261
+ language: 'python',
262
+ path: '', // synthetic — no kernelspec directory on disk
263
+ argv: [pythonPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'],
264
+ };
265
+ }
266
+ /**
267
+ * Resolve a kernel name to a spec: env: names produce a synthetic raw-launch
268
+ * spec, anything else falls back to registered kernelspecs on disk.
269
+ */
270
+ function resolveKernelSpec(name, displayName) {
271
+ const pythonPath = envKernelPythonPath(name);
272
+ if (pythonPath)
273
+ return makeEnvKernelSpec(pythonPath, displayName);
274
+ return getKernelSpec(name);
275
+ }
205
276
  /**
206
277
  * Check if a kernelspec exists
207
278
  */
@@ -45,6 +45,16 @@ export declare class HeadlessOperationHandler {
45
45
  * Get notebook from cache, loading from disk if needed.
46
46
  */
47
47
  private getCachedNotebook;
48
+ /** In-flight cache warm-ups per notebook path (single-flight). */
49
+ private warming;
50
+ /**
51
+ * Warm the notebook + undo-history caches with non-blocking I/O. The sync
52
+ * fallback in getCachedNotebook blocks the event loop on a cache miss — on
53
+ * a network filesystem a large notebook + history read stalls every
54
+ * in-flight request (kernel completions included) for seconds. Entry points
55
+ * await this first so the sync accessors below are pure in-memory hits.
56
+ */
57
+ private ensureCached;
48
58
  private getCells;
49
59
  private getVisibleOutputs;
50
60
  private saveCells;
@@ -43,7 +43,7 @@ exports.HeadlessOperationHandler = void 0;
43
43
  const fs = __importStar(require("fs"));
44
44
  const cell_hash_1 = require("./cell-hash");
45
45
  const path = __importStar(require("path"));
46
- const os = __importStar(require("os"));
46
+ const private_tmp_1 = require("../private-tmp");
47
47
  const uuid_1 = require("uuid");
48
48
  const registry_1 = require("../fs/notebook-formats/registry");
49
49
  const cell_metadata_1 = require("./cell-metadata");
@@ -90,6 +90,37 @@ class HeadlessOperationHandler {
90
90
  }
91
91
  return this.cache.get(notebookPath);
92
92
  }
93
+ /** In-flight cache warm-ups per notebook path (single-flight). */
94
+ warming = new Map();
95
+ /**
96
+ * Warm the notebook + undo-history caches with non-blocking I/O. The sync
97
+ * fallback in getCachedNotebook blocks the event loop on a cache miss — on
98
+ * a network filesystem a large notebook + history read stalls every
99
+ * in-flight request (kernel completions included) for seconds. Entry points
100
+ * await this first so the sync accessors below are pure in-memory hits.
101
+ */
102
+ ensureCached(notebookPath) {
103
+ if (!notebookPath || this.cache.has(notebookPath))
104
+ return Promise.resolve();
105
+ const inflight = this.warming.get(notebookPath);
106
+ if (inflight)
107
+ return inflight;
108
+ const run = (async () => {
109
+ const result = await this.fsService.getNotebookCells(notebookPath);
110
+ if (!this.cache.has(notebookPath)) {
111
+ this.cache.set(notebookPath, {
112
+ cells: result.cells || [],
113
+ metadata: result.metadata || {},
114
+ dirty: false,
115
+ });
116
+ }
117
+ await this.undoRedoManager.warm(notebookPath, this.cache.get(notebookPath).cells);
118
+ })().finally(() => {
119
+ this.warming.delete(notebookPath);
120
+ });
121
+ this.warming.set(notebookPath, run);
122
+ return run;
123
+ }
93
124
  getCells(notebookPath) {
94
125
  return this.getCachedNotebook(notebookPath).cells;
95
126
  }
@@ -176,8 +207,8 @@ class HeadlessOperationHandler {
176
207
  /**
177
208
  * Check if agent has permission to modify this notebook.
178
209
  */
179
- checkAgentPermission(notebookPath, _operationType) {
180
- const status = this.fsService.getAgentPermissionStatus(notebookPath);
210
+ async checkAgentPermission(notebookPath, _operationType) {
211
+ const status = await this.fsService.getAgentPermissionStatusAsync(notebookPath);
181
212
  // can_agent_modify already encodes the tri-state rule (agent-created notebooks
182
213
  // are permitted unless explicitly revoked; user notebooks need permission +
183
214
  // history), so an explicit user revoke wins even for agent-created notebooks.
@@ -252,9 +283,20 @@ class HeadlessOperationHandler {
252
283
  // Read-only operations
253
284
  const readOnlyOps = new Set(['readCell', 'readCellOutput', 'searchCells', 'getUpdatesSince', 'startAgentSession', 'endAgentSession']);
254
285
  const permissionExemptOps = new Set(['createNotebook', 'readCell', 'readCellOutput', 'searchCells', 'getUpdatesSince', 'startAgentSession', 'endAgentSession']);
286
+ // Warm the notebook/history caches without blocking the event loop before
287
+ // any sync accessor below runs. createNotebook targets a file that may
288
+ // not exist yet; a failed warm defers to the operation's own error.
289
+ if (notebookPath && opType !== 'createNotebook') {
290
+ try {
291
+ await this.ensureCached(notebookPath);
292
+ }
293
+ catch {
294
+ // Operation handlers report missing/corrupt notebooks themselves
295
+ }
296
+ }
255
297
  // Check permission for write operations
256
298
  if (!permissionExemptOps.has(opType) && notebookPath) {
257
- const permissionError = this.checkAgentPermission(notebookPath, opType);
299
+ const permissionError = await this.checkAgentPermission(notebookPath, opType);
258
300
  if (permissionError) {
259
301
  return permissionError;
260
302
  }
@@ -385,6 +427,14 @@ class HeadlessOperationHandler {
385
427
  */
386
428
  async readNotebook(notebookPath, includeOutputs = true, maxLines, maxChars, maxLinesError, maxCharsError) {
387
429
  try {
430
+ // Non-blocking cache warm-up; on failure the sync accessor below
431
+ // produces the proper error (e.g. notebook not found).
432
+ try {
433
+ await this.ensureCached(notebookPath);
434
+ }
435
+ catch {
436
+ // fall through to getCachedNotebook's own error handling
437
+ }
388
438
  const notebook = this.getCachedNotebook(notebookPath);
389
439
  let cells = notebook.cells;
390
440
  if (includeOutputs) {
@@ -1034,7 +1084,7 @@ class HeadlessOperationHandler {
1034
1084
  return new Promise(resolve => setTimeout(resolve, ms));
1035
1085
  }
1036
1086
  saveOutputToTempFile(content, cellId) {
1037
- const tempDir = path.join(os.tmpdir(), 'nebula', 'outputs');
1087
+ const tempDir = (0, private_tmp_1.privateTmpDir)('outputs');
1038
1088
  if (!fs.existsSync(tempDir)) {
1039
1089
  fs.mkdirSync(tempDir, { recursive: true });
1040
1090
  }
@@ -139,6 +139,14 @@ export declare class HeadlessUndoRedoManager {
139
139
  * Get or initialize undo state for a notebook.
140
140
  */
141
141
  getState(notebookPath: string, cells: NebulaCell[]): NotebookUndoState;
142
+ /**
143
+ * Preload a notebook's history without blocking the event loop. After this
144
+ * resolves, getState is a pure in-memory hit — its loadHistorySync fallback
145
+ * (a large journal read that stalls every in-flight request on a network
146
+ * filesystem) never fires. No-op if state already exists.
147
+ */
148
+ warm(notebookPath: string, cells: NebulaCell[]): Promise<void>;
149
+ private initState;
142
150
  /**
143
151
  * Rebuild undo stack from history.
144
152
  */
@@ -147,25 +147,48 @@ class HeadlessUndoRedoManager {
147
147
  catch (err) {
148
148
  console.warn(`[UndoRedoManager] Failed to load history for ${notebookPath}:`, err);
149
149
  }
150
- // Initialize state
151
- const state = {
152
- undoStack: [],
153
- redoStack: [],
154
- fullHistory: loadedHistory.length > 0 ? loadedHistory : [{
155
- type: 'snapshot',
156
- cells: cells.map(stripCellOutputs),
157
- timestamp: Date.now()
158
- }],
159
- lastContent: new Map(cells.map(c => [c.id, c.content]))
160
- };
161
- // Rebuild undo stack from history
162
- if (loadedHistory.length > 0) {
163
- state.undoStack = this.rebuildUndoStack(loadedHistory);
164
- }
165
- this.states.set(notebookPath, state);
150
+ this.initState(notebookPath, cells, loadedHistory);
166
151
  }
167
152
  return this.states.get(notebookPath);
168
153
  }
154
+ /**
155
+ * Preload a notebook's history without blocking the event loop. After this
156
+ * resolves, getState is a pure in-memory hit — its loadHistorySync fallback
157
+ * (a large journal read that stalls every in-flight request on a network
158
+ * filesystem) never fires. No-op if state already exists.
159
+ */
160
+ async warm(notebookPath, cells) {
161
+ if (this.states.has(notebookPath))
162
+ return;
163
+ let loadedHistory = [];
164
+ try {
165
+ loadedHistory = (await this.fsService.loadHistory(notebookPath));
166
+ }
167
+ catch (err) {
168
+ console.warn(`[UndoRedoManager] Failed to load history for ${notebookPath}:`, err);
169
+ }
170
+ // A concurrent sync getState may have initialized while we awaited
171
+ if (this.states.has(notebookPath))
172
+ return;
173
+ this.initState(notebookPath, cells, loadedHistory);
174
+ }
175
+ initState(notebookPath, cells, loadedHistory) {
176
+ const state = {
177
+ undoStack: [],
178
+ redoStack: [],
179
+ fullHistory: loadedHistory.length > 0 ? loadedHistory : [{
180
+ type: 'snapshot',
181
+ cells: cells.map(stripCellOutputs),
182
+ timestamp: Date.now()
183
+ }],
184
+ lastContent: new Map(cells.map(c => [c.id, c.content]))
185
+ };
186
+ // Rebuild undo stack from history
187
+ if (loadedHistory.length > 0) {
188
+ state.undoStack = this.rebuildUndoStack(loadedHistory);
189
+ }
190
+ this.states.set(notebookPath, state);
191
+ }
169
192
  /**
170
193
  * Rebuild undo stack from history.
171
194
  */
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Per-user private temp directories.
3
+ *
4
+ * Shared paths like /tmp/nebula-kernels break on multi-user machines twice
5
+ * over: the first user's mkdir (with a restrictive umask) locks everyone else
6
+ * out, and anything world-readable leaks — kernel connection files carry the
7
+ * HMAC key, which is remote code execution as the kernel's owner for anyone
8
+ * who can read it.
9
+ *
10
+ * Layout: <tmpdir>/nebula-<username>/<segments...>, mode 0700 throughout.
11
+ * /tmp is sticky, so another user CAN pre-create our directory name — we
12
+ * verify ownership after mkdir and refuse to use a directory we don't own.
13
+ */
14
+ export declare function privateTmpDir(...segments: string[]): string;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Per-user private temp directories.
4
+ *
5
+ * Shared paths like /tmp/nebula-kernels break on multi-user machines twice
6
+ * over: the first user's mkdir (with a restrictive umask) locks everyone else
7
+ * out, and anything world-readable leaks — kernel connection files carry the
8
+ * HMAC key, which is remote code execution as the kernel's owner for anyone
9
+ * who can read it.
10
+ *
11
+ * Layout: <tmpdir>/nebula-<username>/<segments...>, mode 0700 throughout.
12
+ * /tmp is sticky, so another user CAN pre-create our directory name — we
13
+ * verify ownership after mkdir and refuse to use a directory we don't own.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.privateTmpDir = privateTmpDir;
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const os = __importStar(require("os"));
53
+ function privateTmpDir(...segments) {
54
+ const user = (os.userInfo().username || String(process.getuid?.() ?? 'user'))
55
+ .replace(/[^a-zA-Z0-9._-]/g, '_');
56
+ const root = path.join(os.tmpdir(), `nebula-${user}`);
57
+ const dir = path.join(root, ...segments);
58
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
59
+ if (process.platform !== 'win32' && typeof process.getuid === 'function') {
60
+ const st = fs.statSync(root);
61
+ if (st.uid !== process.getuid()) {
62
+ throw new Error(`${root} exists but is owned by uid ${st.uid}, not us (${process.getuid()}) — ` +
63
+ `refusing to write private files into another user's directory. Remove or rename it.`);
64
+ }
65
+ // mkdirSync's mode is filtered by umask and ignored for pre-existing dirs;
66
+ // assert the closed mode explicitly.
67
+ fs.chmodSync(root, 0o700);
68
+ }
69
+ return dir;
70
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * AI autocomplete API — inline code completions for notebook cells.
3
+ *
4
+ * Routes live under /api and are auth-protected like the rest of the API.
5
+ * Backed by the nebula-autocomplete engine, which drives a Claude Code / Codex
6
+ * CLI using the user's own subscription. The CLI can run in one of two places:
7
+ *
8
+ * - on THIS server (default) — when the Nebula host itself has claude/codex
9
+ * (a local install, or a login node where the CLIs are available);
10
+ * - on the USER'S machine over the reverse SSH tunnel ("run on my machine") —
11
+ * for remote Nebula where the user would rather use their own laptop's CLI +
12
+ * subscription. See the `transport` field on the completion request.
13
+ *
14
+ * Engines are created lazily and cached per (backend, transport) so a warm pool
15
+ * is reused across turns; disposed on server shutdown.
16
+ */
17
+ import { FastifyInstance } from 'fastify';
18
+ export declare function disposeAutocompleteEngines(): void;
19
+ export default function autocompleteRoutes(fastify: FastifyInstance): Promise<void>;