sapper-ai 0.6.2 → 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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +148 -0
- package/dist/openclaw/contextAdapter.d.ts +8 -0
- package/dist/openclaw/contextAdapter.d.ts.map +1 -0
- package/dist/openclaw/contextAdapter.js +30 -0
- package/dist/openclaw/detect.d.ts +22 -0
- package/dist/openclaw/detect.d.ts.map +1 -0
- package/dist/openclaw/detect.js +217 -0
- package/dist/openclaw/docker/DockerSandbox.d.ts +59 -0
- package/dist/openclaw/docker/DockerSandbox.d.ts.map +1 -0
- package/dist/openclaw/docker/DockerSandbox.js +372 -0
- package/dist/openclaw/docker/HoneytokenGenerator.d.ts +20 -0
- package/dist/openclaw/docker/HoneytokenGenerator.d.ts.map +1 -0
- package/dist/openclaw/docker/HoneytokenGenerator.js +224 -0
- package/dist/openclaw/docker/OpenClawTestRunner.d.ts +26 -0
- package/dist/openclaw/docker/OpenClawTestRunner.d.ts.map +1 -0
- package/dist/openclaw/docker/OpenClawTestRunner.js +93 -0
- package/dist/openclaw/docker/TrafficAnalyzer.d.ts +16 -0
- package/dist/openclaw/docker/TrafficAnalyzer.d.ts.map +1 -0
- package/dist/openclaw/docker/TrafficAnalyzer.js +260 -0
- package/dist/openclaw/scanner.d.ts +74 -0
- package/dist/openclaw/scanner.d.ts.map +1 -0
- package/dist/openclaw/scanner.js +452 -0
- package/package.json +9 -5
- package/src/openclaw/docker/Dockerfile +23 -0
- package/src/openclaw/docker/docker-compose.yml +65 -0
- package/src/openclaw/docker/install-ca.sh +30 -0
- 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":";
|
|
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
|
@@ -49,6 +49,8 @@ const harden_1 = require("./harden");
|
|
|
49
49
|
const quarantine_1 = require("./quarantine");
|
|
50
50
|
const wrapConfig_1 = require("./mcp/wrapConfig");
|
|
51
51
|
const scan_1 = require("./scan");
|
|
52
|
+
const detect_1 = require("./openclaw/detect");
|
|
53
|
+
const scanner_1 = require("./openclaw/scanner");
|
|
52
54
|
const env_1 = require("./utils/env");
|
|
53
55
|
async function runCli(argv = process.argv.slice(2)) {
|
|
54
56
|
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
@@ -83,6 +85,17 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
83
85
|
}
|
|
84
86
|
return scanExitCode;
|
|
85
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
|
+
}
|
|
86
99
|
if (argv[0] === 'harden') {
|
|
87
100
|
const parsed = parseHardenArgs(argv.slice(1));
|
|
88
101
|
if (!parsed) {
|
|
@@ -135,6 +148,7 @@ Usage:
|
|
|
135
148
|
sapper-ai scan --harden After scan, offer to apply recommended hardening
|
|
136
149
|
sapper-ai scan --no-open Skip opening report in browser
|
|
137
150
|
sapper-ai scan --no-save Skip saving scan results to ~/.sapperai/scans/
|
|
151
|
+
sapper-ai openclaw OpenClaw skill security scanner
|
|
138
152
|
sapper-ai harden Plan recommended setup changes (no writes)
|
|
139
153
|
sapper-ai harden --apply Apply recommended project changes
|
|
140
154
|
sapper-ai harden --include-system Include system changes (home directory)
|
|
@@ -440,6 +454,140 @@ function displayPath(path) {
|
|
|
440
454
|
return '~';
|
|
441
455
|
return path.startsWith(home + '/') ? `~/${path.slice(home.length + 1)}` : path;
|
|
442
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
|
+
}
|
|
443
591
|
async function promptScanScope(cwd) {
|
|
444
592
|
const answer = await (0, select_1.default)({
|
|
445
593
|
message: 'Scan scope:',
|
|
@@ -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"}
|