nothumanallowed 13.3.2 → 13.4.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # NotHumanAllowed
2
2
 
3
- **38 specialized AI agents, 80 tools, Studio visual workflows — all local, all free.** Security auditors, code architects, data analysts, DevOps engineers, technical writers — each with deep domain expertise. Use them individually, run complex multi-agent workflows in Studio, or let them deliberate together.
3
+ **38 specialized AI agents, 81 tools, Studio visual workflows — all local, all free.** Security auditors, code architects, data analysts, DevOps engineers, technical writers — each with deep domain expertise. Use them individually, run complex multi-agent workflows in Studio, or let them deliberate together with Parliament mode.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -36,7 +36,8 @@ EmailAgent → WebSearchAgent → WriterAgent
36
36
 
37
37
  - **No configuration** — works with any LLM provider including Liara (free, no API key)
38
38
  - **Live canvas** — see each agent activate, stream output, and hand off to the next
39
- - **2–5 step workflows** — context flows automatically between steps
39
+ - **HTML dashboard** — canvas generates a downloadable visual report (HTML + PDF)
40
+ - **Parliament mode** — enable for 2+ specialist agents to cross-read and deliberate: R1 (independent), R2 (agents read each other), R3 (HERALD mediation), convergence score
40
41
  - Open `nha ui` → click **Studio** in the sidebar
41
42
 
42
43
  ## Daily Operations (PAO)
@@ -83,6 +84,26 @@ All data stored locally in `~/.nha/ops/`. Tokens encrypted with AES-256-GCM. You
83
84
 
84
85
  38 agents across 11 domains. Each agent is a standalone `.mjs` file you own locally — inspect it, modify it, run it offline.
85
86
 
87
+ ## Code Execution
88
+
89
+ `execute_code` runs Python, JavaScript, or TypeScript in an isolated sandbox:
90
+
91
+ ```bash
92
+ # Python with auto-installed packages
93
+ nha chat
94
+ > use execute_code to analyze this CSV with pandas
95
+
96
+ # TypeScript
97
+ > write and run a TypeScript script that parses this JSON
98
+ ```
99
+
100
+ - **Isolated sandbox** — dedicated temp dir per run, deleted after execution
101
+ - **Stripped environment** — subprocess never sees NHA API keys
102
+ - **Package install** — `packages: ["pandas", "numpy"]` auto-installs via pip/npm
103
+ - **Multi-file** — pass extra files (CSV, JSON, helper modules) via `files: [{path, content}]`
104
+ - **SIGKILL on timeout** — 30s default, configurable up to 120s
105
+ - **Returns** stdout, stderr, exit code, and list of files created in sandbox
106
+
86
107
  ### Security
87
108
  - **SABER** — Security audit, OWASP, threat modeling, pentest planning
88
109
  - **ZERO** — Vulnerability scanning, dependency audit, secret detection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.3.2",
3
+ "version": "13.4.7",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3664,11 +3664,16 @@ ${writtenSoFar ? `## REPORT WRITTEN SO FAR (for consistency):\n${writtenSoFar.sl
3664
3664
  return out.join('');
3665
3665
  };
3666
3666
 
3667
- // If LLM output has no HTML tags it's markdown convert
3668
- if (!bodyHtml || !bodyHtml.includes('<')) {
3669
- const source = bodyHtml || context;
3667
+ // Quality check: count <p> and <li> tags if output is a sparse skeleton
3668
+ // (many sections but almost no paragraph/list content), fall back to converting context directly.
3669
+ const sectionCount = (bodyHtml.match(/<div[^>]*class="section/g) || []).length;
3670
+ const contentCount = (bodyHtml.match(/<p[\s>]|<li[\s>]/g) || []).length;
3671
+ const isSparse = bodyHtml.includes('<') && sectionCount > 0 && contentCount < sectionCount;
3672
+
3673
+ // If LLM output has no HTML tags, or is a sparse skeleton → use context directly
3674
+ if (!bodyHtml || !bodyHtml.includes('<') || isSparse) {
3675
+ const source = context || bodyHtml;
3670
3676
  const converted = mdToNhaHtml(source);
3671
- const agentNames = (stepDef && Array.isArray(stepDef)) ? '' : '';
3672
3677
  bodyHtml = `<div class="header"><h1>${reportTitle.replace(/</g,'&lt;')}</h1><p>NHA Studio Report \u00b7 ${today}</p><div class="meta"><span>${today}</span></div></div>` +
3673
3678
  converted +
3674
3679
  `<div class="footer">NHA Studio \u00b7 ${today}</div>`;
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 = '13.3.2';
8
+ export const VERSION = '13.4.7';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -47,6 +47,17 @@ import {
47
47
 
48
48
  import { notify } from './notification.mjs';
49
49
 
50
+ // ── execute_code: module-level tsx path cache ─────────────────────────────────
51
+ // Resolved once lazily (first TypeScript execution) — avoids shell spawn on every call.
52
+ import { execSync as _execSyncTsx } from 'child_process';
53
+ let _tsxPath = undefined; // undefined = not yet resolved; null = not found; string = path
54
+ function getTsxPath() {
55
+ if (_tsxPath !== undefined) return _tsxPath;
56
+ try { _tsxPath = _execSyncTsx('which tsx 2>/dev/null', { timeout: 3000 }).toString().trim() || null; }
57
+ catch { _tsxPath = null; }
58
+ return _tsxPath;
59
+ }
60
+
50
61
  // ── Constants ────────────────────────────────────────────────────────────────
51
62
 
52
63
  /** Actions that mutate external state and require user confirmation. */
@@ -452,6 +463,21 @@ TOOLS:
452
463
  Download a file from Drive. For Google Docs/Sheets/Slides, exports as PDF.
453
464
  Returns the file as base64-encoded content. Use for binary files, PDFs, images.
454
465
 
466
+ --- CODE EXECUTION ---
467
+
468
+ 81. execute_code(language: "python"|"javascript"|"typescript", code: string, files?: [{path: string, content: string}], packages?: string[], stdin?: string, timeout?: number)
469
+ Execute code in an isolated sandbox and return the full output.
470
+ - language: "python", "javascript", or "typescript"
471
+ - code: the main script to run
472
+ - files (optional): extra files to create in the sandbox before running (e.g. input CSVs, helper modules). Paths are relative to sandbox.
473
+ - packages (optional): pip or npm packages to install before execution (e.g. ["pandas","numpy"] for python, ["lodash"] for js)
474
+ - stdin (optional): text piped to the process stdin
475
+ - timeout (optional): seconds before SIGKILL, default 30, max 120
476
+ Returns: exit_code (0=success ✓, non-zero=failure ✗), stdout, stderr, list of files written in sandbox.
477
+ Sandbox: isolated temp dir, stripped env (no NHA API keys visible to subprocess), SIGKILL on timeout, sandbox deleted after run.
478
+ Use for: data analysis, algorithm verification, running Python scripts, CSV/JSON processing, math computations, generating files (charts, reports), testing TypeScript/JS logic.
479
+ Do NOT use for: network requests (use fetch_url), permanent file I/O (use file_write/file_read).
480
+
455
481
  RULES:
456
482
  - ABSOLUTE RULE: NEVER LIE. NEVER fabricate, invent, or guess information. If you do not know, say "I don't know." If a tool fails, say it failed. If you cannot see something, say so. Honesty is MORE important than being helpful.
457
483
  - CRITICAL ROUTING RULE — browser_open vs web_search:
@@ -1948,6 +1974,197 @@ export async function executeTool(action, params, config) {
1948
1974
  return `Found ${results.length} match${results.length > 1 ? 'es' : ''} for "${params.query}":\n\n${results.join('\n')}`;
1949
1975
  }
1950
1976
 
1977
+ case 'execute_code': {
1978
+ const {
1979
+ language = 'python',
1980
+ code,
1981
+ files = [], // [{path: string, content: string}] — extra files to write in sandbox
1982
+ packages = [], // string[] — pip/npm packages to install before running
1983
+ stdin = '', // string — piped to process stdin
1984
+ timeout = 30, // seconds (max 120)
1985
+ } = params;
1986
+
1987
+ if (!code || typeof code !== 'string') return 'execute_code: missing required param "code"';
1988
+
1989
+ // bash removed: unrestricted shell has full filesystem access and can exfiltrate data.
1990
+ const SUPPORTED = ['python', 'javascript', 'typescript'];
1991
+ if (!SUPPORTED.includes(language)) {
1992
+ return `execute_code: unsupported language "${language}" — use: ${SUPPORTED.join(', ')}`;
1993
+ }
1994
+
1995
+ const { spawn } = await import('child_process');
1996
+ const os = await import('os');
1997
+ const fs = await import('fs');
1998
+ const path = await import('path');
1999
+ const crypto = await import('crypto');
2000
+
2001
+ const MAX_OUTPUT_BYTES = 128 * 1024; // 128 KB per stream
2002
+ const TIMEOUT_MS = Math.min(Math.max(timeout, 5), 120) * 1000;
2003
+
2004
+ // ── Isolated sandbox directory ─────────────────────────────────────────
2005
+ // Each execution gets its own temp dir — cleaned up after run.
2006
+ // Subprocess never sees NHA's cwd or env vars (API keys etc.).
2007
+ const sandboxId = crypto.default.randomBytes(8).toString('hex');
2008
+ const sandboxDir = path.default.join(os.default.tmpdir(), `nha_sandbox_${sandboxId}`);
2009
+ fs.default.mkdirSync(sandboxDir, { recursive: true });
2010
+
2011
+ // Stripped env — only safe POSIX vars, zero NHA secrets.
2012
+ // NOTE: packages install runs with network access (pip/npm fetch from registries).
2013
+ // This is an accepted risk for a local CLI tool — not suitable for server deployment.
2014
+ const safeEnv = {
2015
+ PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin',
2016
+ HOME: sandboxDir,
2017
+ TMPDIR: sandboxDir,
2018
+ LANG: 'en_US.UTF-8',
2019
+ PYTHONDONTWRITEBYTECODE: '1',
2020
+ PYTHONUNBUFFERED: '1',
2021
+ NODE_NO_WARNINGS: '1',
2022
+ };
2023
+
2024
+ const cleanup = () => {
2025
+ try { fs.default.rmSync(sandboxDir, { recursive: true, force: true }); } catch { /* ignore */ }
2026
+ };
2027
+
2028
+ try {
2029
+ // ── Write extra files first ──────────────────────────────────────────
2030
+ for (const f of (files || [])) {
2031
+ if (!f.path || typeof f.content !== 'string') continue;
2032
+ // Prevent path traversal — only allow relative paths inside sandbox
2033
+ const safePath = path.default.join(sandboxDir, path.default.normalize(f.path).replace(/^(\.\.[/\\])+/, ''));
2034
+ fs.default.mkdirSync(path.default.dirname(safePath), { recursive: true });
2035
+ fs.default.writeFileSync(safePath, f.content, 'utf-8');
2036
+ }
2037
+
2038
+ // ── Install packages ─────────────────────────────────────────────────
2039
+ if (packages && packages.length > 0) {
2040
+ const validPkgName = /^[a-zA-Z0-9@._/-]+$/;
2041
+ const safePkgs = packages.filter(p => typeof p === 'string' && validPkgName.test(p) && p.length < 80);
2042
+ if (safePkgs.length > 0) {
2043
+ let installCmd, installArgs;
2044
+ if (language === 'python') {
2045
+ installCmd = 'pip3';
2046
+ installArgs = ['install', '--quiet', '--target', path.default.join(sandboxDir, 'site-packages'), ...safePkgs];
2047
+ } else {
2048
+ // javascript / typescript
2049
+ fs.default.writeFileSync(path.default.join(sandboxDir, 'package.json'), JSON.stringify({ type: 'module' }));
2050
+ installCmd = 'npm';
2051
+ installArgs = ['install', '--prefix', sandboxDir, '--no-save', '--quiet', ...safePkgs];
2052
+ }
2053
+ await new Promise((resolve) => {
2054
+ const inst = spawn(installCmd, installArgs, { cwd: sandboxDir, env: safeEnv, timeout: 60_000 });
2055
+ inst.on('close', resolve);
2056
+ inst.on('error', resolve);
2057
+ });
2058
+ }
2059
+ }
2060
+
2061
+ // ── Resolve runtime + write main entrypoint ──────────────────────────
2062
+ let cmd, cmdArgs, mainFile;
2063
+ if (language === 'python') {
2064
+ mainFile = path.default.join(sandboxDir, 'main.py');
2065
+ // Prepend sys.path so installed packages are found
2066
+ const sitePkgs = path.default.join(sandboxDir, 'site-packages');
2067
+ const preamble = `import sys; sys.path.insert(0, ${JSON.stringify(sitePkgs)})\n`;
2068
+ fs.default.writeFileSync(mainFile, preamble + code, 'utf-8');
2069
+ cmd = 'python3'; cmdArgs = [mainFile];
2070
+ } else if (language === 'javascript') {
2071
+ mainFile = path.default.join(sandboxDir, 'main.mjs');
2072
+ fs.default.writeFileSync(mainFile, code, 'utf-8');
2073
+ cmd = 'node'; cmdArgs = [mainFile];
2074
+ } else if (language === 'typescript') {
2075
+ // Prefer tsx (faster, more compatible), fallback to node --experimental-strip-types (Node 22+)
2076
+ mainFile = path.default.join(sandboxDir, 'main.ts');
2077
+ fs.default.writeFileSync(mainFile, code, 'utf-8');
2078
+ const tsxPath = getTsxPath();
2079
+ if (tsxPath) { cmd = tsxPath; cmdArgs = [mainFile]; }
2080
+ else { cmd = 'node'; cmdArgs = ['--experimental-strip-types', mainFile]; }
2081
+ }
2082
+
2083
+ // ── Execute ──────────────────────────────────────────────────────────
2084
+ const result = await new Promise((resolve) => {
2085
+ const child = spawn(cmd, cmdArgs, {
2086
+ cwd: sandboxDir,
2087
+ env: safeEnv,
2088
+ stdio: ['pipe', 'pipe', 'pipe'],
2089
+ });
2090
+
2091
+ // Feed stdin if provided
2092
+ if (stdin) {
2093
+ child.stdin.write(stdin);
2094
+ child.stdin.end();
2095
+ } else {
2096
+ child.stdin.end();
2097
+ }
2098
+
2099
+ let stdoutBuf = '';
2100
+ let stderrBuf = '';
2101
+ let stdoutTrunc = false;
2102
+ let stderrTrunc = false;
2103
+
2104
+ child.stdout.on('data', (d) => {
2105
+ if (stdoutBuf.length < MAX_OUTPUT_BYTES) stdoutBuf += d.toString();
2106
+ else stdoutTrunc = true;
2107
+ });
2108
+ child.stderr.on('data', (d) => {
2109
+ if (stderrBuf.length < MAX_OUTPUT_BYTES) stderrBuf += d.toString();
2110
+ else stderrTrunc = true;
2111
+ });
2112
+
2113
+ // Hard kill after timeout
2114
+ const killer = setTimeout(() => {
2115
+ try { child.kill('SIGKILL'); } catch {}
2116
+ resolve({ exit_code: 124, stdout: stdoutBuf, stderr: stderrBuf, timed_out: true });
2117
+ }, TIMEOUT_MS);
2118
+
2119
+ child.on('close', (exitCode) => {
2120
+ clearTimeout(killer);
2121
+ resolve({
2122
+ exit_code: exitCode ?? 1,
2123
+ stdout: stdoutBuf + (stdoutTrunc ? '\n[stdout truncated at 128 KB]' : ''),
2124
+ stderr: stderrBuf + (stderrTrunc ? '\n[stderr truncated at 128 KB]' : ''),
2125
+ timed_out: false,
2126
+ });
2127
+ });
2128
+
2129
+ child.on('error', (err) => {
2130
+ clearTimeout(killer);
2131
+ resolve({ exit_code: 1, stdout: '', stderr: `[spawn error] ${err.message}`, timed_out: false });
2132
+ });
2133
+ });
2134
+
2135
+ // ── Collect created/modified files ───────────────────────────────────
2136
+ // List files written inside sandbox (excluding the main entrypoint and site-packages)
2137
+ let createdFiles = [];
2138
+ try {
2139
+ const walk = (dir, base) => {
2140
+ for (const entry of fs.default.readdirSync(dir, { withFileTypes: true })) {
2141
+ const rel = base ? `${base}/${entry.name}` : entry.name;
2142
+ if (entry.isDirectory()) {
2143
+ if (!['site-packages', 'node_modules', '__pycache__'].includes(entry.name)) walk(path.default.join(dir, entry.name), rel);
2144
+ } else if (!['main.py','main.mjs','main.ts','main.sh','package.json'].includes(entry.name)) {
2145
+ const size = fs.default.statSync(path.default.join(dir, entry.name)).size;
2146
+ createdFiles.push(` ${rel} (${size} bytes)`);
2147
+ }
2148
+ }
2149
+ };
2150
+ walk(sandboxDir, '');
2151
+ } catch {}
2152
+
2153
+ // ── Format response ──────────────────────────────────────────────────
2154
+ const lines = [];
2155
+ if (result.timed_out) lines.push(`⏱ TIMEOUT — execution exceeded ${timeout}s (exit 124)`);
2156
+ lines.push(`exit_code: ${result.exit_code}${result.exit_code === 0 ? ' ✓' : ' ✗'}`);
2157
+ if (result.stdout.trim()) lines.push(`\nstdout:\n${result.stdout.trimEnd()}`);
2158
+ if (result.stderr.trim()) lines.push(`\nstderr:\n${result.stderr.trimEnd()}`);
2159
+ if (!result.stdout.trim() && !result.stderr.trim()) lines.push('\n(no output)');
2160
+ if (createdFiles.length > 0) lines.push(`\nfiles written in sandbox:\n${createdFiles.join('\n')}`);
2161
+
2162
+ return lines.join('\n');
2163
+ } finally {
2164
+ cleanup();
2165
+ }
2166
+ }
2167
+
1951
2168
  default:
1952
2169
  return `Unknown action: ${action}`;
1953
2170
  }