nothumanallowed 16.0.26 → 16.0.28
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 +164 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.28",
|
|
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.28';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -142,6 +142,34 @@ class SandboxManager {
|
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// ── Phase 0: Pre-flight repair ───────────────────────────────────────
|
|
146
|
+
// Before anything else, check that package.json on disk is valid JSON.
|
|
147
|
+
// If a previous LLM generation wrote a corrupt file (e.g. an HTTP error
|
|
148
|
+
// response leaked as content), Node's package_json_reader would crash
|
|
149
|
+
// with SyntaxError before we even get to load shims. Repair in place.
|
|
150
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
151
|
+
if (fs.existsSync(pkgPath)) {
|
|
152
|
+
const pkgRaw = fs.readFileSync(pkgPath, 'utf-8');
|
|
153
|
+
let needsRepair = false;
|
|
154
|
+
let reason = '';
|
|
155
|
+
if (_looksLikeLLMError(pkgRaw)) {
|
|
156
|
+
needsRepair = true;
|
|
157
|
+
reason = 'content looks like an LLM API error response (Access denied / Rate limit / HTML block page)';
|
|
158
|
+
} else {
|
|
159
|
+
try { JSON.parse(pkgRaw); }
|
|
160
|
+
catch (e) { needsRepair = true; reason = `invalid JSON: ${e.message}`; }
|
|
161
|
+
}
|
|
162
|
+
if (needsRepair) {
|
|
163
|
+
emit({ type: 'warn', msg: `package.json corrupt (${reason}) — auto-repairing with minimal fallback so the sandbox can boot.` });
|
|
164
|
+
const projectName = path.basename(projectDir);
|
|
165
|
+
fs.writeFileSync(pkgPath, _fallbackPackageJson(projectName), 'utf-8');
|
|
166
|
+
emit({ type: 'status', msg: `package.json repaired. Original corrupt content backed up to package.json.corrupt-${Date.now()}` });
|
|
167
|
+
try {
|
|
168
|
+
fs.writeFileSync(pkgPath + '.corrupt-' + Date.now(), pkgRaw, 'utf-8');
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
// ── Phase 1: Shims ────────────────────────────────────────────────────
|
|
146
174
|
emit({ type: 'phase', phase: 'shims', msg: 'Injecting runtime shims (pg, redis, mongoose, helmet...)' });
|
|
147
175
|
const shimDir = path.join(projectDir, '.nha-shims');
|
|
@@ -155,6 +183,23 @@ class SandboxManager {
|
|
|
155
183
|
}
|
|
156
184
|
emit({ type: 'status', msg: `Entry point: ${entryFile}` });
|
|
157
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
|
+
|
|
158
203
|
// ── Phase 2: Dependencies (pre-scan + batch install) ──────────────────
|
|
159
204
|
// Pre-scan the project source files for require()/import statements and
|
|
160
205
|
// diff against package.json + node_modules. Install everything missing in
|
|
@@ -647,7 +692,8 @@ const ProjectStore = {
|
|
|
647
692
|
if (!_isSafePath(f.name)) continue;
|
|
648
693
|
const abs = path.join(dir, f.name);
|
|
649
694
|
ensureDir(path.dirname(abs));
|
|
650
|
-
|
|
695
|
+
const sanitized = _sanitizeGeneratedFile(f.name, f.content ?? '', projectName);
|
|
696
|
+
fs.writeFileSync(abs, sanitized, 'utf-8');
|
|
651
697
|
}
|
|
652
698
|
const meta = {
|
|
653
699
|
description: description ?? '',
|
|
@@ -3449,6 +3495,123 @@ function _patchEntry(projectDir, entryFile, shimDir, port) {
|
|
|
3449
3495
|
return '.nha-launcher.js';
|
|
3450
3496
|
}
|
|
3451
3497
|
|
|
3498
|
+
// Detect LLM API error responses that leaked into the file content. When the
|
|
3499
|
+
// LLM endpoint returns 4xx/5xx with a plaintext body (Cloudflare block, rate
|
|
3500
|
+
// limit, "Access temporarily denied"), the response often ends up saved as
|
|
3501
|
+
// file content. Those strings are short, don't start with valid syntax for
|
|
3502
|
+
// the file type, and contain specific marker phrases.
|
|
3503
|
+
const _LLM_ERROR_MARKERS = [
|
|
3504
|
+
// "Access denied" / "Access temporarily denied"
|
|
3505
|
+
/^\s*Access (temporarily )?denied/i,
|
|
3506
|
+
// "Service Temporarily Unavailable", "Server Temporarily Unavailable",
|
|
3507
|
+
// "Temporarily Unavailable" — common nginx/cloudflare/CDN 503 responses
|
|
3508
|
+
/^\s*(Service |Server )?Temporarily Unavailable/i,
|
|
3509
|
+
/^\s*Service Unavailable/i,
|
|
3510
|
+
/^\s*Rate ?limit(ed)?/i,
|
|
3511
|
+
/^\s*Internal Server Error/i,
|
|
3512
|
+
/^\s*Too Many Requests/i,
|
|
3513
|
+
/^\s*Bad Gateway/i,
|
|
3514
|
+
/^\s*Gateway Timeout/i,
|
|
3515
|
+
/^\s*Request Timeout/i,
|
|
3516
|
+
/^\s*Not Found/i,
|
|
3517
|
+
/^\s*Forbidden\b/i,
|
|
3518
|
+
/^\s*Unauthorized\b/i,
|
|
3519
|
+
/^\s*<!DOCTYPE html.*Cloudflare/is,
|
|
3520
|
+
/^\s*<html.*<title>.*(Access|Forbidden|Error|Unavailable|Cloudflare)/is,
|
|
3521
|
+
/^\s*\{?\s*"error"\s*:\s*"(rate.?limit|quota|unauthorized|temporarily)"/i,
|
|
3522
|
+
/^\s*error code:\s*\d{3,4}/i,
|
|
3523
|
+
// Standalone HTTP status phrase at the start of a code file is suspicious
|
|
3524
|
+
/^\s*(HTTP\/[\d.]+\s+)?[45]\d{2}\s+/,
|
|
3525
|
+
];
|
|
3526
|
+
|
|
3527
|
+
function _looksLikeLLMError(content) {
|
|
3528
|
+
if (typeof content !== 'string') return false;
|
|
3529
|
+
const head = content.slice(0, 500);
|
|
3530
|
+
return _LLM_ERROR_MARKERS.some(re => re.test(head));
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
// Minimal package.json template — used when the LLM-generated one is corrupted.
|
|
3534
|
+
function _fallbackPackageJson(projectName) {
|
|
3535
|
+
return JSON.stringify({
|
|
3536
|
+
name: String(projectName || 'nha-project').toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
3537
|
+
version: '1.0.0',
|
|
3538
|
+
description: 'NHA-generated project (package.json auto-repaired)',
|
|
3539
|
+
main: 'index.js',
|
|
3540
|
+
type: 'commonjs',
|
|
3541
|
+
scripts: { start: 'node index.js' },
|
|
3542
|
+
dependencies: {},
|
|
3543
|
+
}, null, 2);
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
// Sanitize content before writing to disk. Rejects LLM error responses,
|
|
3547
|
+
// repairs corrupt JSON manifests, validates JS/TS with acorn, and falls back
|
|
3548
|
+
// to a safe placeholder when content is unsalvageable. This is the LAST line
|
|
3549
|
+
// of defense between the LLM API and the user's filesystem.
|
|
3550
|
+
function _sanitizeGeneratedFile(name, content, projectName) {
|
|
3551
|
+
const ext = (name.split('.').pop() || '').toLowerCase();
|
|
3552
|
+
const isJson = ext === 'json' || name.endsWith('package.json');
|
|
3553
|
+
const isPkg = name === 'package.json' || name.endsWith('/package.json');
|
|
3554
|
+
const isCode = ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx'].includes(ext);
|
|
3555
|
+
|
|
3556
|
+
// Layer A: detect HTTP error responses leaked as file content
|
|
3557
|
+
if (_looksLikeLLMError(content)) {
|
|
3558
|
+
if (isPkg) return _fallbackPackageJson(projectName);
|
|
3559
|
+
if (isCode) {
|
|
3560
|
+
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`;
|
|
3561
|
+
}
|
|
3562
|
+
return '<!-- nha-webcraft: LLM error response detected, content discarded. Re-generate from chat. -->';
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
// Layer B: validate JSON files
|
|
3566
|
+
if (isJson) {
|
|
3567
|
+
try { JSON.parse(content); }
|
|
3568
|
+
catch {
|
|
3569
|
+
if (isPkg) return _fallbackPackageJson(projectName);
|
|
3570
|
+
try {
|
|
3571
|
+
const repaired = content
|
|
3572
|
+
.replace(/,\s*([}\]])/g, '$1')
|
|
3573
|
+
.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/g, '$1"$2":');
|
|
3574
|
+
JSON.parse(repaired);
|
|
3575
|
+
return repaired;
|
|
3576
|
+
} catch {
|
|
3577
|
+
return '{}';
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// Layer C: validate JS/JSX/TS files with acorn — catches partial streams
|
|
3583
|
+
// that ended mid-token, leftover HTML/error text mixed with code, etc.
|
|
3584
|
+
if (isCode && content && content.trim()) {
|
|
3585
|
+
try {
|
|
3586
|
+
const parser = ext === 'jsx' || ext === 'tsx' ? acorn.Parser.extend(acornJsx()) : acorn;
|
|
3587
|
+
parser.parse(content, {
|
|
3588
|
+
ecmaVersion: 'latest',
|
|
3589
|
+
sourceType: ['mjs'].includes(ext) ? 'module' : (/\bimport\s+|\bexport\s+/.test(content) ? 'module' : 'script'),
|
|
3590
|
+
allowReturnOutsideFunction: true,
|
|
3591
|
+
allowAwaitOutsideFunction: true,
|
|
3592
|
+
allowHashBang: true,
|
|
3593
|
+
});
|
|
3594
|
+
} catch (parseErr) {
|
|
3595
|
+
// Only quarantine if the error is at the VERY START of the file —
|
|
3596
|
+
// this signals "the whole file is junk" (HTTP error / partial stream)
|
|
3597
|
+
// rather than a normal bug at line 50 that user can fix.
|
|
3598
|
+
const lineMatch = parseErr.message.match(/\((\d+):(\d+)\)/);
|
|
3599
|
+
const startsAtTop = !lineMatch || parseInt(lineMatch[1]) <= 2;
|
|
3600
|
+
if (startsAtTop && content.length < 5000) {
|
|
3601
|
+
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`;
|
|
3602
|
+
}
|
|
3603
|
+
// Otherwise let it through — it's a real code bug, the user will see it
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
return content;
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
function _extractLLMErrorHint(content) {
|
|
3611
|
+
const head = content.slice(0, 200).replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
3612
|
+
return head.slice(0, 80) + (head.length > 80 ? '...' : '');
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3452
3615
|
// Authoritative list of modules covered by our offline-safe shims.
|
|
3453
3616
|
// Used by both the pre-scan (to skip them from npm install) and the Tier 1
|
|
3454
3617
|
// retry (to detect "missing module" stderr that's already covered by a shim).
|