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 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":";AAmBA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiGpF"}
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"}