sapper-ai 0.6.1 → 0.7.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.
Files changed (33) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +148 -42
  3. package/dist/openclaw/contextAdapter.d.ts +8 -0
  4. package/dist/openclaw/contextAdapter.d.ts.map +1 -0
  5. package/dist/openclaw/contextAdapter.js +30 -0
  6. package/dist/openclaw/detect.d.ts +22 -0
  7. package/dist/openclaw/detect.d.ts.map +1 -0
  8. package/dist/openclaw/detect.js +217 -0
  9. package/dist/openclaw/docker/DockerSandbox.d.ts +59 -0
  10. package/dist/openclaw/docker/DockerSandbox.d.ts.map +1 -0
  11. package/dist/openclaw/docker/DockerSandbox.js +372 -0
  12. package/dist/openclaw/docker/HoneytokenGenerator.d.ts +20 -0
  13. package/dist/openclaw/docker/HoneytokenGenerator.d.ts.map +1 -0
  14. package/dist/openclaw/docker/HoneytokenGenerator.js +224 -0
  15. package/dist/openclaw/docker/OpenClawTestRunner.d.ts +26 -0
  16. package/dist/openclaw/docker/OpenClawTestRunner.d.ts.map +1 -0
  17. package/dist/openclaw/docker/OpenClawTestRunner.js +93 -0
  18. package/dist/openclaw/docker/TrafficAnalyzer.d.ts +16 -0
  19. package/dist/openclaw/docker/TrafficAnalyzer.d.ts.map +1 -0
  20. package/dist/openclaw/docker/TrafficAnalyzer.js +260 -0
  21. package/dist/openclaw/scanner.d.ts +74 -0
  22. package/dist/openclaw/scanner.d.ts.map +1 -0
  23. package/dist/openclaw/scanner.js +452 -0
  24. package/dist/scan.d.ts.map +1 -1
  25. package/dist/scan.js +87 -56
  26. package/dist/utils/progress.d.ts +40 -0
  27. package/dist/utils/progress.d.ts.map +1 -0
  28. package/dist/utils/progress.js +113 -0
  29. package/package.json +9 -13
  30. package/src/openclaw/docker/Dockerfile +23 -0
  31. package/src/openclaw/docker/docker-compose.yml +65 -0
  32. package/src/openclaw/docker/install-ca.sh +30 -0
  33. package/src/openclaw/docker/test-runner.sh +125 -0
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAkBA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAuFpF"}
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"}
package/dist/cli.js CHANGED
@@ -39,7 +39,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.runCli = runCli;
41
41
  const node_fs_1 = require("node:fs");
42
- const node_child_process_1 = require("node:child_process");
43
42
  const node_os_1 = require("node:os");
44
43
  const node_path_1 = require("node:path");
45
44
  const readline = __importStar(require("node:readline"));
@@ -50,6 +49,8 @@ const harden_1 = require("./harden");
50
49
  const quarantine_1 = require("./quarantine");
51
50
  const wrapConfig_1 = require("./mcp/wrapConfig");
52
51
  const scan_1 = require("./scan");
52
+ const detect_1 = require("./openclaw/detect");
53
+ const scanner_1 = require("./openclaw/scanner");
53
54
  const env_1 = require("./utils/env");
54
55
  async function runCli(argv = process.argv.slice(2)) {
55
56
  if (argv[0] === '--help' || argv[0] === '-h') {
@@ -84,6 +85,17 @@ async function runCli(argv = process.argv.slice(2)) {
84
85
  }
85
86
  return scanExitCode;
86
87
  }
88
+ if (argv[0] === 'openclaw') {
89
+ if (argv[1] === '--help' || argv[1] === '-h') {
90
+ printUsage();
91
+ return 0;
92
+ }
93
+ if (argv.length > 1) {
94
+ printUsage();
95
+ return 1;
96
+ }
97
+ return runOpenClawWizard();
98
+ }
87
99
  if (argv[0] === 'harden') {
88
100
  const parsed = parseHardenArgs(argv.slice(1));
89
101
  if (!parsed) {
@@ -111,9 +123,6 @@ async function runCli(argv = process.argv.slice(2)) {
111
123
  }
112
124
  return (0, quarantine_1.runQuarantineRestore)({ id: parsed.id, quarantineDir: parsed.quarantineDir, force: parsed.force });
113
125
  }
114
- if (argv[0] === 'dashboard') {
115
- return runDashboard();
116
- }
117
126
  if (argv[0] !== 'init') {
118
127
  printUsage();
119
128
  return 1;
@@ -139,6 +148,7 @@ Usage:
139
148
  sapper-ai scan --harden After scan, offer to apply recommended hardening
140
149
  sapper-ai scan --no-open Skip opening report in browser
141
150
  sapper-ai scan --no-save Skip saving scan results to ~/.sapperai/scans/
151
+ sapper-ai openclaw OpenClaw skill security scanner
142
152
  sapper-ai harden Plan recommended setup changes (no writes)
143
153
  sapper-ai harden --apply Apply recommended project changes
144
154
  sapper-ai harden --include-system Include system changes (home directory)
@@ -147,7 +157,6 @@ Usage:
147
157
  sapper-ai quarantine list List quarantined files
148
158
  sapper-ai quarantine restore <id> [--force] Restore quarantined file by id
149
159
  sapper-ai init Interactive setup wizard
150
- sapper-ai dashboard Launch web dashboard
151
160
  sapper-ai --help Show this help
152
161
 
153
162
  Learn more: https://github.com/sapper-ai/sapperai
@@ -445,6 +454,140 @@ function displayPath(path) {
445
454
  return '~';
446
455
  return path.startsWith(home + '/') ? `~/${path.slice(home.length + 1)}` : path;
447
456
  }
457
+ function defaultOpenClawAction(dockerAvailable) {
458
+ return dockerAvailable ? 'scan_static_dynamic' : 'scan_static_only';
459
+ }
460
+ function isOpenClawPromptEnabled() {
461
+ return process.stdout.isTTY === true && process.stdin.isTTY === true && (0, env_1.isCiEnv)(process.env) !== true;
462
+ }
463
+ async function promptOpenClawAction(dockerAvailable) {
464
+ if (!isOpenClawPromptEnabled()) {
465
+ return defaultOpenClawAction(dockerAvailable);
466
+ }
467
+ const choices = [];
468
+ if (dockerAvailable) {
469
+ choices.push({
470
+ name: 'Scan all skills (static + dynamic analysis)',
471
+ value: 'scan_static_dynamic',
472
+ });
473
+ }
474
+ choices.push({
475
+ name: 'Scan all skills (static only)',
476
+ value: 'scan_static_only',
477
+ });
478
+ choices.push({
479
+ name: 'Harden configuration',
480
+ value: 'harden',
481
+ });
482
+ return (0, select_1.default)({
483
+ message: 'What would you like to do?',
484
+ choices,
485
+ default: defaultOpenClawAction(dockerAvailable),
486
+ });
487
+ }
488
+ function createOpenClawProgressHandler() {
489
+ if (process.stdout.isTTY !== true) {
490
+ return () => { };
491
+ }
492
+ let previousPhase = null;
493
+ return (event) => {
494
+ const label = event.phase === 'static' ? ' Phase 1 - Static analysis' : ' Phase 2 - Dynamic analysis';
495
+ if (previousPhase !== null && previousPhase !== event.phase) {
496
+ process.stdout.write('\n');
497
+ }
498
+ previousPhase = event.phase;
499
+ process.stdout.write(`\r${label}: ${event.completed}/${event.total}`);
500
+ if (event.completed >= event.total) {
501
+ process.stdout.write('\n');
502
+ }
503
+ };
504
+ }
505
+ async function runOpenClawWizard() {
506
+ console.log('\n Detecting your environment...\n');
507
+ const environment = await (0, detect_1.detectOpenClawEnvironment)({ cwd: process.cwd() });
508
+ console.log(' Found:');
509
+ if (environment.installed) {
510
+ const versionSuffix = environment.version ? ` (v${environment.version})` : '';
511
+ console.log(` OpenClaw Gateway${versionSuffix}`);
512
+ }
513
+ else {
514
+ console.log(' OpenClaw Gateway: not detected');
515
+ }
516
+ if (environment.skillsPaths.length === 0) {
517
+ console.log(' Skills directory: not detected');
518
+ }
519
+ else {
520
+ console.log(` Skills directories: ${environment.skillsPaths.length}`);
521
+ for (const skillPath of environment.skillsPaths) {
522
+ console.log(` - ${displayPath(skillPath)}`);
523
+ }
524
+ }
525
+ console.log(` Skills discovered: ${environment.skillCount}`);
526
+ if (environment.dockerAvailable) {
527
+ const composeStatus = environment.dockerComposeAvailable ? 'yes' : 'no';
528
+ console.log(` Docker: available (compose: ${composeStatus})`);
529
+ }
530
+ else {
531
+ console.log(' Docker: not available');
532
+ }
533
+ console.log();
534
+ if (!environment.installed && environment.skillsPaths.length === 0) {
535
+ console.log(' OpenClaw not detected. Add skills under ~/.openclaw/skills or ./skills and rerun.\n');
536
+ return 1;
537
+ }
538
+ const action = await promptOpenClawAction(environment.dockerAvailable);
539
+ if (action === 'harden') {
540
+ return (0, harden_1.runHarden)({
541
+ apply: true,
542
+ includeSystem: true,
543
+ });
544
+ }
545
+ if (environment.skillCount === 0) {
546
+ console.log(' No skill markdown files found in detected paths.\n');
547
+ return 0;
548
+ }
549
+ const dynamicRequested = action === 'scan_static_dynamic' && environment.dockerAvailable;
550
+ const policy = (0, scanner_1.resolveOpenClawPolicy)({ cwd: process.cwd() });
551
+ const scanResult = await (0, scanner_1.scanSkills)(environment.skillsPaths, policy, {
552
+ dynamicAnalysis: dynamicRequested,
553
+ quarantineOnRisk: true,
554
+ quarantineDir: process.env.SAPPERAI_QUARANTINE_DIR ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'quarantine'),
555
+ onProgress: createOpenClawProgressHandler(),
556
+ });
557
+ const safeCount = scanResult.results.filter((entry) => entry.decision === 'safe').length;
558
+ const suspiciousCount = scanResult.results.filter((entry) => entry.decision === 'suspicious').length;
559
+ const quarantinedCount = scanResult.results.filter((entry) => entry.decision === 'quarantined').length;
560
+ console.log('\n Results:');
561
+ console.log(` ${safeCount} skills safe`);
562
+ console.log(` ${suspiciousCount} skills suspicious`);
563
+ console.log(` ${quarantinedCount} skills quarantined`);
564
+ if (dynamicRequested && scanResult.dynamicStatus === 'skipped_unconfigured') {
565
+ console.log('\n Dynamic analysis requested but no dynamic analyzer is configured yet.');
566
+ console.log(' Static analysis results are shown.\n');
567
+ }
568
+ if (dynamicRequested && scanResult.dynamicStatus === 'skipped_unavailable') {
569
+ console.log('\n Dynamic analysis requested but the dynamic analyzer is unavailable in this environment.');
570
+ console.log(' Static analysis results are shown.\n');
571
+ }
572
+ if (quarantinedCount > 0) {
573
+ console.log('\n Quarantined skills:');
574
+ const quarantined = scanResult.results.filter((entry) => entry.decision === 'quarantined');
575
+ for (const entry of quarantined) {
576
+ const reasons = entry.dynamicResult?.findings?.length
577
+ ? entry.dynamicResult.findings.map((finding) => `${finding.honeytoken.envVar} -> ${finding.destination}`)
578
+ : entry.staticResult?.reasons ?? [];
579
+ const reasonText = reasons.length > 0 ? reasons[0] : 'High-risk behavior detected';
580
+ console.log(` - ${entry.skillName} (${displayPath(entry.skillPath)}): ${reasonText}`);
581
+ }
582
+ }
583
+ if (suspiciousCount > 0) {
584
+ console.log('\n Suspicious skills require manual review.\n');
585
+ }
586
+ else {
587
+ console.log('\n Scan complete.\n');
588
+ }
589
+ return quarantinedCount > 0 || suspiciousCount > 0 ? 1 : 0;
590
+ }
448
591
  async function promptScanScope(cwd) {
449
592
  const answer = await (0, select_1.default)({
450
593
  message: 'Scan scope:',
@@ -524,43 +667,6 @@ async function resolveScanOptions(args) {
524
667
  }
525
668
  return { ...common, targets: [cwd], deep: true, ai, scopeLabel: 'Current + subdirectories' };
526
669
  }
527
- async function runDashboard() {
528
- const configuredPort = process.env.PORT;
529
- const standalonePort = configuredPort ?? '4100';
530
- const devPort = configuredPort ?? '3000';
531
- try {
532
- // eslint-disable-next-line @typescript-eslint/no-var-requires
533
- const startPath = require.resolve('@sapper-ai/dashboard/bin/start');
534
- process.env.PORT = standalonePort;
535
- // eslint-disable-next-line @typescript-eslint/no-var-requires
536
- require(startPath);
537
- return await new Promise((resolveExit) => {
538
- const stop = () => resolveExit(0);
539
- process.once('SIGINT', stop);
540
- process.once('SIGTERM', stop);
541
- });
542
- }
543
- catch {
544
- }
545
- const webDir = (0, node_path_1.resolve)(__dirname, '../../../apps/web');
546
- if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(webDir, 'package.json'))) {
547
- console.log(`\n SapperAI Dashboard (dev): http://localhost:${devPort}/dashboard\n`);
548
- console.log(' Press Ctrl+C to stop\n');
549
- const child = (0, node_child_process_1.spawn)('npx', ['next', 'dev', '--port', devPort], {
550
- cwd: webDir,
551
- stdio: 'inherit',
552
- env: process.env,
553
- });
554
- process.on('SIGINT', () => child.kill('SIGINT'));
555
- process.on('SIGTERM', () => child.kill('SIGTERM'));
556
- return await new Promise((resolveExit) => {
557
- child.on('close', (code) => resolveExit(code ?? 0));
558
- });
559
- }
560
- console.error('\n Install @sapper-ai/dashboard for standalone mode:');
561
- console.error(' pnpm add @sapper-ai/dashboard\n');
562
- return 1;
563
- }
564
670
  async function runInitWizard() {
565
671
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
566
672
  const ask = (q) => new Promise((res) => rl.question(q, res));
@@ -0,0 +1,8 @@
1
+ import type { ParsedSkill } from '@sapper-ai/core';
2
+ import type { AssessmentContext, Policy } from '@sapper-ai/types';
3
+ export interface SkillAssessmentContextOptions {
4
+ skillPath?: string;
5
+ scanSource?: 'file_surface' | 'watch_surface';
6
+ }
7
+ export declare function skillToAssessmentContext(parsed: ParsedSkill, policy: Policy, options?: SkillAssessmentContextOptions): AssessmentContext;
8
+ //# sourceMappingURL=contextAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contextAdapter.d.ts","sourceRoot":"","sources":["../../src/openclaw/contextAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEjE,MAAM,WAAW,6BAA6B;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,cAAc,GAAG,eAAe,CAAA;CAC9C;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,6BAAkC,GAC1C,iBAAiB,CA8BnB"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.skillToAssessmentContext = skillToAssessmentContext;
4
+ function skillToAssessmentContext(parsed, policy, options = {}) {
5
+ const meta = {};
6
+ if (parsed.metadata.homepage) {
7
+ meta.homepage = parsed.metadata.homepage;
8
+ }
9
+ if (parsed.metadata.requires && parsed.metadata.requires.length > 0) {
10
+ meta.requires = parsed.metadata.requires;
11
+ }
12
+ if (options.skillPath) {
13
+ meta.scanSource = options.scanSource ?? 'file_surface';
14
+ meta.sourcePath = options.skillPath;
15
+ meta.sourceType = 'skill';
16
+ }
17
+ return {
18
+ kind: 'install_scan',
19
+ toolCall: {
20
+ toolName: 'skill_install',
21
+ arguments: {
22
+ skillName: parsed.metadata.name,
23
+ content: parsed.body,
24
+ frontmatter: parsed.raw,
25
+ },
26
+ },
27
+ meta,
28
+ policy,
29
+ };
30
+ }
@@ -0,0 +1,22 @@
1
+ export interface OpenClawEnvironment {
2
+ installed: boolean;
3
+ version?: string;
4
+ skillsPaths: string[];
5
+ skillCount: number;
6
+ dockerAvailable: boolean;
7
+ dockerComposeAvailable: boolean;
8
+ }
9
+ interface CommandRunResult {
10
+ ok: boolean;
11
+ stdout: string;
12
+ stderr: string;
13
+ }
14
+ type CommandRunner = (command: string, args: string[]) => Promise<CommandRunResult>;
15
+ export interface DetectOpenClawEnvironmentOptions {
16
+ cwd?: string;
17
+ homeDir?: string;
18
+ commandRunner?: CommandRunner;
19
+ }
20
+ export declare function detectOpenClawEnvironment(options?: DetectOpenClawEnvironmentOptions): Promise<OpenClawEnvironment>;
21
+ export {};
22
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/openclaw/detect.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,OAAO,CAAA;IACxB,sBAAsB,EAAE,OAAO,CAAA;CAChC;AAED,UAAU,gBAAgB;IACxB,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED,KAAK,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAA;AAEnF,MAAM,WAAW,gCAAgC;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;AAuOD,wBAAsB,yBAAyB,CAC7C,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,mBAAmB,CAAC,CA0B9B"}
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectOpenClawEnvironment = detectOpenClawEnvironment;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ function whichCommand() {
9
+ return process.platform === 'win32' ? 'where' : 'which';
10
+ }
11
+ async function defaultCommandRunner(command, args) {
12
+ return new Promise((resolveResult) => {
13
+ (0, node_child_process_1.execFile)(command, args, { encoding: 'utf8' }, (error, stdout, stderr) => {
14
+ resolveResult({
15
+ ok: !error,
16
+ stdout: typeof stdout === 'string' ? stdout : '',
17
+ stderr: typeof stderr === 'string' ? stderr : '',
18
+ });
19
+ });
20
+ });
21
+ }
22
+ async function hasCommand(binary, runCommand) {
23
+ const result = await runCommand(whichCommand(), [binary]);
24
+ return result.ok;
25
+ }
26
+ async function pathExists(path) {
27
+ try {
28
+ await (0, promises_1.stat)(path);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ async function isDirectory(path) {
36
+ try {
37
+ const info = await (0, promises_1.stat)(path);
38
+ return info.isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ function uniqueStrings(values) {
45
+ return Array.from(new Set(values));
46
+ }
47
+ function expandHomePath(value, homeDir) {
48
+ if (value === '~') {
49
+ return homeDir;
50
+ }
51
+ if (value.startsWith('~/')) {
52
+ return (0, node_path_1.join)(homeDir, value.slice(2));
53
+ }
54
+ return value;
55
+ }
56
+ function normalizeCandidatePath(value, baseDir, homeDir) {
57
+ const expanded = expandHomePath(value.trim(), homeDir);
58
+ if ((0, node_path_1.isAbsolute)(expanded)) {
59
+ return (0, node_path_1.resolve)(expanded);
60
+ }
61
+ return (0, node_path_1.resolve)(baseDir, expanded);
62
+ }
63
+ function extractExtraSkillDirs(config) {
64
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
65
+ return [];
66
+ }
67
+ const skills = config.skills;
68
+ if (!skills || typeof skills !== 'object' || Array.isArray(skills)) {
69
+ return [];
70
+ }
71
+ const load = skills.load;
72
+ if (!load || typeof load !== 'object' || Array.isArray(load)) {
73
+ return [];
74
+ }
75
+ const extraDirs = load.extraDirs;
76
+ if (!Array.isArray(extraDirs)) {
77
+ return [];
78
+ }
79
+ return extraDirs.filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
80
+ }
81
+ async function readConfigSkillDirs(configPath, homeDir) {
82
+ const exists = await pathExists(configPath);
83
+ if (!exists) {
84
+ return [];
85
+ }
86
+ let raw = '';
87
+ try {
88
+ raw = await (0, promises_1.readFile)(configPath, 'utf8');
89
+ }
90
+ catch {
91
+ return [];
92
+ }
93
+ if (raw.trim().length === 0) {
94
+ return [];
95
+ }
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(raw);
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ const baseDir = (0, node_path_1.dirname)(configPath);
104
+ return extractExtraSkillDirs(parsed).map((entry) => normalizeCandidatePath(entry, baseDir, homeDir));
105
+ }
106
+ async function collectExistingSkillPaths(candidates) {
107
+ const uniqueCandidates = uniqueStrings(candidates.map((candidate) => (0, node_path_1.resolve)(candidate)));
108
+ const existingPaths = [];
109
+ for (const candidate of uniqueCandidates) {
110
+ if (await isDirectory(candidate)) {
111
+ existingPaths.push(candidate);
112
+ }
113
+ }
114
+ return existingPaths;
115
+ }
116
+ async function collectMarkdownFiles(rootPath) {
117
+ const markdownFiles = [];
118
+ const stack = [rootPath];
119
+ while (stack.length > 0) {
120
+ const current = stack.pop();
121
+ if (!current) {
122
+ continue;
123
+ }
124
+ let entries;
125
+ try {
126
+ entries = await (0, promises_1.readdir)(current, { withFileTypes: true });
127
+ }
128
+ catch {
129
+ continue;
130
+ }
131
+ for (const entry of entries) {
132
+ const fullPath = (0, node_path_1.join)(current, entry.name);
133
+ if (entry.isDirectory()) {
134
+ stack.push(fullPath);
135
+ continue;
136
+ }
137
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
138
+ markdownFiles.push(fullPath);
139
+ }
140
+ }
141
+ }
142
+ return markdownFiles;
143
+ }
144
+ async function countSkillFiles(skillPaths) {
145
+ const seen = new Set();
146
+ for (const skillPath of skillPaths) {
147
+ const files = await collectMarkdownFiles(skillPath);
148
+ for (const file of files) {
149
+ seen.add(file);
150
+ }
151
+ }
152
+ return seen.size;
153
+ }
154
+ function parseOpenClawVersion(output) {
155
+ const line = output
156
+ .split(/\r?\n/)
157
+ .map((entry) => entry.trim())
158
+ .find((entry) => entry.length > 0);
159
+ if (!line) {
160
+ return undefined;
161
+ }
162
+ const matchedVersion = line.match(/(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)/);
163
+ return matchedVersion?.[1] ?? line;
164
+ }
165
+ async function resolveOpenClawVersion(runCommand, hasOpenClawBinary) {
166
+ if (!hasOpenClawBinary) {
167
+ return undefined;
168
+ }
169
+ const versionResult = await runCommand('openclaw', ['--version']);
170
+ if (!versionResult.ok) {
171
+ return undefined;
172
+ }
173
+ return parseOpenClawVersion(versionResult.stdout);
174
+ }
175
+ async function resolveDockerAvailability(runCommand) {
176
+ const dockerInstalled = await hasCommand('docker', runCommand);
177
+ if (!dockerInstalled) {
178
+ return {
179
+ dockerAvailable: false,
180
+ dockerComposeAvailable: false,
181
+ };
182
+ }
183
+ const dockerInfoResult = await runCommand('docker', ['info']);
184
+ if (!dockerInfoResult.ok) {
185
+ return {
186
+ dockerAvailable: false,
187
+ dockerComposeAvailable: false,
188
+ };
189
+ }
190
+ const dockerComposeResult = await runCommand('docker', ['compose', 'version']);
191
+ return {
192
+ dockerAvailable: true,
193
+ dockerComposeAvailable: dockerComposeResult.ok,
194
+ };
195
+ }
196
+ async function detectOpenClawEnvironment(options = {}) {
197
+ const runCommand = options.commandRunner ?? defaultCommandRunner;
198
+ const homeDir = options.homeDir ?? (0, node_os_1.homedir)();
199
+ const cwd = options.cwd ?? process.cwd();
200
+ const openClawHomeDir = (0, node_path_1.join)(homeDir, '.openclaw');
201
+ const hasOpenClawBinary = await hasCommand('openclaw', runCommand);
202
+ const hasOpenClawHomeDir = await isDirectory(openClawHomeDir);
203
+ const version = await resolveOpenClawVersion(runCommand, hasOpenClawBinary);
204
+ const configSkillDirs = await readConfigSkillDirs((0, node_path_1.join)(openClawHomeDir, 'config.json'), homeDir);
205
+ const skillPathCandidates = [(0, node_path_1.join)(openClawHomeDir, 'skills'), (0, node_path_1.join)(cwd, 'skills'), ...configSkillDirs];
206
+ const skillsPaths = await collectExistingSkillPaths(skillPathCandidates);
207
+ const skillCount = await countSkillFiles(skillsPaths);
208
+ const docker = await resolveDockerAvailability(runCommand);
209
+ return {
210
+ installed: hasOpenClawBinary || hasOpenClawHomeDir || skillsPaths.length > 0,
211
+ version,
212
+ skillsPaths,
213
+ skillCount,
214
+ dockerAvailable: docker.dockerAvailable,
215
+ dockerComposeAvailable: docker.dockerComposeAvailable,
216
+ };
217
+ }
@@ -0,0 +1,59 @@
1
+ import type { Honeytoken } from '@sapper-ai/types';
2
+ export interface DockerCommandRunOptions {
3
+ cwd?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ timeoutMs?: number;
6
+ }
7
+ export interface DockerCommandRunResult {
8
+ ok: boolean;
9
+ exitCode: number | null;
10
+ stdout: string;
11
+ stderr: string;
12
+ timedOut: boolean;
13
+ }
14
+ export type DockerCommandRunner = (command: string, args: string[], options?: DockerCommandRunOptions) => Promise<DockerCommandRunResult>;
15
+ export interface DockerSandboxOptions {
16
+ commandRunner?: DockerCommandRunner;
17
+ assetsDir?: string;
18
+ imageTag?: string;
19
+ runTimeoutMs?: number;
20
+ readyTimeoutMs?: number;
21
+ buildTimeoutMs?: number;
22
+ }
23
+ export interface DockerSandboxRunResult {
24
+ sandboxId: string;
25
+ projectName: string;
26
+ openclawContainerId: string;
27
+ proxyContainerId: string;
28
+ durationMs: number;
29
+ ready: true;
30
+ }
31
+ export declare function defaultDockerCommandRunner(command: string, args: string[], options?: DockerCommandRunOptions): Promise<DockerCommandRunResult>;
32
+ export declare class DockerSandbox {
33
+ private readonly commandRunner;
34
+ private readonly imageTag;
35
+ private readonly runTimeoutMs;
36
+ private readonly readyTimeoutMs;
37
+ private readonly buildTimeoutMs;
38
+ private readonly configuredAssetsDir?;
39
+ private readonly sandboxes;
40
+ private composeInvocation;
41
+ constructor(options?: DockerSandboxOptions);
42
+ prepare(skillPath: string, honeytokens: Honeytoken[]): Promise<string>;
43
+ run(sandboxId: string, timeoutMs?: number): Promise<DockerSandboxRunResult>;
44
+ getTrafficLog(sandboxId: string): Promise<string>;
45
+ cleanup(sandboxId: string): Promise<void>;
46
+ getOpenclawContainerId(sandboxId: string): string | undefined;
47
+ getProxyContainerId(sandboxId: string): string | undefined;
48
+ private writePersonaProfile;
49
+ private renderHoneytokenEnvFile;
50
+ private ensureOpenClawImage;
51
+ private assertDockerAvailable;
52
+ private resolveComposeInvocation;
53
+ private resolveAssetsDir;
54
+ private isAssetDirectory;
55
+ private runCompose;
56
+ private resolveContainerId;
57
+ private waitForReady;
58
+ }
59
+ //# sourceMappingURL=DockerSandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DockerSandbox.d.ts","sourceRoot":"","sources":["../../../src/openclaw/docker/DockerSandbox.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AA8BlD,MAAM,WAAW,uBAAuB;IACtC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,OAAO,CAAA;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,uBAAuB,KAC9B,OAAO,CAAC,sBAAsB,CAAC,CAAA;AAEpC,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,IAAI,CAAA;CACZ;AA8CD,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,sBAAsB,CAAC,CA+CjC;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,OAAO,CAAC,iBAAiB,CAAiC;gBAE9C,OAAO,GAAE,oBAAyB;IASxC,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA4DtE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAkC3E,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmCjD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B/C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7D,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;YAI5C,mBAAmB;IAmBjC,OAAO,CAAC,uBAAuB;YAsBjB,mBAAmB;YAcnB,qBAAqB;YAOrB,wBAAwB;YA4BxB,gBAAgB;YAwBhB,gBAAgB;YAUhB,UAAU;YAyBV,kBAAkB;YAkBlB,YAAY;CAgB3B"}