sapper-ai 0.7.0 → 0.8.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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +268 -0
- package/dist/guard/ScanCache.d.ts +42 -0
- package/dist/guard/ScanCache.d.ts.map +1 -0
- package/dist/guard/ScanCache.js +261 -0
- package/dist/guard/WarningStore.d.ts +33 -0
- package/dist/guard/WarningStore.d.ts.map +1 -0
- package/dist/guard/WarningStore.js +305 -0
- package/dist/guard/getDefaultPolicy.d.ts +3 -0
- package/dist/guard/getDefaultPolicy.d.ts.map +1 -0
- package/dist/guard/getDefaultPolicy.js +23 -0
- package/dist/guard/hooks/guardCheck.d.ts +12 -0
- package/dist/guard/hooks/guardCheck.d.ts.map +1 -0
- package/dist/guard/hooks/guardCheck.js +137 -0
- package/dist/guard/hooks/guardScan.d.ts +27 -0
- package/dist/guard/hooks/guardScan.d.ts.map +1 -0
- package/dist/guard/hooks/guardScan.js +242 -0
- package/dist/guard/scanSingleSkill.d.ts +11 -0
- package/dist/guard/scanSingleSkill.d.ts.map +1 -0
- package/dist/guard/scanSingleSkill.js +82 -0
- package/dist/guard/setup.d.ts +44 -0
- package/dist/guard/setup.d.ts.map +1 -0
- package/dist/guard/setup.js +296 -0
- package/dist/guard/types.d.ts +43 -0
- package/dist/guard/types.d.ts.map +1 -0
- package/dist/guard/types.js +2 -0
- package/dist/postinstall.d.ts.map +1 -1
- package/dist/postinstall.js +40 -1
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +7 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
export declare function runCli(argv?: string[]): Promise<number>;
|
|
3
|
+
type GuardModuleLoader = (modulePath: string) => Promise<Record<string, unknown>>;
|
|
4
|
+
export declare function __setGuardModuleLoaderForTests(loader: GuardModuleLoader | null): void;
|
|
5
|
+
export {};
|
|
3
6
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAoBA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CA+HpF;AA4WD,KAAK,iBAAiB,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AASjF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAErF"}
|
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.runCli = runCli;
|
|
41
|
+
exports.__setGuardModuleLoaderForTests = __setGuardModuleLoaderForTests;
|
|
41
42
|
const node_fs_1 = require("node:fs");
|
|
42
43
|
const node_os_1 = require("node:os");
|
|
43
44
|
const node_path_1 = require("node:path");
|
|
@@ -52,6 +53,7 @@ const scan_1 = require("./scan");
|
|
|
52
53
|
const detect_1 = require("./openclaw/detect");
|
|
53
54
|
const scanner_1 = require("./openclaw/scanner");
|
|
54
55
|
const env_1 = require("./utils/env");
|
|
56
|
+
const setup_1 = require("./guard/setup");
|
|
55
57
|
async function runCli(argv = process.argv.slice(2)) {
|
|
56
58
|
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
57
59
|
printUsage();
|
|
@@ -96,6 +98,30 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
96
98
|
}
|
|
97
99
|
return runOpenClawWizard();
|
|
98
100
|
}
|
|
101
|
+
if (argv[0] === 'setup') {
|
|
102
|
+
if (argv[1] === '--help' || argv[1] === '-h') {
|
|
103
|
+
printUsage();
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
const parsed = parseSetupArgs(argv.slice(1));
|
|
107
|
+
if (!parsed) {
|
|
108
|
+
printUsage();
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
return runSetupCommand(parsed);
|
|
112
|
+
}
|
|
113
|
+
if (argv[0] === 'guard') {
|
|
114
|
+
if (argv[1] === '--help' || argv[1] === '-h') {
|
|
115
|
+
printUsage();
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
const parsed = parseGuardArgs(argv.slice(1));
|
|
119
|
+
if (!parsed) {
|
|
120
|
+
printUsage();
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
return runGuardCommand(parsed);
|
|
124
|
+
}
|
|
99
125
|
if (argv[0] === 'harden') {
|
|
100
126
|
const parsed = parseHardenArgs(argv.slice(1));
|
|
101
127
|
if (!parsed) {
|
|
@@ -152,6 +178,15 @@ Usage:
|
|
|
152
178
|
sapper-ai harden Plan recommended setup changes (no writes)
|
|
153
179
|
sapper-ai harden --apply Apply recommended project changes
|
|
154
180
|
sapper-ai harden --include-system Include system changes (home directory)
|
|
181
|
+
sapper-ai setup Register Claude Code hooks for Skill Guard
|
|
182
|
+
sapper-ai setup --remove Remove only sapper-ai Skill Guard hooks
|
|
183
|
+
sapper-ai setup --status Show current Skill Guard hook registration status
|
|
184
|
+
sapper-ai guard scan Run SessionStart guard scan hook
|
|
185
|
+
sapper-ai guard check Run UserPromptSubmit guard check hook
|
|
186
|
+
sapper-ai guard dismiss <name> Dismiss warning by skill name
|
|
187
|
+
sapper-ai guard rescan Clear guard cache, then scan again
|
|
188
|
+
sapper-ai guard cache list Show guard cache entries
|
|
189
|
+
sapper-ai guard cache clear Clear guard cache entries
|
|
155
190
|
sapper-ai mcp wrap-config Wrap MCP servers to run behind sapperai-proxy (defaults to Claude Code config)
|
|
156
191
|
sapper-ai mcp unwrap-config Undo MCP wrapping
|
|
157
192
|
sapper-ai quarantine list List quarantined files
|
|
@@ -295,6 +330,239 @@ function parseHardenArgs(argv) {
|
|
|
295
330
|
mcpVersion,
|
|
296
331
|
};
|
|
297
332
|
}
|
|
333
|
+
function parseSetupArgs(argv) {
|
|
334
|
+
if (argv.length === 0) {
|
|
335
|
+
return { command: 'setup_register' };
|
|
336
|
+
}
|
|
337
|
+
if (argv.length > 1) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
if (argv[0] === '--remove') {
|
|
341
|
+
return { command: 'setup_remove' };
|
|
342
|
+
}
|
|
343
|
+
if (argv[0] === '--status') {
|
|
344
|
+
return { command: 'setup_status' };
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
function printSetupStatus() {
|
|
349
|
+
const status = (0, setup_1.getStatus)();
|
|
350
|
+
console.log('Skill Guard setup status:');
|
|
351
|
+
console.log(` Claude directory: ${displayPath(status.claudeDirPath)} ${status.claudeDirExists ? '(found)' : '(missing)'}`);
|
|
352
|
+
console.log(` Settings file: ${displayPath(status.settingsPath)} ${status.settingsExists ? '(found)' : '(missing)'}`);
|
|
353
|
+
for (const commandStatus of status.commands) {
|
|
354
|
+
const state = commandStatus.registered ? 'registered' : 'not registered';
|
|
355
|
+
console.log(` ${commandStatus.event}: ${state} (${commandStatus.matchCount})`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function runSetupCommand(args) {
|
|
359
|
+
if (args.command === 'setup_status') {
|
|
360
|
+
printSetupStatus();
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
if (args.command === 'setup_register') {
|
|
364
|
+
const result = (0, setup_1.registerHooks)();
|
|
365
|
+
if (!result.ok) {
|
|
366
|
+
console.error(`Claude directory not found: ${displayPath(result.status.claudeDirPath)}`);
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
if (result.action === 'already_registered') {
|
|
370
|
+
console.log('Skill Guard hooks are already registered.');
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
console.log(`Registered Skill Guard hooks in ${displayPath(result.status.settingsPath)}.`);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
const result = (0, setup_1.removeHooks)();
|
|
377
|
+
if (!result.ok) {
|
|
378
|
+
console.error(`Claude directory not found: ${displayPath(result.status.claudeDirPath)}`);
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
if (result.removedCount === 0) {
|
|
382
|
+
console.log('No sapper-ai Skill Guard hooks were registered.');
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
console.log(`Removed ${result.removedCount} sapper-ai Skill Guard hook(s).`);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
function parseGuardArgs(argv) {
|
|
389
|
+
const subcommand = argv[0];
|
|
390
|
+
if (!subcommand) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
if (subcommand === 'scan') {
|
|
394
|
+
return argv.length === 1 ? { command: 'guard_scan' } : null;
|
|
395
|
+
}
|
|
396
|
+
if (subcommand === 'check') {
|
|
397
|
+
return argv.length === 1 ? { command: 'guard_check' } : null;
|
|
398
|
+
}
|
|
399
|
+
if (subcommand === 'dismiss') {
|
|
400
|
+
if (argv.length !== 2)
|
|
401
|
+
return null;
|
|
402
|
+
const name = argv[1]?.trim();
|
|
403
|
+
if (!name)
|
|
404
|
+
return null;
|
|
405
|
+
return { command: 'guard_dismiss', name };
|
|
406
|
+
}
|
|
407
|
+
if (subcommand === 'rescan') {
|
|
408
|
+
return argv.length === 1 ? { command: 'guard_rescan' } : null;
|
|
409
|
+
}
|
|
410
|
+
if (subcommand === 'cache') {
|
|
411
|
+
if (argv.length !== 2)
|
|
412
|
+
return null;
|
|
413
|
+
if (argv[1] === 'list')
|
|
414
|
+
return { command: 'guard_cache_list' };
|
|
415
|
+
if (argv[1] === 'clear')
|
|
416
|
+
return { command: 'guard_cache_clear' };
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function resolveNamedExport(moduleExports, names, label) {
|
|
422
|
+
for (const name of names) {
|
|
423
|
+
const candidate = moduleExports[name];
|
|
424
|
+
if (typeof candidate === 'function') {
|
|
425
|
+
return candidate;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
throw new Error(`Missing export for ${label}`);
|
|
429
|
+
}
|
|
430
|
+
const defaultGuardModuleLoader = async (modulePath) => {
|
|
431
|
+
const fullModulePath = `./guard/${modulePath}`;
|
|
432
|
+
return (await Promise.resolve(`${fullModulePath}`).then(s => __importStar(require(s))));
|
|
433
|
+
};
|
|
434
|
+
let guardModuleLoader = defaultGuardModuleLoader;
|
|
435
|
+
function __setGuardModuleLoaderForTests(loader) {
|
|
436
|
+
guardModuleLoader = loader ?? defaultGuardModuleLoader;
|
|
437
|
+
}
|
|
438
|
+
async function loadGuardModule(modulePath) {
|
|
439
|
+
if (modulePath.includes('..')) {
|
|
440
|
+
throw new Error(`Invalid guard module path: ${modulePath}`);
|
|
441
|
+
}
|
|
442
|
+
return guardModuleLoader(modulePath);
|
|
443
|
+
}
|
|
444
|
+
function toExitCode(value) {
|
|
445
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
if (isObject(value) && typeof value.exitCode === 'number' && Number.isInteger(value.exitCode)) {
|
|
449
|
+
return value.exitCode;
|
|
450
|
+
}
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
function isObject(value) {
|
|
454
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
455
|
+
}
|
|
456
|
+
async function runGuardScanHook() {
|
|
457
|
+
const moduleExports = await loadGuardModule('hooks/guardScan');
|
|
458
|
+
const hook = resolveNamedExport(moduleExports, ['guardScan', 'runGuardScan', 'default'], 'guard scan hook');
|
|
459
|
+
const result = await hook();
|
|
460
|
+
return toExitCode(result);
|
|
461
|
+
}
|
|
462
|
+
async function runGuardCheckHook() {
|
|
463
|
+
const moduleExports = await loadGuardModule('hooks/guardCheck');
|
|
464
|
+
const hook = resolveNamedExport(moduleExports, ['guardCheck', 'runGuardCheck', 'default'], 'guard check hook');
|
|
465
|
+
const result = await hook();
|
|
466
|
+
return toExitCode(result);
|
|
467
|
+
}
|
|
468
|
+
async function createGuardClassInstance(modulePath, classNames) {
|
|
469
|
+
const moduleExports = await loadGuardModule(modulePath);
|
|
470
|
+
const Constructor = resolveNamedExport(moduleExports, classNames, modulePath);
|
|
471
|
+
const GuardConstructor = Constructor;
|
|
472
|
+
return new GuardConstructor();
|
|
473
|
+
}
|
|
474
|
+
async function clearGuardCache() {
|
|
475
|
+
const scanCache = await createGuardClassInstance('ScanCache', ['ScanCache', 'default']);
|
|
476
|
+
const clearMethod = scanCache.clear;
|
|
477
|
+
if (typeof clearMethod !== 'function') {
|
|
478
|
+
throw new Error('ScanCache.clear() is not available');
|
|
479
|
+
}
|
|
480
|
+
await clearMethod.call(scanCache);
|
|
481
|
+
}
|
|
482
|
+
async function listGuardCacheEntries() {
|
|
483
|
+
const scanCache = await createGuardClassInstance('ScanCache', ['ScanCache', 'default']);
|
|
484
|
+
const listMethod = scanCache.list;
|
|
485
|
+
if (typeof listMethod !== 'function') {
|
|
486
|
+
throw new Error('ScanCache.list() is not available');
|
|
487
|
+
}
|
|
488
|
+
const listResult = await listMethod.call(scanCache);
|
|
489
|
+
return Array.isArray(listResult) ? listResult : [];
|
|
490
|
+
}
|
|
491
|
+
async function dismissGuardWarning(name) {
|
|
492
|
+
const warningStore = await createGuardClassInstance('WarningStore', ['WarningStore', 'default']);
|
|
493
|
+
const dismissMethod = warningStore.dismiss;
|
|
494
|
+
if (typeof dismissMethod !== 'function') {
|
|
495
|
+
throw new Error('WarningStore.dismiss() is not available');
|
|
496
|
+
}
|
|
497
|
+
return dismissMethod.call(warningStore, name);
|
|
498
|
+
}
|
|
499
|
+
function printGuardCacheList(entries) {
|
|
500
|
+
if (entries.length === 0) {
|
|
501
|
+
console.log('Guard cache is empty.');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
console.log('Guard cache entries:');
|
|
505
|
+
for (const entry of entries) {
|
|
506
|
+
if (!isObject(entry)) {
|
|
507
|
+
console.log(` - ${String(entry)}`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const nestedEntry = isObject(entry.entry) ? entry.entry : undefined;
|
|
511
|
+
const pathValue = typeof entry.path === 'string'
|
|
512
|
+
? entry.path
|
|
513
|
+
: nestedEntry && typeof nestedEntry.path === 'string'
|
|
514
|
+
? nestedEntry.path
|
|
515
|
+
: undefined;
|
|
516
|
+
const skillName = typeof entry.skillName === 'string'
|
|
517
|
+
? entry.skillName
|
|
518
|
+
: nestedEntry && typeof nestedEntry.skillName === 'string'
|
|
519
|
+
? nestedEntry.skillName
|
|
520
|
+
: undefined;
|
|
521
|
+
const path = typeof pathValue === 'string' ? displayPath(pathValue) : undefined;
|
|
522
|
+
const hash = typeof entry.contentHash === 'string' ? entry.contentHash.slice(0, 12) : undefined;
|
|
523
|
+
const display = [skillName, path, hash].filter(Boolean).join(' | ');
|
|
524
|
+
if (display.length > 0) {
|
|
525
|
+
console.log(` - ${display}`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
console.log(` - ${JSON.stringify(entry)}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function toErrorMessage(error) {
|
|
532
|
+
return error instanceof Error ? error.message : String(error);
|
|
533
|
+
}
|
|
534
|
+
async function runGuardCommand(args) {
|
|
535
|
+
try {
|
|
536
|
+
if (args.command === 'guard_scan') {
|
|
537
|
+
return runGuardScanHook();
|
|
538
|
+
}
|
|
539
|
+
if (args.command === 'guard_check') {
|
|
540
|
+
return runGuardCheckHook();
|
|
541
|
+
}
|
|
542
|
+
if (args.command === 'guard_dismiss') {
|
|
543
|
+
await dismissGuardWarning(args.name);
|
|
544
|
+
console.log(`Dismissed warning for "${args.name}".`);
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
if (args.command === 'guard_rescan') {
|
|
548
|
+
await clearGuardCache();
|
|
549
|
+
console.log('Guard cache cleared.');
|
|
550
|
+
return runGuardScanHook();
|
|
551
|
+
}
|
|
552
|
+
if (args.command === 'guard_cache_list') {
|
|
553
|
+
const entries = await listGuardCacheEntries();
|
|
554
|
+
printGuardCacheList(entries);
|
|
555
|
+
return 0;
|
|
556
|
+
}
|
|
557
|
+
await clearGuardCache();
|
|
558
|
+
console.log('Guard cache cleared.');
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.error(`Guard command failed: ${toErrorMessage(error)}`);
|
|
563
|
+
return 1;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
298
566
|
function parseMcpArgs(argv) {
|
|
299
567
|
const subcommand = argv[0];
|
|
300
568
|
const rest = argv.slice(1);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ScanCacheEntry, ScanCacheVerificationResult } from './types';
|
|
2
|
+
export interface ScanCacheOptions {
|
|
3
|
+
filePath?: string;
|
|
4
|
+
homeDir?: string;
|
|
5
|
+
keyPath?: string;
|
|
6
|
+
hostName?: string;
|
|
7
|
+
userId?: string;
|
|
8
|
+
readFileFn?: (filePath: string, encoding: BufferEncoding) => Promise<string>;
|
|
9
|
+
writeFileFn?: (filePath: string, content: string) => Promise<void>;
|
|
10
|
+
readBufferFileFn?: (filePath: string) => Promise<Buffer>;
|
|
11
|
+
writeBufferFileFn?: (filePath: string, content: Buffer) => Promise<void>;
|
|
12
|
+
randomBytesFn?: (size: number) => Buffer;
|
|
13
|
+
}
|
|
14
|
+
export declare class ScanCache {
|
|
15
|
+
private readonly cachePath;
|
|
16
|
+
private readonly hmacKeyPath;
|
|
17
|
+
private readonly readFileFn;
|
|
18
|
+
private readonly writeFileFn;
|
|
19
|
+
private readonly readBufferFileFn;
|
|
20
|
+
private readonly writeBufferFileFn;
|
|
21
|
+
private readonly randomBytesFn;
|
|
22
|
+
private state;
|
|
23
|
+
private hmacKey;
|
|
24
|
+
constructor(options?: ScanCacheOptions);
|
|
25
|
+
has(contentHash: string): Promise<boolean>;
|
|
26
|
+
get(contentHash: string): Promise<ScanCacheEntry | null>;
|
|
27
|
+
set(contentHash: string, entry: ScanCacheEntry): Promise<void>;
|
|
28
|
+
list(): Promise<Array<{
|
|
29
|
+
contentHash: string;
|
|
30
|
+
entry: ScanCacheEntry;
|
|
31
|
+
}>>;
|
|
32
|
+
clear(): Promise<void>;
|
|
33
|
+
verify(): Promise<ScanCacheVerificationResult>;
|
|
34
|
+
private loadState;
|
|
35
|
+
private normalizeState;
|
|
36
|
+
private createEmptyState;
|
|
37
|
+
private persist;
|
|
38
|
+
private computeHmac;
|
|
39
|
+
private getOrCreateHmacKey;
|
|
40
|
+
private readExistingHmacKey;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=ScanCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScanCache.d.ts","sourceRoot":"","sources":["../../src/guard/ScanCache.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAA;AAiB1E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5E,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAClE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACxD,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACzC;AA8GD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiE;IAC5F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsD;IAClF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAuC;IACxE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAsD;IACxF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA0B;IAExD,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,OAAO,CAAsB;gBAEzB,OAAO,GAAE,gBAAqB;IAoBpC,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK1C,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAaxD,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9D,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAatE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,MAAM,IAAI,OAAO,CAAC,2BAA2B,CAAC;YActC,SAAS;IAkBvB,OAAO,CAAC,cAAc;YA0BR,gBAAgB;YAShB,OAAO;YAmBP,WAAW;YAMX,kBAAkB;YAiBlB,mBAAmB;CAWlC"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScanCache = void 0;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const fs_1 = require("../utils/fs");
|
|
9
|
+
const SCAN_CACHE_VERSION = 2;
|
|
10
|
+
const SCAN_CACHE_DIR = '.sapper-ai';
|
|
11
|
+
const SCAN_CACHE_FILE = 'scan-cache.json';
|
|
12
|
+
const HMAC_KEY_FILE = 'hmac-key';
|
|
13
|
+
const HMAC_KEY_BYTES = 32;
|
|
14
|
+
const SHA256_HEX_LENGTH = 64;
|
|
15
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
16
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
17
|
+
function createEntryMap() {
|
|
18
|
+
return Object.create(null);
|
|
19
|
+
}
|
|
20
|
+
function isHexDigest(value) {
|
|
21
|
+
return value.length === SHA256_HEX_LENGTH && /^[0-9a-f]+$/i.test(value);
|
|
22
|
+
}
|
|
23
|
+
function hmacEqual(left, right) {
|
|
24
|
+
if (!isHexDigest(left) || !isHexDigest(right) || left.length !== right.length) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return (0, node_crypto_1.timingSafeEqual)(Buffer.from(left, 'hex'), Buffer.from(right, 'hex'));
|
|
28
|
+
}
|
|
29
|
+
function sortEntries(entries) {
|
|
30
|
+
const ordered = createEntryMap();
|
|
31
|
+
for (const hash of Object.keys(entries).sort()) {
|
|
32
|
+
if (DANGEROUS_KEYS.has(hash)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const entry = entries[hash];
|
|
36
|
+
if (!entry) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
ordered[hash] = {
|
|
40
|
+
path: entry.path,
|
|
41
|
+
skillName: entry.skillName,
|
|
42
|
+
decision: entry.decision,
|
|
43
|
+
risk: entry.risk,
|
|
44
|
+
reasons: [...entry.reasons],
|
|
45
|
+
scannedAt: entry.scannedAt,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return ordered;
|
|
49
|
+
}
|
|
50
|
+
function normalizeEntry(entry) {
|
|
51
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const value = entry;
|
|
55
|
+
const decision = value.decision === 'suspicious' ? 'suspicious' : value.decision === 'safe' ? 'safe' : null;
|
|
56
|
+
if (decision === null) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value.path !== 'string' || value.path.length === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value.skillName !== 'string' || value.skillName.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value.risk !== 'number' || !Number.isFinite(value.risk)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(value.reasons) || value.reasons.some((reason) => typeof reason !== 'string')) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (typeof value.scannedAt !== 'string' || value.scannedAt.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
path: value.path,
|
|
76
|
+
skillName: value.skillName,
|
|
77
|
+
decision,
|
|
78
|
+
risk: value.risk,
|
|
79
|
+
reasons: [...value.reasons],
|
|
80
|
+
scannedAt: value.scannedAt,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function normalizeEntries(entries) {
|
|
84
|
+
if (!entries || typeof entries !== 'object' || Array.isArray(entries)) {
|
|
85
|
+
return createEntryMap();
|
|
86
|
+
}
|
|
87
|
+
const normalized = createEntryMap();
|
|
88
|
+
for (const [hash, entry] of Object.entries(entries)) {
|
|
89
|
+
if (DANGEROUS_KEYS.has(hash)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const safeEntry = normalizeEntry(entry);
|
|
93
|
+
if (!safeEntry) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
normalized[hash] = safeEntry;
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
class ScanCache {
|
|
101
|
+
constructor(options = {}) {
|
|
102
|
+
this.state = null;
|
|
103
|
+
this.hmacKey = null;
|
|
104
|
+
const homePath = options.homeDir ?? (0, node_os_1.homedir)();
|
|
105
|
+
this.cachePath = options.filePath ?? (0, node_path_1.join)(homePath, SCAN_CACHE_DIR, SCAN_CACHE_FILE);
|
|
106
|
+
this.hmacKeyPath = options.keyPath ?? (0, node_path_1.join)(homePath, SCAN_CACHE_DIR, HMAC_KEY_FILE);
|
|
107
|
+
this.readFileFn = options.readFileFn ?? promises_1.readFile;
|
|
108
|
+
this.writeFileFn =
|
|
109
|
+
options.writeFileFn ?? ((filePath, content) => (0, fs_1.atomicWriteFile)(filePath, content, { mode: PRIVATE_FILE_MODE }));
|
|
110
|
+
this.readBufferFileFn = options.readBufferFileFn ?? ((filePath) => (0, promises_1.readFile)(filePath));
|
|
111
|
+
this.writeBufferFileFn =
|
|
112
|
+
options.writeBufferFileFn ??
|
|
113
|
+
(async (filePath, content) => {
|
|
114
|
+
await (0, fs_1.ensureDir)((0, node_path_1.dirname)(filePath));
|
|
115
|
+
await (0, promises_1.writeFile)(filePath, content, { mode: PRIVATE_FILE_MODE });
|
|
116
|
+
});
|
|
117
|
+
this.randomBytesFn = options.randomBytesFn ?? node_crypto_1.randomBytes;
|
|
118
|
+
}
|
|
119
|
+
async has(contentHash) {
|
|
120
|
+
const state = await this.loadState();
|
|
121
|
+
return Object.prototype.hasOwnProperty.call(state.entries, contentHash);
|
|
122
|
+
}
|
|
123
|
+
async get(contentHash) {
|
|
124
|
+
const state = await this.loadState();
|
|
125
|
+
const entry = state.entries[contentHash];
|
|
126
|
+
if (!entry) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...entry,
|
|
131
|
+
reasons: [...entry.reasons],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async set(contentHash, entry) {
|
|
135
|
+
const state = await this.loadState();
|
|
136
|
+
state.entries[contentHash] = {
|
|
137
|
+
path: entry.path,
|
|
138
|
+
skillName: entry.skillName,
|
|
139
|
+
decision: entry.decision,
|
|
140
|
+
risk: entry.risk,
|
|
141
|
+
reasons: [...entry.reasons],
|
|
142
|
+
scannedAt: entry.scannedAt,
|
|
143
|
+
};
|
|
144
|
+
await this.persist(state);
|
|
145
|
+
}
|
|
146
|
+
async list() {
|
|
147
|
+
const state = await this.loadState();
|
|
148
|
+
return Object.keys(state.entries)
|
|
149
|
+
.sort()
|
|
150
|
+
.map((contentHash) => ({
|
|
151
|
+
contentHash,
|
|
152
|
+
entry: {
|
|
153
|
+
...state.entries[contentHash],
|
|
154
|
+
reasons: [...state.entries[contentHash].reasons],
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
async clear() {
|
|
159
|
+
const state = await this.loadState();
|
|
160
|
+
state.entries = createEntryMap();
|
|
161
|
+
await this.persist(state);
|
|
162
|
+
}
|
|
163
|
+
async verify() {
|
|
164
|
+
const state = await this.loadState();
|
|
165
|
+
const expectedHmac = await this.computeHmac(state.entries);
|
|
166
|
+
if (state.version === SCAN_CACHE_VERSION && hmacEqual(state.hmac, expectedHmac)) {
|
|
167
|
+
return { valid: true };
|
|
168
|
+
}
|
|
169
|
+
state.version = SCAN_CACHE_VERSION;
|
|
170
|
+
state.entries = createEntryMap();
|
|
171
|
+
await this.persist(state);
|
|
172
|
+
return { valid: false };
|
|
173
|
+
}
|
|
174
|
+
async loadState() {
|
|
175
|
+
if (this.state) {
|
|
176
|
+
return this.state;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const raw = await this.readFileFn(this.cachePath, 'utf8');
|
|
180
|
+
const parsed = JSON.parse(raw);
|
|
181
|
+
const data = this.normalizeState(parsed);
|
|
182
|
+
this.state = data;
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
const empty = await this.createEmptyState();
|
|
187
|
+
this.state = empty;
|
|
188
|
+
return empty;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
normalizeState(raw) {
|
|
192
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
193
|
+
return {
|
|
194
|
+
version: SCAN_CACHE_VERSION,
|
|
195
|
+
hmac: '',
|
|
196
|
+
entries: createEntryMap(),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const record = raw;
|
|
200
|
+
const entries = normalizeEntries(record.entries);
|
|
201
|
+
const hmac = typeof record.hmac === 'string' ? record.hmac : '';
|
|
202
|
+
const version = typeof record.version === 'number' ? record.version : SCAN_CACHE_VERSION;
|
|
203
|
+
return {
|
|
204
|
+
version,
|
|
205
|
+
hmac,
|
|
206
|
+
entries,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async createEmptyState() {
|
|
210
|
+
const entries = createEntryMap();
|
|
211
|
+
return {
|
|
212
|
+
version: SCAN_CACHE_VERSION,
|
|
213
|
+
hmac: await this.computeHmac(entries),
|
|
214
|
+
entries,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async persist(state) {
|
|
218
|
+
const sorted = sortEntries(state.entries);
|
|
219
|
+
state.entries = sorted;
|
|
220
|
+
state.version = SCAN_CACHE_VERSION;
|
|
221
|
+
state.hmac = await this.computeHmac(sorted);
|
|
222
|
+
const payload = JSON.stringify({
|
|
223
|
+
version: state.version,
|
|
224
|
+
hmac: state.hmac,
|
|
225
|
+
entries: state.entries,
|
|
226
|
+
}, null, 2);
|
|
227
|
+
await this.writeFileFn(this.cachePath, `${payload}\n`);
|
|
228
|
+
}
|
|
229
|
+
async computeHmac(entries) {
|
|
230
|
+
const key = await this.getOrCreateHmacKey();
|
|
231
|
+
const canonicalEntries = JSON.stringify(sortEntries(entries));
|
|
232
|
+
return (0, node_crypto_1.createHmac)('sha256', key).update(canonicalEntries).digest('hex');
|
|
233
|
+
}
|
|
234
|
+
async getOrCreateHmacKey() {
|
|
235
|
+
if (this.hmacKey) {
|
|
236
|
+
return this.hmacKey;
|
|
237
|
+
}
|
|
238
|
+
const existing = await this.readExistingHmacKey();
|
|
239
|
+
if (existing) {
|
|
240
|
+
this.hmacKey = existing;
|
|
241
|
+
return existing;
|
|
242
|
+
}
|
|
243
|
+
const generated = this.randomBytesFn(HMAC_KEY_BYTES);
|
|
244
|
+
await this.writeBufferFileFn(this.hmacKeyPath, generated);
|
|
245
|
+
this.hmacKey = Buffer.from(generated);
|
|
246
|
+
return this.hmacKey;
|
|
247
|
+
}
|
|
248
|
+
async readExistingHmacKey() {
|
|
249
|
+
try {
|
|
250
|
+
const existing = await this.readBufferFileFn(this.hmacKeyPath);
|
|
251
|
+
if (existing.length === HMAC_KEY_BYTES) {
|
|
252
|
+
return Buffer.from(existing);
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
exports.ScanCache = ScanCache;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DismissedWarning, SkillWarning } from './types';
|
|
2
|
+
export interface WarningStoreOptions {
|
|
3
|
+
filePath?: string;
|
|
4
|
+
homeDir?: string;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
readFileFn?: (filePath: string, encoding: BufferEncoding) => Promise<string>;
|
|
7
|
+
writeFileFn?: (filePath: string, content: string) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare class WarningStore {
|
|
10
|
+
private readonly filePath;
|
|
11
|
+
private readonly now;
|
|
12
|
+
private readonly readFileFn;
|
|
13
|
+
private readonly writeFileFn;
|
|
14
|
+
private state;
|
|
15
|
+
constructor(options?: WarningStoreOptions);
|
|
16
|
+
addPending(warning: SkillWarning): Promise<void>;
|
|
17
|
+
getPending(): Promise<SkillWarning[]>;
|
|
18
|
+
getAcknowledged(): Promise<SkillWarning[]>;
|
|
19
|
+
getDismissed(): Promise<DismissedWarning[]>;
|
|
20
|
+
isDismissed(skillName: string, contentHash: string): Promise<boolean>;
|
|
21
|
+
acknowledge(skillName: string, skillPath?: string): Promise<number>;
|
|
22
|
+
acknowledgeAll(warnings: SkillWarning[]): Promise<number>;
|
|
23
|
+
dismiss(skillName: string): Promise<number>;
|
|
24
|
+
clearPending(): Promise<void>;
|
|
25
|
+
replacePending(warnings: SkillWarning[]): Promise<void>;
|
|
26
|
+
private normalizeInputWarning;
|
|
27
|
+
private isDismissedInState;
|
|
28
|
+
private loadState;
|
|
29
|
+
private normalizeState;
|
|
30
|
+
private createEmptyState;
|
|
31
|
+
private persist;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=WarningStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WarningStore.d.ts","sourceRoot":"","sources":["../../src/guard/WarningStore.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAc7D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5E,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACnE;AAgJD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiE;IAC5F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsD;IAClF,OAAO,CAAC,KAAK,CAAiC;gBAElC,OAAO,GAAE,mBAAwB;IASvC,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBhD,UAAU,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAKrC,eAAe,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAK1C,YAAY,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAK3C,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrE,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BnE,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAmCzD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmB3C,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAM7B,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B7D,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,kBAAkB;YAIZ,SAAS;IAkBvB,OAAO,CAAC,cAAc;IAsBtB,OAAO,CAAC,gBAAgB;YASV,OAAO;CAgBtB"}
|