neoctl-web 0.1.12 → 0.1.14

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/server.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import http from 'node:http';
2
+ import { createChunkUploadHandler } from './chunk-uploads.mjs';
2
3
  import fs from 'node:fs';
3
4
  import fsp from 'node:fs/promises';
4
5
  import path from 'node:path';
@@ -12,6 +13,8 @@ let desktopControlConfig;
12
13
  try { desktopControlConfig = raw ? JSON.parse(raw) : undefined; } catch {}
13
14
  }
14
15
 
16
+ const { containerMode, workspaceFs, deliverUpload, verifyExecutionBackend } = await import('./execution-backend.mjs');
17
+ await verifyExecutionBackend();
15
18
  const { coreRuntimeInfo, createWebRuntime, loadNeoPlugins, runWebServer } = await import('./core-runtime.mjs');
16
19
  const { createWebPluginHost } = await import('./plugins.mjs');
17
20
  const { createWebPluginSettings } = await import('./plugin-settings.mjs');
@@ -37,7 +40,9 @@ const root = path.resolve(process.env.DIST_DIR || path.join(__dirname, 'dist'));
37
40
  const host = process.env.APP_HOST || '0.0.0.0';
38
41
  const port = Number(process.env.APP_PORT || process.env.PORT || 5173);
39
42
  const runtimeTarget = new URL(process.env.NEO_RUNTIME_TARGET || 'http://127.0.0.1:3101');
40
- const { dataRoot, workspaceRoot } = resolveWebStorage();
43
+ const storage = resolveWebStorage();
44
+ const dataRoot = storage.dataRoot;
45
+ const workspaceRoot = containerMode ? '/workspace' : storage.workspaceRoot;
41
46
  const promptLibraryFile = path.resolve(process.env.NEO_PROMPT_LIBRARY_FILE || path.join(dataRoot, 'prompt-library.json'));
42
47
  const uploadsDir = path.resolve(process.env.NEO_UPLOADS_DIR || path.join(dataRoot, 'uploads'));
43
48
  const pluginDir = path.resolve(process.env.NEO_WEB_PLUGIN_DIR || path.join(__dirname, 'plugins'));
@@ -46,7 +51,7 @@ const cpaConfigFile = path.resolve(process.env.NEO_CPA_CONFIG_FILE || path.join(
46
51
  const memoryMonitorFile = path.resolve(process.env.NEO_MEMORY_MONITOR_FILE || path.join(dataRoot, 'memory-monitor.json'));
47
52
  const pluginSettingsFile = path.resolve(process.env.NEO_WEB_PLUGIN_SETTINGS_FILE || path.join(dataRoot, 'plugins.json'));
48
53
  const toolSettingsFile = path.resolve(process.env.NEO_WEB_TOOL_SETTINGS_FILE || path.join(dataRoot, 'tools.json'));
49
- const maxUploadBytes = Number(process.env.NEO_UPLOAD_MAX_BYTES || 25 * 1024 * 1024);
54
+ const chunkUploads = createChunkUploadHandler({ uploadsDir, baseDir: __dirname, finalize: containerMode ? (file, url) => deliverUpload(file, url, runtimeTarget) : undefined });
50
55
  const pluginSettings = await createWebPluginSettings(pluginSettingsFile);
51
56
  const toolSettings = await createWebToolSettings(toolSettingsFile);
52
57
  const pluginEnv = process.env.NEO_WEB_PLUGINS;
@@ -86,6 +91,7 @@ const workspaceRuntime = createWorkspaceRuntimeManager({
86
91
  ...runtimeOptions,
87
92
  ...pluginHost.runtimePlugins(runtimeOptions.sessionId),
88
93
  globalToolOverrides: toolSettings.globalOverrides(),
94
+ resolveGlobalToolOverrides: () => toolSettings.globalOverrides(),
89
95
  sessionToolOverrides: toolSettings.sessionOverrides(runtimeOptions.sessionId),
90
96
  persistGlobalToolOverrides: (overrides) => toolSettings.setGlobalOverrides(overrides),
91
97
  persistSessionToolOverrides: (sessionId, overrides) => toolSettings.setSessionOverrides(sessionId, overrides),
@@ -93,6 +99,10 @@ const workspaceRuntime = createWorkspaceRuntimeManager({
93
99
  }),
94
100
  });
95
101
 
102
+ const { createIsolationMode } = await import('./isolation.mjs');
103
+ const isolation = await createIsolationMode({ dataRoot, workspaceRoot, pluginDir, pluginSettings, toolSettings, cpaQuotaMonitor, memoryState: () => memoryMonitor.getPublicState() });
104
+ if (isolation.enabled && controlEnabled) throw new Error('Isolation mode cannot use desktop control sync');
105
+
96
106
  const DEFAULT_APP_PROMPT_LIBRARY = [];
97
107
 
98
108
  const mime = {
@@ -117,9 +127,10 @@ const server = http.createServer((req, res) => {
117
127
  void routeRequest(req, res);
118
128
  });
119
129
 
130
+ server.once('close', () => isolation.close());
120
131
  server.keepAliveTimeout = 70_000;
121
132
  server.headersTimeout = 75_000;
122
- if (embedRuntime) await startEmbeddedRuntime();
133
+ if (embedRuntime && !isolation.enabled) await startEmbeddedRuntime();
123
134
  await cpaQuotaMonitor.start();
124
135
  await memoryMonitor.start();
125
136
  await new Promise((resolve, reject) => {
@@ -157,6 +168,7 @@ async function startEmbeddedRuntime() {
157
168
  async function routeRequest(req, res) {
158
169
  const url = new URL(req.url || '/', 'http://localhost');
159
170
  try {
171
+ if (await isolation.route(req, res, url)) return;
160
172
  if (await pluginHost.route(req, res, url, { readJsonBody, sendJson })) return;
161
173
  if (req.method === 'GET' && url.pathname === '/api/prompt-library') {
162
174
  return sendJson(res, { items: await readPromptLibrary() });
@@ -212,6 +224,7 @@ async function routeRequest(req, res) {
212
224
  await writePromptLibrary(ordered);
213
225
  return sendJson(res, { ok: true, items: ordered });
214
226
  }
227
+ if (await chunkUploads(req, res, url)) return;
215
228
  if (req.method === 'GET' && url.pathname.startsWith('/api/uploads/')) {
216
229
  const storedName = decodeURIComponent(url.pathname.slice('/api/uploads/'.length));
217
230
  return serveUploadedFile(res, storedName);
@@ -222,7 +235,7 @@ async function routeRequest(req, res) {
222
235
  }
223
236
  if (req.method === 'POST' && url.pathname === '/api/uploads') {
224
237
  const body = await readJsonBody(req);
225
- const file = await storeUploadedFile(body);
238
+ const file = await deliverUpload(await storeUploadedFile(body), url, runtimeTarget);
226
239
  return sendJson(res, { ok: true, file });
227
240
  }
228
241
  if (shouldProxy(url.pathname)) {
@@ -384,7 +397,6 @@ async function storeUploadedFile(payload) {
384
397
  if (!data) throw new Error('missing upload data');
385
398
  const buffer = Buffer.from(data, 'base64');
386
399
  if (!buffer.length) throw new Error('empty upload data');
387
- if (buffer.length > maxUploadBytes) throw new Error(`upload too large: max ${maxUploadBytes} bytes`);
388
400
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
389
401
  const random = Math.random().toString(36).slice(2, 8);
390
402
  const storedName = `${stamp}-${random}-${name}`;
@@ -418,12 +430,14 @@ async function serveUploadedFile(res, storedName) {
418
430
  return;
419
431
  }
420
432
  try {
421
- const body = await fsp.readFile(filePath);
433
+ const stat = await fsp.stat(filePath);
434
+ if (!stat.isFile()) throw new Error('not a file');
422
435
  res.writeHead(200, {
436
+ 'Content-Length': String(stat.size),
423
437
  'Content-Type': mime[path.extname(filePath).toLowerCase()] || 'application/octet-stream',
424
438
  'Cache-Control': 'public, max-age=31536000, immutable',
425
439
  });
426
- res.end(body);
440
+ fs.createReadStream(filePath).on('error', () => res.destroy()).pipe(res);
427
441
  } catch {
428
442
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
429
443
  res.end('not found');
@@ -447,6 +461,12 @@ async function serveLocalImage(res, encodedPath) {
447
461
  return;
448
462
  }
449
463
  try {
464
+ if (containerMode) {
465
+ const body = await workspaceFs.readFile(absolutePath);
466
+ res.writeHead(200, { "Content-Type": contentType, "Content-Length": body.length, "Cache-Control": "no-store" });
467
+ res.end(body);
468
+ return;
469
+ }
450
470
  const fileStat = await fsp.stat(absolutePath);
451
471
  if (!fileStat.isFile()) throw new Error('not a file');
452
472
  res.writeHead(200, {
package/tool-settings.mjs CHANGED
@@ -9,10 +9,14 @@ export async function createWebToolSettings(storageFile) {
9
9
  return structuredClone(state);
10
10
  }
11
11
 
12
- async function update(next) {
13
- state = next;
14
- writeQueue = writeQueue.catch(() => undefined).then(() => writeState(storageFile, state));
15
- await writeQueue;
12
+ async function update(transform) {
13
+ const mutation = writeQueue.catch(() => undefined).then(async () => {
14
+ const next = transform(state);
15
+ await writeState(storageFile, next);
16
+ state = next;
17
+ });
18
+ writeQueue = mutation;
19
+ await mutation;
16
20
  }
17
21
 
18
22
  return {
@@ -25,16 +29,19 @@ export async function createWebToolSettings(storageFile) {
25
29
  return value && typeof value === 'object' ? { ...value } : {};
26
30
  },
27
31
  async setGlobalOverrides(overrides) {
28
- await update({ ...state, global: normalizeOverrides(overrides) });
32
+ const global = normalizeOverrides(overrides);
33
+ await update(current => ({ ...current, global }));
29
34
  },
30
35
  async setSessionOverrides(sessionId, overrides) {
31
36
  const id = String(sessionId || '').trim();
32
37
  if (!id) throw new Error('session id is required');
33
- const sessions = { ...state.sessions };
34
38
  const normalized = normalizeOverrides(overrides);
35
- if (Object.keys(normalized).length) sessions[id] = normalized;
36
- else delete sessions[id];
37
- await update({ ...state, sessions });
39
+ await update(current => {
40
+ const sessions = { ...current.sessions };
41
+ if (Object.keys(normalized).length) sessions[id] = normalized;
42
+ else delete sessions[id];
43
+ return { ...current, sessions };
44
+ });
38
45
  },
39
46
  };
40
47
  }