nothumanallowed 16.0.28 → 16.0.30
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/package.json +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/webcraft.mjs +140 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.30",
|
|
4
4
|
"description": "Local AI assistant: 80 tools (Gmail, Calendar, Drive, GitHub, Slack, browser, code, files), 38 agents, visual workflows (Studio, AWF, WebCraft). Install with `npm i -g nothumanallowed`, run with `nha ui`. Free tier built-in (Liara), no API key required. Your data stays on your PC — OAuth tokens local, no cloud. Open-source MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '16.0.
|
|
8
|
+
export const VERSION = '16.0.30';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -183,21 +183,39 @@ class SandboxManager {
|
|
|
183
183
|
}
|
|
184
184
|
emit({ type: 'status', msg: `Entry point: ${entryFile}` });
|
|
185
185
|
|
|
186
|
-
// Pre-flight: validate
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
186
|
+
// Pre-flight: validate ALL .js/.mjs/.cjs/.jsx files in the project with
|
|
187
|
+
// acorn. If any are corrupted (partial stream / HTTP error leaked as code),
|
|
188
|
+
// quarantine them with a minimal stub. This catches LLM stream interruptions
|
|
189
|
+
// in route handlers (routes/auth.js, routes/billing.js, etc.) — not just
|
|
190
|
+
// the entry file.
|
|
191
|
+
const repaired = [];
|
|
192
|
+
const codeExts = new Set(['.js', '.mjs', '.cjs', '.jsx']);
|
|
193
|
+
const scanSkip = new Set(['node_modules', '.git', '.nha-shims', 'dist', 'build', '.next', 'coverage']);
|
|
194
|
+
const projectBase = path.basename(projectDir);
|
|
195
|
+
const stack = [projectDir];
|
|
196
|
+
while (stack.length) {
|
|
197
|
+
const cur = stack.pop();
|
|
198
|
+
let entries;
|
|
199
|
+
try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
200
|
+
for (const ent of entries) {
|
|
201
|
+
if (scanSkip.has(ent.name) || ent.name.startsWith('.')) continue;
|
|
202
|
+
const abs = path.join(cur, ent.name);
|
|
203
|
+
if (ent.isDirectory()) { stack.push(abs); continue; }
|
|
204
|
+
if (!codeExts.has(path.extname(ent.name))) continue;
|
|
205
|
+
try {
|
|
206
|
+
const content = fs.readFileSync(abs, 'utf-8');
|
|
207
|
+
const relName = path.relative(projectDir, abs);
|
|
208
|
+
const sanitized = _sanitizeGeneratedFile(relName, content, projectBase);
|
|
209
|
+
if (sanitized !== content) {
|
|
210
|
+
fs.writeFileSync(abs + '.corrupt-' + Date.now(), content, 'utf-8');
|
|
211
|
+
fs.writeFileSync(abs, sanitized, 'utf-8');
|
|
212
|
+
repaired.push(relName);
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (repaired.length > 0) {
|
|
218
|
+
emit({ type: 'warn', msg: `Pre-flight repair: ${repaired.length} file${repaired.length === 1 ? '' : 's'} quarantined (corrupted content) → ${repaired.join(', ')}. Re-generate from chat to restore them.` });
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
// ── Phase 2: Dependencies (pre-scan + batch install) ──────────────────
|
|
@@ -330,6 +348,43 @@ class SandboxManager {
|
|
|
330
348
|
emit({ type: 'phase', phase: 'ready', msg: `Server running on port ${port}` });
|
|
331
349
|
emit({ type: 'ready', port });
|
|
332
350
|
}
|
|
351
|
+
// ── HTTP probe: actually fetch GET / and report what came back ─────
|
|
352
|
+
// Tells the user IMMEDIATELY if the sandbox bound the port but serves
|
|
353
|
+
// a 404 / empty body / wrong content-type. Otherwise they see "ready"
|
|
354
|
+
// but the iframe is black/blank and can't tell why.
|
|
355
|
+
try {
|
|
356
|
+
const probeRes = await fetch(`http://127.0.0.1:${port}/`, {
|
|
357
|
+
signal: AbortSignal.timeout(5000),
|
|
358
|
+
redirect: 'manual',
|
|
359
|
+
}).catch(e => ({ _err: e.message }));
|
|
360
|
+
if (probeRes._err) {
|
|
361
|
+
emit({ type: 'warn', msg: `Probe GET /: ${probeRes._err}. The port is bound but the app doesn't respond on / — check your route handlers.` });
|
|
362
|
+
} else {
|
|
363
|
+
const ct = probeRes.headers.get('content-type') || '(none)';
|
|
364
|
+
const status = probeRes.status;
|
|
365
|
+
const body = await probeRes.text().catch(() => '');
|
|
366
|
+
const len = body.length;
|
|
367
|
+
const isHtml = /text\/html/i.test(ct);
|
|
368
|
+
const isJson = /application\/json/i.test(ct);
|
|
369
|
+
const preview = body.slice(0, 200).replace(/\s+/g, ' ').trim();
|
|
370
|
+
emit({ type: 'log', msg: `[probe] GET / → ${status} (${ct}, ${len} bytes)` });
|
|
371
|
+
if (status >= 400) {
|
|
372
|
+
emit({ type: 'warn', msg: `Probe: GET / returned ${status}. The sandbox is running but has no route for "/". Add app.get('/', ...) in your code or generate an index.html.` });
|
|
373
|
+
} else if (len === 0) {
|
|
374
|
+
emit({ type: 'warn', msg: `Probe: GET / returned empty body (${status}). The route exists but sends no response — check that you call res.send() / res.json() / res.end().` });
|
|
375
|
+
} else if (isHtml && !/<\w+/.test(body)) {
|
|
376
|
+
emit({ type: 'warn', msg: `Probe: GET / claims text/html but has no HTML tags. Preview: "${preview}". Likely a plain text leaked into the response.` });
|
|
377
|
+
} else if (isJson) {
|
|
378
|
+
emit({ type: 'status', msg: `Probe: GET / returns JSON. Browser preview will show raw JSON, not a rendered page. Consider serving an index.html with app.use(express.static('public')) or add a / route returning HTML.` });
|
|
379
|
+
} else if (isHtml) {
|
|
380
|
+
emit({ type: 'status', msg: `Probe: GET / returns HTML (${len} bytes). Iframe preview should render fine.` });
|
|
381
|
+
} else {
|
|
382
|
+
emit({ type: 'log', msg: `[probe] body preview: ${preview}` });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch (e) {
|
|
386
|
+
emit({ type: 'log', msg: `[probe] failed: ${e.message}` });
|
|
387
|
+
}
|
|
333
388
|
return;
|
|
334
389
|
}
|
|
335
390
|
|
|
@@ -363,10 +418,22 @@ class SandboxManager {
|
|
|
363
418
|
// The pre-scan in Phase 2 catches most cases. Tier 1 here is the safety
|
|
364
419
|
// net for files generated AFTER pre-scan (e.g. user added a require()
|
|
365
420
|
// during chat) or for transitive crashes that surface only at runtime.
|
|
366
|
-
const
|
|
421
|
+
const rawMissing = [...stderrBuf.matchAll(/Cannot find module ['"]([^'"]+)['"]/g)]
|
|
367
422
|
.map(m => m[1])
|
|
368
423
|
.filter(m => !m.startsWith('.') && !m.startsWith('/') && !m.startsWith('node:'))
|
|
369
|
-
.map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0])
|
|
424
|
+
.map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0])
|
|
425
|
+
// Drop Node.js built-ins — they're in the runtime, can't be installed
|
|
426
|
+
.filter(m => !_NODE_BUILTINS.has(m));
|
|
427
|
+
// Resolve LLM hallucinated aliases (jwt → jsonwebtoken, bcrypt → bcryptjs)
|
|
428
|
+
const missingModules = rawMissing.map(m => _PACKAGE_ALIASES.get(m) || m);
|
|
429
|
+
// Detect aliases — if the shim already covers the resolved name, no install needed
|
|
430
|
+
const aliasedFromShim = rawMissing.filter(m => {
|
|
431
|
+
const real = _PACKAGE_ALIASES.get(m);
|
|
432
|
+
return real && _SHIMMED_MODULES.has(real);
|
|
433
|
+
});
|
|
434
|
+
if (aliasedFromShim.length > 0) {
|
|
435
|
+
emit({ type: 'warn', msg: `LLM hallucinated package names: ${aliasedFromShim.join(', ')} — the runtime shim already aliases these. If you still see this crash, the shim wasn't re-generated. Restart "nha ui".` });
|
|
436
|
+
}
|
|
370
437
|
const uniqueMissing = [...new Set(missingModules)].filter(m => !_SHIMMED_MODULES.has(m));
|
|
371
438
|
if (uniqueMissing.length > 0 && _attempt < MAX_RETRIES) {
|
|
372
439
|
emit({ type: 'phase', phase: 'autofix', msg: `Missing module${uniqueMissing.length === 1 ? '' : 's'}: ${uniqueMissing.join(', ')} — batch installing...` });
|
|
@@ -3658,6 +3725,39 @@ function _installedDeps(projectDir) {
|
|
|
3658
3725
|
} catch { return new Set(); }
|
|
3659
3726
|
}
|
|
3660
3727
|
|
|
3728
|
+
// Node.js built-in modules — these are part of the runtime, not npm packages.
|
|
3729
|
+
// `require('fs')` always works without installation. The pre-scan MUST exclude
|
|
3730
|
+
// these or it tries to `npm install fs` which is meaningless (or worse, picks
|
|
3731
|
+
// up a malicious typosquat). Authoritative list from Node 22 LTS docs.
|
|
3732
|
+
export const _NODE_BUILTINS = new Set([
|
|
3733
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
|
|
3734
|
+
'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
|
|
3735
|
+
'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector',
|
|
3736
|
+
'inspector/promises', 'module', 'net', 'os', 'path', 'path/posix',
|
|
3737
|
+
'path/win32', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline',
|
|
3738
|
+
'readline/promises', 'repl', 'stream', 'stream/consumers', 'stream/promises',
|
|
3739
|
+
'stream/web', 'string_decoder', 'sys', 'timers', 'timers/promises', 'tls',
|
|
3740
|
+
'trace_events', 'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'wasi',
|
|
3741
|
+
'worker_threads', 'zlib',
|
|
3742
|
+
]);
|
|
3743
|
+
|
|
3744
|
+
// Common LLM hallucinations → real package name. The LLM sometimes invents
|
|
3745
|
+
// shorter aliases for popular packages. We map them transparently so the
|
|
3746
|
+
// generated code "just works" without npm install of fake packages.
|
|
3747
|
+
export const _PACKAGE_ALIASES = new Map([
|
|
3748
|
+
['jwt', 'jsonwebtoken'],
|
|
3749
|
+
['bcrypt', 'bcryptjs'],
|
|
3750
|
+
['mongo', 'mongoose'],
|
|
3751
|
+
['postgres', 'pg'],
|
|
3752
|
+
['postgresql', 'pg'],
|
|
3753
|
+
['mysql', 'mysql2'],
|
|
3754
|
+
['env', 'dotenv'],
|
|
3755
|
+
['util-lodash', 'lodash'],
|
|
3756
|
+
['express-cors', 'cors'],
|
|
3757
|
+
['express-helmet', 'helmet'],
|
|
3758
|
+
['express-body-parser', 'body-parser'],
|
|
3759
|
+
]);
|
|
3760
|
+
|
|
3661
3761
|
/** Walk project files and extract all bare-import module names. */
|
|
3662
3762
|
export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_000) {
|
|
3663
3763
|
const found = new Set();
|
|
@@ -3690,9 +3790,16 @@ export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_0
|
|
|
3690
3790
|
let m;
|
|
3691
3791
|
while ((m = re.exec(content)) !== null) {
|
|
3692
3792
|
const spec = m[1];
|
|
3793
|
+
// Filter 1: node:fs, node:path etc.
|
|
3693
3794
|
if (spec.startsWith('node:')) continue;
|
|
3795
|
+
// Get the bare package name (handle @scope/name and subpaths)
|
|
3694
3796
|
const pkg = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
|
|
3695
|
-
if (pkg)
|
|
3797
|
+
if (!pkg) continue;
|
|
3798
|
+
// Filter 2: skip Node built-ins (fs, path, crypto, http, etc.)
|
|
3799
|
+
if (_NODE_BUILTINS.has(pkg)) continue;
|
|
3800
|
+
// Filter 3: resolve LLM-hallucinated aliases to real packages
|
|
3801
|
+
const real = _PACKAGE_ALIASES.get(pkg) || pkg;
|
|
3802
|
+
found.add(real);
|
|
3696
3803
|
}
|
|
3697
3804
|
}
|
|
3698
3805
|
}
|
|
@@ -4357,12 +4464,26 @@ module.exports.default = proxy;
|
|
|
4357
4464
|
// real implementation, and only falls back to our shim when resolution fails.
|
|
4358
4465
|
// Also supports NHA_OFFLINE_SHIM=1 to no-op any unresolvable module.
|
|
4359
4466
|
const shimList = JSON.stringify(Object.keys(shimFiles).filter(f => f !== 'noop.js').map(f => f.replace(/\.js$/, '')));
|
|
4467
|
+
// ALIAS map: bare names the LLM tends to invent → real package name we shim.
|
|
4468
|
+
// Must stay in sync with _PACKAGE_ALIASES in the parent module so pre-scan
|
|
4469
|
+
// and runtime shim agree on what to resolve.
|
|
4470
|
+
const aliasJson = JSON.stringify({
|
|
4471
|
+
'bcrypt': 'bcryptjs',
|
|
4472
|
+
'jwt': 'jsonwebtoken',
|
|
4473
|
+
'mongo': 'mongoose',
|
|
4474
|
+
'postgres': 'pg',
|
|
4475
|
+
'postgresql': 'pg',
|
|
4476
|
+
'env': 'dotenv',
|
|
4477
|
+
'express-cors': 'cors',
|
|
4478
|
+
'express-helmet': 'helmet',
|
|
4479
|
+
'express-body-parser': 'body-parser',
|
|
4480
|
+
});
|
|
4360
4481
|
const shimIndex = `
|
|
4361
4482
|
const Module = require('module');
|
|
4362
4483
|
const path = require('path');
|
|
4363
4484
|
const __shimDir = ${JSON.stringify(shimDir)};
|
|
4364
4485
|
const SHIM_NAMES = new Set(${shimList});
|
|
4365
|
-
const ALIAS = {
|
|
4486
|
+
const ALIAS = ${aliasJson};
|
|
4366
4487
|
const OFFLINE = process.env.NHA_OFFLINE_SHIM === '1';
|
|
4367
4488
|
const _original = Module._resolveFilename.bind(Module);
|
|
4368
4489
|
|