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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +148 -42
- 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/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +87 -56
- package/dist/utils/progress.d.ts +40 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +113 -0
- package/package.json +9 -13
- 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
|
@@ -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"}
|