nothumanallowed 8.3.2 → 8.4.0
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/commands/plugin.mjs +105 -22
- package/src/constants.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.0",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents + unified productivity suite. Gmail, Calendar, Drive, Contacts, Tasks, GitHub, Notion, Slack, voice chat, smart scheduler. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/plugin.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import fs from 'fs';
|
|
21
21
|
import path from 'path';
|
|
22
|
+
import crypto from 'crypto';
|
|
22
23
|
import { loadConfig } from '../config.mjs';
|
|
23
24
|
import { callLLM, callAgent } from '../services/llm.mjs';
|
|
24
25
|
import { NHA_DIR, PLUGINS_DIR, BASE_URL, VERSION } from '../constants.mjs';
|
|
@@ -28,6 +29,7 @@ import { info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
|
|
|
28
29
|
// ── Constants ──────────────────────────────────────────────────────────────
|
|
29
30
|
|
|
30
31
|
const PLUGINS_REGISTRY_URL = `${BASE_URL}/plugins/registry.json`;
|
|
32
|
+
const LOCAL_REGISTRY_FILE = path.join(PLUGINS_DIR, '.registry.json');
|
|
31
33
|
|
|
32
34
|
// ── Plugin Loader ──────────────────────────────────────────────────────────
|
|
33
35
|
|
|
@@ -42,7 +44,8 @@ export async function loadPlugin(name) {
|
|
|
42
44
|
if (!fs.existsSync(pluginFile)) return null;
|
|
43
45
|
|
|
44
46
|
try {
|
|
45
|
-
const
|
|
47
|
+
const { pathToFileURL } = await import('url');
|
|
48
|
+
const mod = await import(pathToFileURL(pluginFile).href);
|
|
46
49
|
return {
|
|
47
50
|
card: mod.PLUGIN_CARD || { name: sanitized, version: '0.0.0', description: '', commands: [] },
|
|
48
51
|
run: typeof mod.run === 'function' ? mod.run : null,
|
|
@@ -151,19 +154,69 @@ async function buildPluginContext(config) {
|
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
// ── SHA-256 Integrity Verification ───────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Compute SHA-256 hash of a file.
|
|
161
|
+
* @param {string} filePath
|
|
162
|
+
* @returns {string} hex hash
|
|
163
|
+
*/
|
|
164
|
+
function computeSHA256(filePath) {
|
|
165
|
+
const content = fs.readFileSync(filePath);
|
|
166
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Verify a plugin file against its registry SHA-256 hash.
|
|
171
|
+
* @param {string} filePath — path to the downloaded plugin
|
|
172
|
+
* @param {string} expectedHash — SHA-256 hex from registry
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
function verifyIntegrity(filePath, expectedHash) {
|
|
176
|
+
if (!expectedHash) return false;
|
|
177
|
+
const actual = computeSHA256(filePath);
|
|
178
|
+
return actual === expectedHash;
|
|
179
|
+
}
|
|
180
|
+
|
|
154
181
|
// ── Registry (available plugins from server) ────────────────────────────────
|
|
155
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Fetch the plugin registry from the server.
|
|
185
|
+
* Registry format: { plugins: [{ name, version, description, sha256, size, author }] }
|
|
186
|
+
* The sha256 field is the hex hash of the .mjs file — verified on install.
|
|
187
|
+
*/
|
|
156
188
|
async function fetchRegistry() {
|
|
157
189
|
try {
|
|
158
190
|
const res = await fetch(PLUGINS_REGISTRY_URL, { signal: AbortSignal.timeout(10000) });
|
|
159
191
|
if (!res.ok) return [];
|
|
160
192
|
const data = await res.json();
|
|
161
|
-
|
|
193
|
+
const plugins = Array.isArray(data.plugins) ? data.plugins : [];
|
|
194
|
+
|
|
195
|
+
// Cache registry locally for offline reference
|
|
196
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
197
|
+
fs.writeFileSync(LOCAL_REGISTRY_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
198
|
+
|
|
199
|
+
return plugins;
|
|
162
200
|
} catch {
|
|
201
|
+
// Fallback to local cached registry
|
|
202
|
+
try {
|
|
203
|
+
if (fs.existsSync(LOCAL_REGISTRY_FILE)) {
|
|
204
|
+
const data = JSON.parse(fs.readFileSync(LOCAL_REGISTRY_FILE, 'utf-8'));
|
|
205
|
+
return Array.isArray(data.plugins) ? data.plugins : [];
|
|
206
|
+
}
|
|
207
|
+
} catch {}
|
|
163
208
|
return [];
|
|
164
209
|
}
|
|
165
210
|
}
|
|
166
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Get the registry entry for a plugin by name.
|
|
214
|
+
*/
|
|
215
|
+
async function getRegistryEntry(name) {
|
|
216
|
+
const registry = await fetchRegistry();
|
|
217
|
+
return registry.find(p => p.name === name) || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
167
220
|
// ── Plugin Template ─────────────────────────────────────────────────────────
|
|
168
221
|
|
|
169
222
|
function generateTemplate(name) {
|
|
@@ -286,31 +339,61 @@ async function cmdInstall(name) {
|
|
|
286
339
|
const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '').replace(/\.mjs$/, '');
|
|
287
340
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
288
341
|
|
|
342
|
+
// Step 1: Check registry for SHA-256 hash
|
|
343
|
+
info(`Checking registry for "${sanitized}"...`);
|
|
344
|
+
const entry = await getRegistryEntry(sanitized);
|
|
345
|
+
|
|
346
|
+
if (!entry) {
|
|
347
|
+
fail(`Plugin "${sanitized}" not found in registry.`);
|
|
348
|
+
info('Available plugins: nha plugin list');
|
|
349
|
+
info(`Or create your own: nha plugin create ${sanitized}`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!entry.sha256) {
|
|
354
|
+
fail(`Plugin "${sanitized}" has no integrity hash in registry. Aborting for security.`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Step 2: Download the plugin
|
|
289
359
|
const dest = path.join(PLUGINS_DIR, `${sanitized}.mjs`);
|
|
290
360
|
const url = `${BASE_URL}/plugins/${sanitized}.mjs`;
|
|
291
361
|
|
|
292
|
-
info(`
|
|
293
|
-
|
|
362
|
+
info(`Downloading "${sanitized}" v${entry.version || '?'}...`);
|
|
294
363
|
const success = await download(url, dest, { timeout: 15000 });
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const plugin = await loadPlugin(sanitized);
|
|
298
|
-
if (plugin && plugin.run) {
|
|
299
|
-
ok(`Plugin "${sanitized}" installed to ~/.nha/plugins/`);
|
|
300
|
-
if (plugin.card.description) {
|
|
301
|
-
info(plugin.card.description);
|
|
302
|
-
}
|
|
303
|
-
if (plugin.card.commands && plugin.card.commands.length > 0) {
|
|
304
|
-
info(`Commands: ${plugin.card.commands.join(', ')}`);
|
|
305
|
-
}
|
|
306
|
-
} else if (plugin) {
|
|
307
|
-
warn(`Plugin "${sanitized}" installed but has no run() function.`);
|
|
308
|
-
} else {
|
|
309
|
-
warn(`Plugin "${sanitized}" downloaded but could not be loaded. Check the file.`);
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
364
|
+
|
|
365
|
+
if (!success) {
|
|
312
366
|
fail(`Could not download plugin "${sanitized}".`);
|
|
313
|
-
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Step 3: Verify SHA-256 integrity
|
|
371
|
+
info('Verifying SHA-256 integrity...');
|
|
372
|
+
const isValid = verifyIntegrity(dest, entry.sha256);
|
|
373
|
+
|
|
374
|
+
if (!isValid) {
|
|
375
|
+
// CRITICAL: hash mismatch — file may be tampered
|
|
376
|
+
fs.rmSync(dest, { force: true });
|
|
377
|
+
fail(`INTEGRITY CHECK FAILED for "${sanitized}"!`);
|
|
378
|
+
fail(`Expected SHA-256: ${entry.sha256}`);
|
|
379
|
+
fail(`Got SHA-256: ${computeSHA256(dest)}`);
|
|
380
|
+
fail('The downloaded file does not match the registry hash. File deleted for safety.');
|
|
381
|
+
fail('This could indicate a compromised server or man-in-the-middle attack.');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
ok(`SHA-256 verified: ${entry.sha256.slice(0, 16)}...`);
|
|
386
|
+
|
|
387
|
+
// Step 4: Load and validate the plugin
|
|
388
|
+
const plugin = await loadPlugin(sanitized);
|
|
389
|
+
if (plugin && plugin.run) {
|
|
390
|
+
ok(`Plugin "${sanitized}" v${entry.version || plugin.card.version} installed.`);
|
|
391
|
+
if (plugin.card.description) info(plugin.card.description);
|
|
392
|
+
if (plugin.card.commands?.length > 0) info(`Commands: ${plugin.card.commands.join(', ')}`);
|
|
393
|
+
} else if (plugin) {
|
|
394
|
+
warn(`Plugin "${sanitized}" installed but has no run() function.`);
|
|
395
|
+
} else {
|
|
396
|
+
warn(`Plugin "${sanitized}" downloaded but could not be loaded.`);
|
|
314
397
|
}
|
|
315
398
|
}
|
|
316
399
|
|
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 = '8.
|
|
8
|
+
export const VERSION = '8.4.0';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|