nothumanallowed 16.0.27 → 16.0.29
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 +144 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.29",
|
|
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.29';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -183,6 +183,23 @@ class SandboxManager {
|
|
|
183
183
|
}
|
|
184
184
|
emit({ type: 'status', msg: `Entry point: ${entryFile}` });
|
|
185
185
|
|
|
186
|
+
// Pre-flight: validate entry file with acorn. If the file content was
|
|
187
|
+
// corrupted by a partial LLM stream (HTTP error leaked as code), quarantine
|
|
188
|
+
// it with a minimal stub so the sandbox can still boot. The user sees a
|
|
189
|
+
// clear "re-generate from chat" message instead of a SyntaxError crash.
|
|
190
|
+
const entryAbs = path.join(projectDir, entryFile);
|
|
191
|
+
if (fs.existsSync(entryAbs)) {
|
|
192
|
+
try {
|
|
193
|
+
const entryContent = fs.readFileSync(entryAbs, 'utf-8');
|
|
194
|
+
const sanitized = _sanitizeGeneratedFile(entryFile, entryContent, path.basename(projectDir));
|
|
195
|
+
if (sanitized !== entryContent) {
|
|
196
|
+
emit({ type: 'warn', msg: `Entry file ${entryFile} was corrupted (likely partial stream / HTTP error leaked into content). Auto-quarantined — re-generate from chat.` });
|
|
197
|
+
fs.writeFileSync(entryAbs + '.corrupt-' + Date.now(), entryContent, 'utf-8');
|
|
198
|
+
fs.writeFileSync(entryAbs, sanitized, 'utf-8');
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
|
|
186
203
|
// ── Phase 2: Dependencies (pre-scan + batch install) ──────────────────
|
|
187
204
|
// Pre-scan the project source files for require()/import statements and
|
|
188
205
|
// diff against package.json + node_modules. Install everything missing in
|
|
@@ -346,10 +363,22 @@ class SandboxManager {
|
|
|
346
363
|
// The pre-scan in Phase 2 catches most cases. Tier 1 here is the safety
|
|
347
364
|
// net for files generated AFTER pre-scan (e.g. user added a require()
|
|
348
365
|
// during chat) or for transitive crashes that surface only at runtime.
|
|
349
|
-
const
|
|
366
|
+
const rawMissing = [...stderrBuf.matchAll(/Cannot find module ['"]([^'"]+)['"]/g)]
|
|
350
367
|
.map(m => m[1])
|
|
351
368
|
.filter(m => !m.startsWith('.') && !m.startsWith('/') && !m.startsWith('node:'))
|
|
352
|
-
.map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0])
|
|
369
|
+
.map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0])
|
|
370
|
+
// Drop Node.js built-ins — they're in the runtime, can't be installed
|
|
371
|
+
.filter(m => !_NODE_BUILTINS.has(m));
|
|
372
|
+
// Resolve LLM hallucinated aliases (jwt → jsonwebtoken, bcrypt → bcryptjs)
|
|
373
|
+
const missingModules = rawMissing.map(m => _PACKAGE_ALIASES.get(m) || m);
|
|
374
|
+
// Detect aliases — if the shim already covers the resolved name, no install needed
|
|
375
|
+
const aliasedFromShim = rawMissing.filter(m => {
|
|
376
|
+
const real = _PACKAGE_ALIASES.get(m);
|
|
377
|
+
return real && _SHIMMED_MODULES.has(real);
|
|
378
|
+
});
|
|
379
|
+
if (aliasedFromShim.length > 0) {
|
|
380
|
+
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".` });
|
|
381
|
+
}
|
|
353
382
|
const uniqueMissing = [...new Set(missingModules)].filter(m => !_SHIMMED_MODULES.has(m));
|
|
354
383
|
if (uniqueMissing.length > 0 && _attempt < MAX_RETRIES) {
|
|
355
384
|
emit({ type: 'phase', phase: 'autofix', msg: `Missing module${uniqueMissing.length === 1 ? '' : 's'}: ${uniqueMissing.join(', ')} — batch installing...` });
|
|
@@ -3484,17 +3513,27 @@ function _patchEntry(projectDir, entryFile, shimDir, port) {
|
|
|
3484
3513
|
// file content. Those strings are short, don't start with valid syntax for
|
|
3485
3514
|
// the file type, and contain specific marker phrases.
|
|
3486
3515
|
const _LLM_ERROR_MARKERS = [
|
|
3516
|
+
// "Access denied" / "Access temporarily denied"
|
|
3487
3517
|
/^\s*Access (temporarily )?denied/i,
|
|
3488
|
-
|
|
3518
|
+
// "Service Temporarily Unavailable", "Server Temporarily Unavailable",
|
|
3519
|
+
// "Temporarily Unavailable" — common nginx/cloudflare/CDN 503 responses
|
|
3520
|
+
/^\s*(Service |Server )?Temporarily Unavailable/i,
|
|
3489
3521
|
/^\s*Service Unavailable/i,
|
|
3522
|
+
/^\s*Rate ?limit(ed)?/i,
|
|
3490
3523
|
/^\s*Internal Server Error/i,
|
|
3491
3524
|
/^\s*Too Many Requests/i,
|
|
3492
3525
|
/^\s*Bad Gateway/i,
|
|
3493
3526
|
/^\s*Gateway Timeout/i,
|
|
3527
|
+
/^\s*Request Timeout/i,
|
|
3528
|
+
/^\s*Not Found/i,
|
|
3529
|
+
/^\s*Forbidden\b/i,
|
|
3530
|
+
/^\s*Unauthorized\b/i,
|
|
3494
3531
|
/^\s*<!DOCTYPE html.*Cloudflare/is,
|
|
3495
|
-
/^\s*<html.*<title>.*(Access|Forbidden|Error)/is,
|
|
3496
|
-
/^\s*\{?\s*"error"\s*:\s*"(rate.?limit|quota|unauthorized)"/i,
|
|
3532
|
+
/^\s*<html.*<title>.*(Access|Forbidden|Error|Unavailable|Cloudflare)/is,
|
|
3533
|
+
/^\s*\{?\s*"error"\s*:\s*"(rate.?limit|quota|unauthorized|temporarily)"/i,
|
|
3497
3534
|
/^\s*error code:\s*\d{3,4}/i,
|
|
3535
|
+
// Standalone HTTP status phrase at the start of a code file is suspicious
|
|
3536
|
+
/^\s*(HTTP\/[\d.]+\s+)?[45]\d{2}\s+/,
|
|
3498
3537
|
];
|
|
3499
3538
|
|
|
3500
3539
|
function _looksLikeLLMError(content) {
|
|
@@ -3517,17 +3556,25 @@ function _fallbackPackageJson(projectName) {
|
|
|
3517
3556
|
}
|
|
3518
3557
|
|
|
3519
3558
|
// Sanitize content before writing to disk. Rejects LLM error responses,
|
|
3520
|
-
// repairs corrupt JSON manifests,
|
|
3521
|
-
// content is unsalvageable. This is the LAST line
|
|
3522
|
-
// API and the user's filesystem.
|
|
3559
|
+
// repairs corrupt JSON manifests, validates JS/TS with acorn, and falls back
|
|
3560
|
+
// to a safe placeholder when content is unsalvageable. This is the LAST line
|
|
3561
|
+
// of defense between the LLM API and the user's filesystem.
|
|
3523
3562
|
function _sanitizeGeneratedFile(name, content, projectName) {
|
|
3524
|
-
const
|
|
3563
|
+
const ext = (name.split('.').pop() || '').toLowerCase();
|
|
3564
|
+
const isJson = ext === 'json' || name.endsWith('package.json');
|
|
3525
3565
|
const isPkg = name === 'package.json' || name.endsWith('/package.json');
|
|
3566
|
+
const isCode = ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx'].includes(ext);
|
|
3526
3567
|
|
|
3568
|
+
// Layer A: detect HTTP error responses leaked as file content
|
|
3527
3569
|
if (_looksLikeLLMError(content)) {
|
|
3528
3570
|
if (isPkg) return _fallbackPackageJson(projectName);
|
|
3529
|
-
|
|
3571
|
+
if (isCode) {
|
|
3572
|
+
return `// nha-webcraft: this file's content from the LLM looked like an HTTP error response\n// (status leaked into stream — likely '${_extractLLMErrorHint(content)}').\n// File quarantined to keep the sandbox bootable. Re-generate from chat.\nmodule.exports = {};\n`;
|
|
3573
|
+
}
|
|
3574
|
+
return '<!-- nha-webcraft: LLM error response detected, content discarded. Re-generate from chat. -->';
|
|
3530
3575
|
}
|
|
3576
|
+
|
|
3577
|
+
// Layer B: validate JSON files
|
|
3531
3578
|
if (isJson) {
|
|
3532
3579
|
try { JSON.parse(content); }
|
|
3533
3580
|
catch {
|
|
@@ -3543,9 +3590,40 @@ function _sanitizeGeneratedFile(name, content, projectName) {
|
|
|
3543
3590
|
}
|
|
3544
3591
|
}
|
|
3545
3592
|
}
|
|
3593
|
+
|
|
3594
|
+
// Layer C: validate JS/JSX/TS files with acorn — catches partial streams
|
|
3595
|
+
// that ended mid-token, leftover HTML/error text mixed with code, etc.
|
|
3596
|
+
if (isCode && content && content.trim()) {
|
|
3597
|
+
try {
|
|
3598
|
+
const parser = ext === 'jsx' || ext === 'tsx' ? acorn.Parser.extend(acornJsx()) : acorn;
|
|
3599
|
+
parser.parse(content, {
|
|
3600
|
+
ecmaVersion: 'latest',
|
|
3601
|
+
sourceType: ['mjs'].includes(ext) ? 'module' : (/\bimport\s+|\bexport\s+/.test(content) ? 'module' : 'script'),
|
|
3602
|
+
allowReturnOutsideFunction: true,
|
|
3603
|
+
allowAwaitOutsideFunction: true,
|
|
3604
|
+
allowHashBang: true,
|
|
3605
|
+
});
|
|
3606
|
+
} catch (parseErr) {
|
|
3607
|
+
// Only quarantine if the error is at the VERY START of the file —
|
|
3608
|
+
// this signals "the whole file is junk" (HTTP error / partial stream)
|
|
3609
|
+
// rather than a normal bug at line 50 that user can fix.
|
|
3610
|
+
const lineMatch = parseErr.message.match(/\((\d+):(\d+)\)/);
|
|
3611
|
+
const startsAtTop = !lineMatch || parseInt(lineMatch[1]) <= 2;
|
|
3612
|
+
if (startsAtTop && content.length < 5000) {
|
|
3613
|
+
return `// nha-webcraft: this file failed to parse near the start (${parseErr.message}).\n// Likely a partial/corrupted stream from the LLM. Quarantined to keep sandbox bootable.\n// Re-generate from chat.\nmodule.exports = {};\n`;
|
|
3614
|
+
}
|
|
3615
|
+
// Otherwise let it through — it's a real code bug, the user will see it
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3546
3619
|
return content;
|
|
3547
3620
|
}
|
|
3548
3621
|
|
|
3622
|
+
function _extractLLMErrorHint(content) {
|
|
3623
|
+
const head = content.slice(0, 200).replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
3624
|
+
return head.slice(0, 80) + (head.length > 80 ? '...' : '');
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3549
3627
|
// Authoritative list of modules covered by our offline-safe shims.
|
|
3550
3628
|
// Used by both the pre-scan (to skip them from npm install) and the Tier 1
|
|
3551
3629
|
// retry (to detect "missing module" stderr that's already covered by a shim).
|
|
@@ -3592,6 +3670,39 @@ function _installedDeps(projectDir) {
|
|
|
3592
3670
|
} catch { return new Set(); }
|
|
3593
3671
|
}
|
|
3594
3672
|
|
|
3673
|
+
// Node.js built-in modules — these are part of the runtime, not npm packages.
|
|
3674
|
+
// `require('fs')` always works without installation. The pre-scan MUST exclude
|
|
3675
|
+
// these or it tries to `npm install fs` which is meaningless (or worse, picks
|
|
3676
|
+
// up a malicious typosquat). Authoritative list from Node 22 LTS docs.
|
|
3677
|
+
const _NODE_BUILTINS = new Set([
|
|
3678
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
|
|
3679
|
+
'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
|
|
3680
|
+
'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector',
|
|
3681
|
+
'inspector/promises', 'module', 'net', 'os', 'path', 'path/posix',
|
|
3682
|
+
'path/win32', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline',
|
|
3683
|
+
'readline/promises', 'repl', 'stream', 'stream/consumers', 'stream/promises',
|
|
3684
|
+
'stream/web', 'string_decoder', 'sys', 'timers', 'timers/promises', 'tls',
|
|
3685
|
+
'trace_events', 'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'wasi',
|
|
3686
|
+
'worker_threads', 'zlib',
|
|
3687
|
+
]);
|
|
3688
|
+
|
|
3689
|
+
// Common LLM hallucinations → real package name. The LLM sometimes invents
|
|
3690
|
+
// shorter aliases for popular packages. We map them transparently so the
|
|
3691
|
+
// generated code "just works" without npm install of fake packages.
|
|
3692
|
+
const _PACKAGE_ALIASES = new Map([
|
|
3693
|
+
['jwt', 'jsonwebtoken'],
|
|
3694
|
+
['bcrypt', 'bcryptjs'],
|
|
3695
|
+
['mongo', 'mongoose'],
|
|
3696
|
+
['postgres', 'pg'],
|
|
3697
|
+
['postgresql', 'pg'],
|
|
3698
|
+
['mysql', 'mysql2'],
|
|
3699
|
+
['env', 'dotenv'],
|
|
3700
|
+
['util-lodash', 'lodash'],
|
|
3701
|
+
['express-cors', 'cors'],
|
|
3702
|
+
['express-helmet', 'helmet'],
|
|
3703
|
+
['express-body-parser', 'body-parser'],
|
|
3704
|
+
]);
|
|
3705
|
+
|
|
3595
3706
|
/** Walk project files and extract all bare-import module names. */
|
|
3596
3707
|
export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_000) {
|
|
3597
3708
|
const found = new Set();
|
|
@@ -3624,9 +3735,16 @@ export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_0
|
|
|
3624
3735
|
let m;
|
|
3625
3736
|
while ((m = re.exec(content)) !== null) {
|
|
3626
3737
|
const spec = m[1];
|
|
3738
|
+
// Filter 1: node:fs, node:path etc.
|
|
3627
3739
|
if (spec.startsWith('node:')) continue;
|
|
3740
|
+
// Get the bare package name (handle @scope/name and subpaths)
|
|
3628
3741
|
const pkg = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
|
|
3629
|
-
if (pkg)
|
|
3742
|
+
if (!pkg) continue;
|
|
3743
|
+
// Filter 2: skip Node built-ins (fs, path, crypto, http, etc.)
|
|
3744
|
+
if (_NODE_BUILTINS.has(pkg)) continue;
|
|
3745
|
+
// Filter 3: resolve LLM-hallucinated aliases to real packages
|
|
3746
|
+
const real = _PACKAGE_ALIASES.get(pkg) || pkg;
|
|
3747
|
+
found.add(real);
|
|
3630
3748
|
}
|
|
3631
3749
|
}
|
|
3632
3750
|
}
|
|
@@ -4291,12 +4409,26 @@ module.exports.default = proxy;
|
|
|
4291
4409
|
// real implementation, and only falls back to our shim when resolution fails.
|
|
4292
4410
|
// Also supports NHA_OFFLINE_SHIM=1 to no-op any unresolvable module.
|
|
4293
4411
|
const shimList = JSON.stringify(Object.keys(shimFiles).filter(f => f !== 'noop.js').map(f => f.replace(/\.js$/, '')));
|
|
4412
|
+
// ALIAS map: bare names the LLM tends to invent → real package name we shim.
|
|
4413
|
+
// Must stay in sync with _PACKAGE_ALIASES in the parent module so pre-scan
|
|
4414
|
+
// and runtime shim agree on what to resolve.
|
|
4415
|
+
const aliasJson = JSON.stringify({
|
|
4416
|
+
'bcrypt': 'bcryptjs',
|
|
4417
|
+
'jwt': 'jsonwebtoken',
|
|
4418
|
+
'mongo': 'mongoose',
|
|
4419
|
+
'postgres': 'pg',
|
|
4420
|
+
'postgresql': 'pg',
|
|
4421
|
+
'env': 'dotenv',
|
|
4422
|
+
'express-cors': 'cors',
|
|
4423
|
+
'express-helmet': 'helmet',
|
|
4424
|
+
'express-body-parser': 'body-parser',
|
|
4425
|
+
});
|
|
4294
4426
|
const shimIndex = `
|
|
4295
4427
|
const Module = require('module');
|
|
4296
4428
|
const path = require('path');
|
|
4297
4429
|
const __shimDir = ${JSON.stringify(shimDir)};
|
|
4298
4430
|
const SHIM_NAMES = new Set(${shimList});
|
|
4299
|
-
const ALIAS = {
|
|
4431
|
+
const ALIAS = ${aliasJson};
|
|
4300
4432
|
const OFFLINE = process.env.NHA_OFFLINE_SHIM === '1';
|
|
4301
4433
|
const _original = Module._resolveFilename.bind(Module);
|
|
4302
4434
|
|