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.
- package/README.md +35 -20
- package/dist/assets/{errorwidget-C4r2j2DQ.js → errorwidget-Wy5ajPiM.js} +1 -1
- package/dist/assets/{index-Day3QcNs.js → index-B_Bs5koo.js} +1 -1
- package/dist/assets/index-DK4nS74r.js +829 -0
- package/dist/assets/{index-D5w21_Z8.js → index-DeQN03nW.js} +1 -1
- package/dist/assets/index-DzjpmUak.css +32 -0
- package/dist/assets/{index-CsHoPQy-.js → index-g6MjT-VG.js} +1 -1
- package/dist/assets/{services-shim-D6p_A67v.js → services-shim-BirHVsih.js} +1 -1
- package/dist/index.html +2 -2
- package/node-server/dist/auth/auth-middleware.js +22 -4
- package/node-server/dist/auth/auth-service.js +37 -7
- package/node-server/dist/cluster/client-registration.d.ts +12 -1
- package/node-server/dist/cluster/client-registration.js +35 -2
- package/node-server/dist/cluster/kernel-proxy.js +19 -3
- package/node-server/dist/cluster/server-registry.d.ts +9 -0
- package/node-server/dist/cluster/server-registry.js +26 -0
- package/node-server/dist/discovery/conda-locations.d.ts +73 -0
- package/node-server/dist/discovery/conda-locations.js +427 -0
- package/node-server/dist/discovery/discovery-service.d.ts +73 -4
- package/node-server/dist/discovery/discovery-service.js +211 -127
- package/node-server/dist/discovery/types.d.ts +0 -1
- package/node-server/dist/discovery/types.js +1 -2
- package/node-server/dist/environment.d.ts +12 -0
- package/node-server/dist/environment.js +104 -0
- package/node-server/dist/fs/fs-service.d.ts +31 -1
- package/node-server/dist/fs/fs-service.js +160 -28
- package/node-server/dist/fs/types.d.ts +9 -0
- package/node-server/dist/fs/types.js +8 -0
- package/node-server/dist/index.js +71 -4
- package/node-server/dist/kernel/kernel-service.d.ts +17 -0
- package/node-server/dist/kernel/kernel-service.js +72 -4
- package/node-server/dist/kernel/kernelspec.d.ts +27 -0
- package/node-server/dist/kernel/kernelspec.js +75 -4
- package/node-server/dist/notebook/headless-handler.d.ts +10 -0
- package/node-server/dist/notebook/headless-handler.js +55 -5
- package/node-server/dist/notebook/undoRedoManager.d.ts +8 -0
- package/node-server/dist/notebook/undoRedoManager.js +39 -16
- package/node-server/dist/private-tmp.d.ts +14 -0
- package/node-server/dist/private-tmp.js +70 -0
- package/node-server/dist/routes/autocomplete.d.ts +19 -0
- package/node-server/dist/routes/autocomplete.js +294 -0
- package/node-server/dist/routes/fs.js +21 -4
- package/node-server/dist/routes/kernel.js +45 -2
- package/node-server/dist/routes/notebook.js +8 -2
- package/node-server/dist/routes/python.d.ts +1 -2
- package/node-server/dist/routes/python.js +139 -3
- package/node-server/dist/scheduler/allocation-service.d.ts +20 -0
- package/node-server/dist/scheduler/allocation-service.js +124 -8
- package/node-server/dist/scheduler/mock-scheduler.js +7 -3
- package/node-server/dist/scheduler/slurm-scheduler.js +21 -8
- package/node-server/dist/scheduler/types.d.ts +3 -2
- package/node-server/dist/terminal/agent-registry.d.ts +87 -0
- package/node-server/dist/terminal/agent-registry.js +246 -0
- package/node-server/dist/terminal/binding-store.d.ts +57 -0
- package/node-server/dist/terminal/binding-store.js +0 -0
- package/node-server/dist/terminal/pty-manager.d.ts +23 -6
- package/node-server/dist/terminal/pty-manager.js +130 -19
- package/node-server/dist/terminal/server.js +112 -21
- package/node-server/dist/terminal/types.d.ts +2 -0
- package/node-server/package.json +1 -0
- package/node_modules/nebula-autocomplete/README.md +145 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
- package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
- package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
- package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/index.js +5 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
- package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
- package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
- package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
- package/node_modules/nebula-autocomplete/dist/types.js +1 -0
- package/node_modules/nebula-autocomplete/package.json +58 -0
- package/node_modules/nebula-autocomplete/server.d.ts +3 -0
- package/node_modules/nebula-autocomplete/server.js +3 -0
- package/package.json +17 -5
- package/scripts/postinstall.cjs +25 -5
- package/dist/assets/index-7-YBurka.js +0 -716
- package/dist/assets/index-CFBUnxSZ.css +0 -32
- package/node-server/dist/llm/index.d.ts +0 -5
- package/node-server/dist/llm/index.js +0 -21
- package/node-server/dist/llm/llm-service.d.ts +0 -77
- package/node-server/dist/llm/llm-service.js +0 -454
- package/node-server/dist/llm/types.d.ts +0 -40
- package/node-server/dist/llm/types.js +0 -15
- package/node-server/dist/routes/llm.d.ts +0 -8
- 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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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>;
|