hopeid 1.1.0 ā 1.2.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/cli/hopeid.js +254 -19
- package/package.json +1 -1
package/cli/hopeid.js
CHANGED
|
@@ -27,6 +27,7 @@ Usage:
|
|
|
27
27
|
hopeid scan --stdin Read message from stdin
|
|
28
28
|
hopeid test Run test suite (heuristic-only)
|
|
29
29
|
hopeid stats Show pattern statistics
|
|
30
|
+
hopeid doctor Run health checks
|
|
30
31
|
hopeid setup Full OpenClaw integration setup
|
|
31
32
|
hopeid help Show this help
|
|
32
33
|
|
|
@@ -73,6 +74,9 @@ async function main() {
|
|
|
73
74
|
case 'stats':
|
|
74
75
|
handleStats();
|
|
75
76
|
break;
|
|
77
|
+
case 'doctor':
|
|
78
|
+
await handleDoctor(args.slice(1));
|
|
79
|
+
break;
|
|
76
80
|
case 'setup':
|
|
77
81
|
await handleSetup(args.slice(1));
|
|
78
82
|
break;
|
|
@@ -290,6 +294,251 @@ function readStdin() {
|
|
|
290
294
|
});
|
|
291
295
|
}
|
|
292
296
|
|
|
297
|
+
async function handleDoctor(args) {
|
|
298
|
+
const os = require('os');
|
|
299
|
+
|
|
300
|
+
console.log('\nš„ hopeIDS Doctor\n');
|
|
301
|
+
|
|
302
|
+
let exitCode = 0;
|
|
303
|
+
const checks = [];
|
|
304
|
+
|
|
305
|
+
// Check 1: Node.js version
|
|
306
|
+
const nodeVersion = process.version;
|
|
307
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
308
|
+
const nodeOk = nodeMajor >= 18;
|
|
309
|
+
|
|
310
|
+
checks.push({
|
|
311
|
+
name: 'Node.js',
|
|
312
|
+
status: nodeOk ? 'ā
' : 'ā',
|
|
313
|
+
details: nodeVersion,
|
|
314
|
+
ok: nodeOk
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (!nodeOk) exitCode = 1;
|
|
318
|
+
|
|
319
|
+
// Check 2: Pattern files
|
|
320
|
+
let patternStatus = 'ā
';
|
|
321
|
+
let patternDetails = '';
|
|
322
|
+
let patternOk = true;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const ids = new HopeIDS({ logLevel: 'error' });
|
|
326
|
+
const stats = ids.getStats();
|
|
327
|
+
patternDetails = `${stats.patternCount} loaded (${stats.categories.length} categories)`;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
patternStatus = 'ā';
|
|
330
|
+
patternDetails = `Failed to load: ${error.message}`;
|
|
331
|
+
patternOk = false;
|
|
332
|
+
exitCode = 1;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
checks.push({
|
|
336
|
+
name: 'Patterns',
|
|
337
|
+
status: patternStatus,
|
|
338
|
+
details: patternDetails,
|
|
339
|
+
ok: patternOk
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Check 3: LLM endpoint
|
|
343
|
+
let llmStatus = 'ā
';
|
|
344
|
+
let llmDetails = '';
|
|
345
|
+
let llmOk = true;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const ids = new HopeIDS({
|
|
349
|
+
semanticEnabled: true,
|
|
350
|
+
requireLLM: false,
|
|
351
|
+
logLevel: 'error'
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Try to detect provider
|
|
355
|
+
await ids.semantic.ensureProvider();
|
|
356
|
+
|
|
357
|
+
const provider = ids.semantic._detectedProvider;
|
|
358
|
+
const model = ids.semantic.options.llmModel;
|
|
359
|
+
const endpoint = ids.semantic.options.llmEndpoint;
|
|
360
|
+
|
|
361
|
+
if (provider === 'none' || !provider) {
|
|
362
|
+
llmStatus = 'ā ļø';
|
|
363
|
+
llmDetails = 'No endpoint configured (pattern-only mode)';
|
|
364
|
+
llmOk = true; // Not an error, just a warning
|
|
365
|
+
} else {
|
|
366
|
+
// Try a quick connection test
|
|
367
|
+
try {
|
|
368
|
+
if (provider === 'ollama') {
|
|
369
|
+
const response = await fetch('http://localhost:11434/api/tags', {
|
|
370
|
+
signal: AbortSignal.timeout(2000)
|
|
371
|
+
});
|
|
372
|
+
if (!response.ok) throw new Error('Ollama not responding');
|
|
373
|
+
} else if (provider === 'lmstudio') {
|
|
374
|
+
const response = await fetch('http://localhost:1234/v1/models', {
|
|
375
|
+
signal: AbortSignal.timeout(2000)
|
|
376
|
+
});
|
|
377
|
+
if (!response.ok) throw new Error('LM Studio not responding');
|
|
378
|
+
} else if (provider === 'openai' || provider === 'anthropic') {
|
|
379
|
+
// Just check if API key exists
|
|
380
|
+
if (!ids.semantic.options.apiKey) {
|
|
381
|
+
throw new Error('API key not set');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
llmDetails = `${provider} (${model})`;
|
|
386
|
+
} catch (testError) {
|
|
387
|
+
llmStatus = 'ā ļø';
|
|
388
|
+
llmDetails = `${provider} configured but unreachable: ${testError.message}`;
|
|
389
|
+
llmOk = true; // Warning, not error
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
llmStatus = 'ā';
|
|
394
|
+
llmDetails = `Error: ${error.message}`;
|
|
395
|
+
llmOk = false;
|
|
396
|
+
exitCode = 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
checks.push({
|
|
400
|
+
name: 'LLM',
|
|
401
|
+
status: llmStatus,
|
|
402
|
+
details: llmDetails,
|
|
403
|
+
ok: llmOk
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Check 4: OpenClaw plugin
|
|
407
|
+
let pluginStatus = 'ā
';
|
|
408
|
+
let pluginDetails = 'OpenClaw plugin found';
|
|
409
|
+
let pluginOk = true;
|
|
410
|
+
|
|
411
|
+
const pluginPath = path.join(__dirname, '..', 'extensions', 'openclaw-plugin');
|
|
412
|
+
if (!fs.existsSync(pluginPath)) {
|
|
413
|
+
pluginStatus = 'ā ļø';
|
|
414
|
+
pluginDetails = 'Plugin directory not found (optional)';
|
|
415
|
+
pluginOk = true; // Not critical
|
|
416
|
+
} else {
|
|
417
|
+
// Check if plugin manifest exists
|
|
418
|
+
const manifestPath = path.join(pluginPath, 'openclaw.plugin.json');
|
|
419
|
+
if (!fs.existsSync(manifestPath)) {
|
|
420
|
+
pluginStatus = 'ā ļø';
|
|
421
|
+
pluginDetails = 'Plugin manifest missing';
|
|
422
|
+
pluginOk = true; // Not critical
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
checks.push({
|
|
427
|
+
name: 'Plugin',
|
|
428
|
+
status: pluginStatus,
|
|
429
|
+
details: pluginDetails,
|
|
430
|
+
ok: pluginOk
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Check 5: Test suite
|
|
434
|
+
let testStatus = 'ā
';
|
|
435
|
+
let testDetails = '';
|
|
436
|
+
let testOk = true;
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const testDir = path.join(__dirname, '../test');
|
|
440
|
+
|
|
441
|
+
if (!fs.existsSync(testDir)) {
|
|
442
|
+
testStatus = 'ā ļø';
|
|
443
|
+
testDetails = 'Test directory not found';
|
|
444
|
+
testOk = true; // Not critical for end users
|
|
445
|
+
} else {
|
|
446
|
+
// Count test files
|
|
447
|
+
const attacksDir = path.join(testDir, 'attacks');
|
|
448
|
+
const benignDir = path.join(testDir, 'benign');
|
|
449
|
+
|
|
450
|
+
let attackCount = 0;
|
|
451
|
+
let benignCount = 0;
|
|
452
|
+
|
|
453
|
+
if (fs.existsSync(attacksDir)) {
|
|
454
|
+
attackCount = fs.readdirSync(attacksDir).filter(f => f.endsWith('.txt')).length;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (fs.existsSync(benignDir)) {
|
|
458
|
+
benignCount = fs.readdirSync(benignDir).filter(f => f.endsWith('.txt')).length;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const totalTests = attackCount + benignCount;
|
|
462
|
+
|
|
463
|
+
if (totalTests === 0) {
|
|
464
|
+
testStatus = 'ā ļø';
|
|
465
|
+
testDetails = 'No test files found';
|
|
466
|
+
testOk = true;
|
|
467
|
+
} else {
|
|
468
|
+
testDetails = `${totalTests} tests available (run 'hopeid test' to execute)`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} catch (error) {
|
|
472
|
+
testStatus = 'ā ļø';
|
|
473
|
+
testDetails = `Error checking tests: ${error.message}`;
|
|
474
|
+
testOk = true; // Not critical
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
checks.push({
|
|
478
|
+
name: 'Tests',
|
|
479
|
+
status: testStatus,
|
|
480
|
+
details: testDetails,
|
|
481
|
+
ok: testOk
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Check 6: Config file
|
|
485
|
+
let configStatus = 'ā
';
|
|
486
|
+
let configDetails = '';
|
|
487
|
+
let configOk = true;
|
|
488
|
+
|
|
489
|
+
const homeDir = os.homedir();
|
|
490
|
+
const configPaths = [
|
|
491
|
+
path.join(homeDir, '.hopeid', 'config.json'),
|
|
492
|
+
path.join(homeDir, '.config', 'hopeid', 'config.json')
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
let configFound = false;
|
|
496
|
+
for (const configPath of configPaths) {
|
|
497
|
+
if (fs.existsSync(configPath)) {
|
|
498
|
+
configDetails = configPath;
|
|
499
|
+
configFound = true;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!configFound) {
|
|
505
|
+
configStatus = 'ā¹ļø';
|
|
506
|
+
configDetails = 'No config file (using defaults)';
|
|
507
|
+
configOk = true; // Config is optional
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
checks.push({
|
|
511
|
+
name: 'Config',
|
|
512
|
+
status: configStatus,
|
|
513
|
+
details: configDetails,
|
|
514
|
+
ok: configOk
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Print results
|
|
518
|
+
for (const check of checks) {
|
|
519
|
+
const padding = ' '.repeat(Math.max(0, 12 - check.name.length));
|
|
520
|
+
console.log(` ${check.name}:${padding}${check.status} ${check.details}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
console.log();
|
|
524
|
+
|
|
525
|
+
// Summary
|
|
526
|
+
const failed = checks.filter(c => !c.ok).length;
|
|
527
|
+
const warnings = checks.filter(c => c.ok && c.status !== 'ā
').length;
|
|
528
|
+
|
|
529
|
+
if (failed > 0) {
|
|
530
|
+
console.log(`ā ${failed} check(s) failed`);
|
|
531
|
+
} else if (warnings > 0) {
|
|
532
|
+
console.log(`ā ļø ${warnings} warning(s) - hopeIDS is functional but some features may be limited`);
|
|
533
|
+
} else {
|
|
534
|
+
console.log('ā
All checks passed - hopeIDS is healthy!');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
console.log();
|
|
538
|
+
|
|
539
|
+
process.exit(exitCode);
|
|
540
|
+
}
|
|
541
|
+
|
|
293
542
|
async function handleSetup(args) {
|
|
294
543
|
const { execSync, spawnSync } = require('child_process');
|
|
295
544
|
const os = require('os');
|
|
@@ -389,25 +638,11 @@ async function handleSetup(args) {
|
|
|
389
638
|
console.log(' āļø Plugin already enabled');
|
|
390
639
|
}
|
|
391
640
|
|
|
392
|
-
//
|
|
393
|
-
console.log('\nš
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (!config.agents.defaults.sandbox) {
|
|
399
|
-
config.agents.defaults.sandbox = {
|
|
400
|
-
mode: 'non-main',
|
|
401
|
-
scope: 'session',
|
|
402
|
-
workspaceAccess: 'none'
|
|
403
|
-
};
|
|
404
|
-
console.log(' ā
Sandbox enabled for non-main agents');
|
|
405
|
-
console.log(' Mode: non-main (main agent runs on host, others sandboxed)');
|
|
406
|
-
console.log(' Scope: session (each session gets isolated container)');
|
|
407
|
-
console.log(' Workspace: none (sandboxed agents get clean workspace)');
|
|
408
|
-
} else {
|
|
409
|
-
console.log(' āļø Sandbox already configured');
|
|
410
|
-
}
|
|
641
|
+
// Note about sandboxing (don't auto-configure - it can break workers)
|
|
642
|
+
console.log('\nš Sandbox configuration...');
|
|
643
|
+
console.log(' ā¹ļø Sandbox NOT auto-configured (can break worker agents)');
|
|
644
|
+
console.log(' š For public-facing agents (moltbook, social), manually add:');
|
|
645
|
+
console.log(' agents.list[].sandbox: { mode: "all", workspaceAccess: "none" }');
|
|
411
646
|
|
|
412
647
|
// Write updated config
|
|
413
648
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
package/package.json
CHANGED