@js-eyes/protocol 2.4.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/index.js +267 -0
- package/package.json +31 -0
- package/skills.js +581 -0
- package/zip-extract.js +208 -0
package/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pkg = require('./package.json');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SERVER_HOST = 'localhost';
|
|
6
|
+
const DEFAULT_SERVER_PORT = 18080;
|
|
7
|
+
const DEFAULT_REQUEST_TIMEOUT_SECONDS = 1800;
|
|
8
|
+
const REQUEST_TIMEOUT_MS = DEFAULT_REQUEST_TIMEOUT_SECONDS * 1000;
|
|
9
|
+
const PROTOCOL_VERSION = '1.0';
|
|
10
|
+
const PACKAGE_VERSION = pkg.version;
|
|
11
|
+
const SKILLS_REGISTRY_URL = 'https://js-eyes.com/skills.json';
|
|
12
|
+
const RELEASE_BASE_URL = 'https://github.com/imjszhang/js-eyes/releases/download';
|
|
13
|
+
|
|
14
|
+
const FORWARDABLE_ACTIONS = [
|
|
15
|
+
'open_url',
|
|
16
|
+
'close_tab',
|
|
17
|
+
'get_html',
|
|
18
|
+
'execute_script',
|
|
19
|
+
'inject_css',
|
|
20
|
+
'get_cookies',
|
|
21
|
+
'get_cookies_by_domain',
|
|
22
|
+
'get_page_info',
|
|
23
|
+
'upload_file_to_tab',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const SENSITIVE_TOOL_NAMES = Object.freeze([
|
|
27
|
+
'js_eyes_execute_script',
|
|
28
|
+
'js_eyes_get_cookies',
|
|
29
|
+
'js_eyes_get_cookies_by_domain',
|
|
30
|
+
'js_eyes_inject_css',
|
|
31
|
+
'js_eyes_upload_file',
|
|
32
|
+
'js_eyes_upload_file_to_tab',
|
|
33
|
+
'js_eyes_install_skill',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const LOOPBACK_HOSTS = Object.freeze(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1', '0:0:0:0:0:0:0:1']);
|
|
37
|
+
|
|
38
|
+
const DEFAULT_ALLOWED_ORIGINS = Object.freeze([
|
|
39
|
+
'chrome-extension://*',
|
|
40
|
+
'moz-extension://*',
|
|
41
|
+
'http://localhost',
|
|
42
|
+
'https://localhost',
|
|
43
|
+
'http://127.0.0.1',
|
|
44
|
+
'https://127.0.0.1',
|
|
45
|
+
'http://[::1]',
|
|
46
|
+
'https://[::1]',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const DEFAULT_TASK_ORIGIN_CONFIG = Object.freeze({
|
|
50
|
+
enabled: true,
|
|
51
|
+
sources: ['user-message', 'skill-platforms', 'active-tab', 'fetched-links'],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const DEFAULT_TAINT_CONFIG = Object.freeze({
|
|
55
|
+
enabled: true,
|
|
56
|
+
mode: 'canary+substring',
|
|
57
|
+
minValueLength: 6,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const DEFAULT_PROFILE_CONFIG = Object.freeze({
|
|
61
|
+
default: 'full',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const POLICY_ENFORCEMENT_LEVELS = Object.freeze(['off', 'soft', 'strict']);
|
|
65
|
+
|
|
66
|
+
const DEFAULT_SECURITY_CONFIG = Object.freeze({
|
|
67
|
+
allowAnonymous: false,
|
|
68
|
+
allowedOrigins: DEFAULT_ALLOWED_ORIGINS.slice(),
|
|
69
|
+
allowRemoteBind: false,
|
|
70
|
+
allowRawEval: false,
|
|
71
|
+
requireLockfile: true,
|
|
72
|
+
enforcement: 'soft',
|
|
73
|
+
taskOrigin: { ...DEFAULT_TASK_ORIGIN_CONFIG, sources: DEFAULT_TASK_ORIGIN_CONFIG.sources.slice() },
|
|
74
|
+
egressAllowlist: [],
|
|
75
|
+
taint: { ...DEFAULT_TAINT_CONFIG },
|
|
76
|
+
profile: { ...DEFAULT_PROFILE_CONFIG },
|
|
77
|
+
toolPolicies: {
|
|
78
|
+
js_eyes_execute_script: 'confirm',
|
|
79
|
+
js_eyes_get_cookies: 'confirm',
|
|
80
|
+
js_eyes_get_cookies_by_domain: 'confirm',
|
|
81
|
+
js_eyes_inject_css: 'confirm',
|
|
82
|
+
js_eyes_upload_file: 'confirm',
|
|
83
|
+
js_eyes_upload_file_to_tab: 'confirm',
|
|
84
|
+
js_eyes_install_skill: 'confirm',
|
|
85
|
+
},
|
|
86
|
+
sensitiveCookieDomains: [
|
|
87
|
+
'bank',
|
|
88
|
+
'paypal.com',
|
|
89
|
+
'google.com',
|
|
90
|
+
'live.com',
|
|
91
|
+
'apple.com',
|
|
92
|
+
'icloud.com',
|
|
93
|
+
'aws.amazon.com',
|
|
94
|
+
'amazon.com',
|
|
95
|
+
'office.com',
|
|
96
|
+
'microsoft.com',
|
|
97
|
+
'github.com',
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const WS_CLOSE_CODE_AUTH_REQUIRED = 4401;
|
|
102
|
+
const WS_CLOSE_CODE_FORBIDDEN_ORIGIN = 4403;
|
|
103
|
+
|
|
104
|
+
const COMPATIBILITY_MATRIX = Object.freeze({
|
|
105
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
106
|
+
cliVersion: PACKAGE_VERSION,
|
|
107
|
+
extensionVersion: PACKAGE_VERSION,
|
|
108
|
+
serverCoreVersion: PACKAGE_VERSION,
|
|
109
|
+
clientSdkVersion: PACKAGE_VERSION,
|
|
110
|
+
openclawPluginVersion: PACKAGE_VERSION,
|
|
111
|
+
skillClientSdkVersion: PACKAGE_VERSION,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function isLoopbackHost(host) {
|
|
115
|
+
if (!host) return false;
|
|
116
|
+
const normalized = String(host).toLowerCase();
|
|
117
|
+
if (LOOPBACK_HOSTS.includes(normalized)) return true;
|
|
118
|
+
if (normalized.startsWith('127.')) return true;
|
|
119
|
+
if (normalized === '::ffff:127.0.0.1') return true;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeOriginPattern(pattern) {
|
|
124
|
+
if (!pattern) return null;
|
|
125
|
+
const trimmed = String(pattern).trim().toLowerCase();
|
|
126
|
+
if (!trimmed) return null;
|
|
127
|
+
return trimmed.replace(/\/$/, '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function matchesOriginPattern(origin, pattern) {
|
|
131
|
+
if (!origin || !pattern) return false;
|
|
132
|
+
const normOrigin = String(origin).trim().toLowerCase().replace(/\/$/, '');
|
|
133
|
+
const normPattern = normalizeOriginPattern(pattern);
|
|
134
|
+
if (!normPattern) return false;
|
|
135
|
+
if (normPattern === '*') return true;
|
|
136
|
+
if (normPattern.endsWith('://*')) {
|
|
137
|
+
const scheme = normPattern.slice(0, -3);
|
|
138
|
+
return normOrigin.startsWith(scheme);
|
|
139
|
+
}
|
|
140
|
+
if (normPattern.includes('*')) {
|
|
141
|
+
const regex = new RegExp(
|
|
142
|
+
'^' + normPattern.split('*').map(escapeRegex).join('.*') + '$',
|
|
143
|
+
);
|
|
144
|
+
return regex.test(normOrigin);
|
|
145
|
+
}
|
|
146
|
+
if (normOrigin === normPattern) return true;
|
|
147
|
+
if (normOrigin.startsWith(normPattern + ':')) return true;
|
|
148
|
+
if (normOrigin.startsWith(normPattern + '/')) return true;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function escapeRegex(text) {
|
|
153
|
+
return text.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isOriginAllowed(origin, allowedOrigins) {
|
|
157
|
+
if (!Array.isArray(allowedOrigins)) return false;
|
|
158
|
+
for (const pattern of allowedOrigins) {
|
|
159
|
+
if (matchesOriginPattern(origin, pattern)) return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveSecurityConfig(config = {}) {
|
|
165
|
+
const raw = (config && config.security && typeof config.security === 'object')
|
|
166
|
+
? config.security
|
|
167
|
+
: {};
|
|
168
|
+
const merged = {
|
|
169
|
+
...DEFAULT_SECURITY_CONFIG,
|
|
170
|
+
...raw,
|
|
171
|
+
allowedOrigins: Array.isArray(raw.allowedOrigins) && raw.allowedOrigins.length
|
|
172
|
+
? Array.from(new Set([
|
|
173
|
+
...DEFAULT_SECURITY_CONFIG.allowedOrigins,
|
|
174
|
+
...raw.allowedOrigins,
|
|
175
|
+
]))
|
|
176
|
+
: DEFAULT_SECURITY_CONFIG.allowedOrigins.slice(),
|
|
177
|
+
toolPolicies: {
|
|
178
|
+
...DEFAULT_SECURITY_CONFIG.toolPolicies,
|
|
179
|
+
...(raw.toolPolicies || {}),
|
|
180
|
+
},
|
|
181
|
+
sensitiveCookieDomains: Array.isArray(raw.sensitiveCookieDomains)
|
|
182
|
+
? raw.sensitiveCookieDomains.slice()
|
|
183
|
+
: DEFAULT_SECURITY_CONFIG.sensitiveCookieDomains.slice(),
|
|
184
|
+
enforcement: POLICY_ENFORCEMENT_LEVELS.includes(raw.enforcement)
|
|
185
|
+
? raw.enforcement
|
|
186
|
+
: DEFAULT_SECURITY_CONFIG.enforcement,
|
|
187
|
+
taskOrigin: mergeTaskOriginConfig(raw.taskOrigin),
|
|
188
|
+
egressAllowlist: Array.isArray(raw.egressAllowlist)
|
|
189
|
+
? raw.egressAllowlist.slice()
|
|
190
|
+
: DEFAULT_SECURITY_CONFIG.egressAllowlist.slice(),
|
|
191
|
+
taint: mergeTaintConfig(raw.taint),
|
|
192
|
+
profile: mergeProfileConfig(raw.profile),
|
|
193
|
+
};
|
|
194
|
+
if (process.env.JS_EYES_INSECURE === '1') {
|
|
195
|
+
merged.allowAnonymous = true;
|
|
196
|
+
}
|
|
197
|
+
if (process.env.JS_EYES_ALLOW_REMOTE_BIND === '1') {
|
|
198
|
+
merged.allowRemoteBind = true;
|
|
199
|
+
}
|
|
200
|
+
if (process.env.JS_EYES_POLICY_ENFORCEMENT && POLICY_ENFORCEMENT_LEVELS.includes(process.env.JS_EYES_POLICY_ENFORCEMENT)) {
|
|
201
|
+
merged.enforcement = process.env.JS_EYES_POLICY_ENFORCEMENT;
|
|
202
|
+
}
|
|
203
|
+
return merged;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function mergeTaskOriginConfig(raw) {
|
|
207
|
+
if (!raw || typeof raw !== 'object') {
|
|
208
|
+
return { ...DEFAULT_TASK_ORIGIN_CONFIG, sources: DEFAULT_TASK_ORIGIN_CONFIG.sources.slice() };
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
enabled: typeof raw.enabled === 'boolean' ? raw.enabled : DEFAULT_TASK_ORIGIN_CONFIG.enabled,
|
|
212
|
+
sources: Array.isArray(raw.sources) && raw.sources.length
|
|
213
|
+
? raw.sources.slice()
|
|
214
|
+
: DEFAULT_TASK_ORIGIN_CONFIG.sources.slice(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function mergeTaintConfig(raw) {
|
|
219
|
+
if (!raw || typeof raw !== 'object') {
|
|
220
|
+
return { ...DEFAULT_TAINT_CONFIG };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
enabled: typeof raw.enabled === 'boolean' ? raw.enabled : DEFAULT_TAINT_CONFIG.enabled,
|
|
224
|
+
mode: typeof raw.mode === 'string' && raw.mode ? raw.mode : DEFAULT_TAINT_CONFIG.mode,
|
|
225
|
+
minValueLength: Number.isFinite(raw.minValueLength) && raw.minValueLength > 0
|
|
226
|
+
? Math.floor(raw.minValueLength)
|
|
227
|
+
: DEFAULT_TAINT_CONFIG.minValueLength,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function mergeProfileConfig(raw) {
|
|
232
|
+
if (!raw || typeof raw !== 'object') {
|
|
233
|
+
return { ...DEFAULT_PROFILE_CONFIG };
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
default: typeof raw.default === 'string' && raw.default
|
|
237
|
+
? raw.default
|
|
238
|
+
: DEFAULT_PROFILE_CONFIG.default,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
DEFAULT_ALLOWED_ORIGINS,
|
|
244
|
+
DEFAULT_SECURITY_CONFIG,
|
|
245
|
+
DEFAULT_TASK_ORIGIN_CONFIG,
|
|
246
|
+
DEFAULT_TAINT_CONFIG,
|
|
247
|
+
DEFAULT_PROFILE_CONFIG,
|
|
248
|
+
POLICY_ENFORCEMENT_LEVELS,
|
|
249
|
+
DEFAULT_SERVER_HOST,
|
|
250
|
+
DEFAULT_SERVER_PORT,
|
|
251
|
+
DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
252
|
+
LOOPBACK_HOSTS,
|
|
253
|
+
REQUEST_TIMEOUT_MS,
|
|
254
|
+
PROTOCOL_VERSION,
|
|
255
|
+
PACKAGE_VERSION,
|
|
256
|
+
SKILLS_REGISTRY_URL,
|
|
257
|
+
RELEASE_BASE_URL,
|
|
258
|
+
FORWARDABLE_ACTIONS,
|
|
259
|
+
SENSITIVE_TOOL_NAMES,
|
|
260
|
+
COMPATIBILITY_MATRIX,
|
|
261
|
+
WS_CLOSE_CODE_AUTH_REQUIRED,
|
|
262
|
+
WS_CLOSE_CODE_FORBIDDEN_ORIGIN,
|
|
263
|
+
isLoopbackHost,
|
|
264
|
+
isOriginAllowed,
|
|
265
|
+
matchesOriginPattern,
|
|
266
|
+
resolveSecurityConfig,
|
|
267
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@js-eyes/protocol",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Shared protocol constants for JS Eyes runtime packages",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"skills.js",
|
|
9
|
+
"zip-extract.js"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "imjszhang <ortle3x3@gmail.com>",
|
|
13
|
+
"homepage": "https://js-eyes.com",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/imjszhang/JS-Eyes.git",
|
|
17
|
+
"directory": "packages/protocol"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/imjszhang/JS-Eyes/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"js-eyes",
|
|
24
|
+
"protocol",
|
|
25
|
+
"browser-automation",
|
|
26
|
+
"openclaw"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/skills.js
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const { extractZipBuffer } = require('./zip-extract');
|
|
9
|
+
|
|
10
|
+
const SKILL_CONTRACT_FILE = 'skill.contract.js';
|
|
11
|
+
const INTEGRITY_FILE = '.integrity.json';
|
|
12
|
+
const INSTALL_MANIFEST_FILE = 'skills-install.json';
|
|
13
|
+
|
|
14
|
+
function ensureDir(dir) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readJson(filePath) {
|
|
20
|
+
if (!fs.existsSync(filePath)) return null;
|
|
21
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadSkillContract(skillDir) {
|
|
25
|
+
const contractPath = path.resolve(skillDir, SKILL_CONTRACT_FILE);
|
|
26
|
+
if (!fs.existsSync(contractPath)) return null;
|
|
27
|
+
delete require.cache[require.resolve(contractPath)];
|
|
28
|
+
return require(contractPath);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasSkillContract(skillDir) {
|
|
32
|
+
return fs.existsSync(path.join(skillDir, SKILL_CONTRACT_FILE));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveSkillsDir(paths, config = {}) {
|
|
36
|
+
if (config.skillsDir) {
|
|
37
|
+
return path.resolve(config.skillsDir);
|
|
38
|
+
}
|
|
39
|
+
if (paths && paths.skillsDir) {
|
|
40
|
+
return paths.skillsDir;
|
|
41
|
+
}
|
|
42
|
+
if (paths && paths.baseDir) {
|
|
43
|
+
return path.join(paths.baseDir, 'skills');
|
|
44
|
+
}
|
|
45
|
+
return path.resolve('skills');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getOpenClawConfigPath(options = {}) {
|
|
49
|
+
const env = options.env || process.env;
|
|
50
|
+
const home = options.home || os.homedir();
|
|
51
|
+
|
|
52
|
+
if (env.OPENCLAW_CONFIG_PATH) {
|
|
53
|
+
return path.resolve(env.OPENCLAW_CONFIG_PATH);
|
|
54
|
+
}
|
|
55
|
+
if (env.OPENCLAW_STATE_DIR) {
|
|
56
|
+
return path.resolve(env.OPENCLAW_STATE_DIR, 'openclaw.json');
|
|
57
|
+
}
|
|
58
|
+
if (env.OPENCLAW_HOME) {
|
|
59
|
+
return path.resolve(env.OPENCLAW_HOME, '.openclaw', 'openclaw.json');
|
|
60
|
+
}
|
|
61
|
+
return path.join(home, '.openclaw', 'openclaw.json');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeSkillMetadata(skillDir) {
|
|
65
|
+
const contract = loadSkillContract(skillDir);
|
|
66
|
+
const pkg = readJson(path.join(skillDir, 'package.json')) || {};
|
|
67
|
+
const cli = contract && contract.cli ? contract.cli : {};
|
|
68
|
+
const openclaw = contract && contract.openclaw ? contract.openclaw : {};
|
|
69
|
+
const tools = Array.isArray(openclaw.tools)
|
|
70
|
+
? openclaw.tools.map((tool) => tool.name)
|
|
71
|
+
: [];
|
|
72
|
+
const commands = Array.isArray(cli.commands)
|
|
73
|
+
? cli.commands.map((command) => command.name)
|
|
74
|
+
: [];
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
id: contract?.id || pkg.name || path.basename(skillDir),
|
|
78
|
+
name: contract?.name || pkg.name || path.basename(skillDir),
|
|
79
|
+
version: contract?.version || pkg.version || '1.0.0',
|
|
80
|
+
description: contract?.description || pkg.description || '',
|
|
81
|
+
skillDir,
|
|
82
|
+
cliEntry: cli.entry ? path.resolve(skillDir, cli.entry) : path.join(skillDir, 'index.js'),
|
|
83
|
+
commands,
|
|
84
|
+
tools,
|
|
85
|
+
runtime: contract?.runtime || {},
|
|
86
|
+
contract,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function discoverLocalSkills(skillsDir) {
|
|
91
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
92
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
93
|
+
return entries
|
|
94
|
+
.filter((entry) => entry.isDirectory())
|
|
95
|
+
.map((entry) => path.join(skillsDir, entry.name))
|
|
96
|
+
.filter((skillDir) => hasSkillContract(skillDir))
|
|
97
|
+
.map((skillDir) => normalizeSkillMetadata(skillDir))
|
|
98
|
+
.filter((skill) => skill && skill.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readSkillById(skillsDir, skillId) {
|
|
102
|
+
const skillDir = path.join(skillsDir, skillId);
|
|
103
|
+
if (!fs.existsSync(skillDir) || !hasSkillContract(skillDir)) return null;
|
|
104
|
+
return normalizeSkillMetadata(skillDir);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fetchSkillsRegistry(registryUrl) {
|
|
108
|
+
const response = await fetch(registryUrl, {
|
|
109
|
+
headers: { Accept: 'application/json' },
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`HTTP ${response.status}`);
|
|
113
|
+
}
|
|
114
|
+
return response.json();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveOpenClawPluginEntry(definition) {
|
|
118
|
+
try {
|
|
119
|
+
const sdk = require('openclaw/plugin-sdk/plugin-entry');
|
|
120
|
+
if (typeof sdk.definePluginEntry === 'function') {
|
|
121
|
+
return sdk.definePluginEntry(definition);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Fallback for local development without the OpenClaw SDK package installed.
|
|
125
|
+
}
|
|
126
|
+
return definition.register;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getSkillsState(config = {}) {
|
|
130
|
+
const state = config && typeof config === 'object' ? config.skillsEnabled : null;
|
|
131
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
return state;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getLegacyOpenClawSkillState(options = {}) {
|
|
138
|
+
const {
|
|
139
|
+
openclawConfigPath = getOpenClawConfigPath(options),
|
|
140
|
+
skillIds = null,
|
|
141
|
+
} = options;
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(openclawConfigPath)) {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let config = null;
|
|
148
|
+
try {
|
|
149
|
+
config = JSON.parse(fs.readFileSync(openclawConfigPath, 'utf8'));
|
|
150
|
+
} catch {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const entries = config?.plugins?.entries;
|
|
155
|
+
if (!entries || typeof entries !== 'object') {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const allowedSkillIds = Array.isArray(skillIds) && skillIds.length > 0
|
|
160
|
+
? new Set(skillIds)
|
|
161
|
+
: null;
|
|
162
|
+
const state = {};
|
|
163
|
+
|
|
164
|
+
for (const [skillId, entry] of Object.entries(entries)) {
|
|
165
|
+
if (skillId === 'js-eyes') {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (allowedSkillIds && !allowedSkillIds.has(skillId)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!entry || typeof entry !== 'object' || entry.enabled === undefined) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
state[skillId] = entry.enabled !== false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return state;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isSkillEnabled(config = {}, skillId, legacyState = {}) {
|
|
181
|
+
const state = getSkillsState(config);
|
|
182
|
+
if (Object.prototype.hasOwnProperty.call(state, skillId)) {
|
|
183
|
+
return state[skillId] !== false;
|
|
184
|
+
}
|
|
185
|
+
if (legacyState && Object.prototype.hasOwnProperty.call(legacyState, skillId)) {
|
|
186
|
+
return legacyState[skillId] !== false;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function registerOpenClawTools(api, adapter, options = {}) {
|
|
192
|
+
const logger = options.logger || api.logger || console;
|
|
193
|
+
const registeredNames = options.registeredNames || null;
|
|
194
|
+
const sourceName = options.sourceName || adapter?.id || 'js-eyes-skill';
|
|
195
|
+
const declaredTools = Array.isArray(options.declaredTools) && options.declaredTools.length > 0
|
|
196
|
+
? new Set(options.declaredTools)
|
|
197
|
+
: null;
|
|
198
|
+
const wrapTool = typeof options.wrapTool === 'function' ? options.wrapTool : null;
|
|
199
|
+
const summary = {
|
|
200
|
+
registered: [],
|
|
201
|
+
skipped: [],
|
|
202
|
+
failed: [],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
for (const tool of adapter.tools || []) {
|
|
206
|
+
if (!tool || !tool.name) {
|
|
207
|
+
summary.skipped.push({ name: '(anonymous)', reason: 'missing-name' });
|
|
208
|
+
logger.warn(`[js-eyes] Skipping tool with missing name from ${sourceName}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (declaredTools && !declaredTools.has(tool.name)) {
|
|
212
|
+
summary.skipped.push({ name: tool.name, reason: 'undeclared' });
|
|
213
|
+
logger.warn(`[js-eyes] Skipping undeclared tool "${tool.name}" from ${sourceName}`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (registeredNames && registeredNames.has(tool.name)) {
|
|
217
|
+
summary.skipped.push({ name: tool.name, reason: 'duplicate-name' });
|
|
218
|
+
logger.warn(`[js-eyes] Skipping duplicate tool "${tool.name}" from ${sourceName}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const definition = {
|
|
224
|
+
name: tool.name,
|
|
225
|
+
label: tool.label,
|
|
226
|
+
description: tool.description,
|
|
227
|
+
parameters: tool.parameters,
|
|
228
|
+
execute: tool.execute,
|
|
229
|
+
};
|
|
230
|
+
const wrapped = wrapTool ? wrapTool(definition, { source: sourceName }) : definition;
|
|
231
|
+
api.registerTool(wrapped, tool.optional ? { optional: true } : undefined);
|
|
232
|
+
if (registeredNames) {
|
|
233
|
+
registeredNames.add(tool.name);
|
|
234
|
+
}
|
|
235
|
+
summary.registered.push(tool.name);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
summary.failed.push({ name: tool.name, reason: error.message });
|
|
238
|
+
logger.warn(`[js-eyes] Failed to register tool "${tool.name}" from ${sourceName}: ${error.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return summary;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function sha256(buffer) {
|
|
246
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isMainRefUrl(url) {
|
|
250
|
+
if (typeof url !== 'string') return false;
|
|
251
|
+
return /\/(refs\/heads\/)?main(?=[/?])/.test(url);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function downloadBuffer(urls, logger = console) {
|
|
255
|
+
let lastError = null;
|
|
256
|
+
for (const url of urls) {
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch(url);
|
|
259
|
+
if (response.ok) {
|
|
260
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
261
|
+
return { buffer: buf, url };
|
|
262
|
+
}
|
|
263
|
+
lastError = new Error(`HTTP ${response.status} (${url})`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
lastError = error;
|
|
266
|
+
}
|
|
267
|
+
if (logger && typeof logger.warn === 'function') {
|
|
268
|
+
logger.warn(`[js-eyes] Download failed (${url}): ${lastError?.message || 'unknown'}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw lastError || new Error('Download failed for all URLs');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function detectPackageManager(targetDir) {
|
|
275
|
+
if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
276
|
+
if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) return 'yarn';
|
|
277
|
+
if (fs.existsSync(path.join(targetDir, 'package-lock.json'))) return 'npm';
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function installSkillDependencies(targetDir, options = {}) {
|
|
282
|
+
const pkgJson = path.join(targetDir, 'package.json');
|
|
283
|
+
if (!fs.existsSync(pkgJson)) return { ran: false, manager: null };
|
|
284
|
+
|
|
285
|
+
const requireLockfile = options.requireLockfile !== false;
|
|
286
|
+
const manager = detectPackageManager(targetDir);
|
|
287
|
+
|
|
288
|
+
if (requireLockfile && manager !== 'npm') {
|
|
289
|
+
throw new Error('安装拒绝执行:缺少 package-lock.json(开启 security.requireLockfile=false 可放宽)');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const ignoreScripts = options.allowPostinstall ? [] : ['--ignore-scripts'];
|
|
293
|
+
const command = manager === 'npm' ? 'ci' : 'install';
|
|
294
|
+
const args = [command, ...ignoreScripts, '--no-audit', '--no-fund'];
|
|
295
|
+
|
|
296
|
+
const result = spawnSync('npm', args, {
|
|
297
|
+
cwd: targetDir,
|
|
298
|
+
stdio: options.stdio || 'pipe',
|
|
299
|
+
windowsHide: true,
|
|
300
|
+
env: { ...process.env, npm_config_ignore_scripts: options.allowPostinstall ? 'false' : 'true' },
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (result.status !== 0) {
|
|
304
|
+
const stderr = result.stderr ? String(result.stderr) : '';
|
|
305
|
+
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${stderr.slice(0, 500)}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { ran: true, manager: manager || 'npm', allowPostinstall: Boolean(options.allowPostinstall) };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function listFilesRecursive(dir) {
|
|
312
|
+
const out = [];
|
|
313
|
+
function walk(current) {
|
|
314
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
const full = path.join(current, entry.name);
|
|
317
|
+
const rel = path.relative(dir, full);
|
|
318
|
+
if (rel === INTEGRITY_FILE || rel === INSTALL_MANIFEST_FILE) continue;
|
|
319
|
+
if (rel.split(path.sep)[0] === 'node_modules') continue;
|
|
320
|
+
if (entry.isDirectory()) {
|
|
321
|
+
walk(full);
|
|
322
|
+
} else if (entry.isFile()) {
|
|
323
|
+
out.push(rel.split(path.sep).join('/'));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
walk(dir);
|
|
328
|
+
return out.sort();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function writeIntegrityManifest(targetDir, payload = {}) {
|
|
332
|
+
const files = {};
|
|
333
|
+
for (const rel of listFilesRecursive(targetDir)) {
|
|
334
|
+
const full = path.join(targetDir, rel);
|
|
335
|
+
files[rel] = sha256(fs.readFileSync(full));
|
|
336
|
+
}
|
|
337
|
+
const manifest = {
|
|
338
|
+
version: 1,
|
|
339
|
+
createdAt: new Date().toISOString(),
|
|
340
|
+
skillId: payload.skillId || null,
|
|
341
|
+
sourceUrl: payload.sourceUrl || null,
|
|
342
|
+
bundleSha256: payload.bundleSha256 || null,
|
|
343
|
+
declaredTools: payload.declaredTools || [],
|
|
344
|
+
files,
|
|
345
|
+
};
|
|
346
|
+
const filePath = path.join(targetDir, INTEGRITY_FILE);
|
|
347
|
+
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2) + '\n');
|
|
348
|
+
try { fs.chmodSync(filePath, 0o600); } catch {}
|
|
349
|
+
return manifest;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readSkillIntegrity(skillDir) {
|
|
353
|
+
const filePath = path.join(skillDir, INTEGRITY_FILE);
|
|
354
|
+
if (!fs.existsSync(filePath)) return null;
|
|
355
|
+
try {
|
|
356
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function verifySkillIntegrity(skillDir) {
|
|
363
|
+
const manifest = readSkillIntegrity(skillDir);
|
|
364
|
+
if (!manifest || !manifest.files) {
|
|
365
|
+
return { hasIntegrity: false, ok: false, mismatches: [], missing: [], extra: [], checked: 0 };
|
|
366
|
+
}
|
|
367
|
+
const expected = manifest.files;
|
|
368
|
+
const expectedKeys = Object.keys(expected);
|
|
369
|
+
const present = new Set(listFilesRecursive(skillDir));
|
|
370
|
+
|
|
371
|
+
const mismatches = [];
|
|
372
|
+
const missing = [];
|
|
373
|
+
|
|
374
|
+
for (const rel of expectedKeys) {
|
|
375
|
+
const full = path.join(skillDir, rel);
|
|
376
|
+
if (!fs.existsSync(full)) {
|
|
377
|
+
missing.push(rel);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const actual = sha256(fs.readFileSync(full));
|
|
381
|
+
if (actual !== expected[rel]) {
|
|
382
|
+
mismatches.push(rel);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const extra = [...present].filter((rel) => !Object.prototype.hasOwnProperty.call(expected, rel));
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
hasIntegrity: true,
|
|
390
|
+
ok: mismatches.length === 0 && missing.length === 0,
|
|
391
|
+
mismatches,
|
|
392
|
+
missing,
|
|
393
|
+
extra,
|
|
394
|
+
checked: expectedKeys.length,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function planSkillInstall(options) {
|
|
399
|
+
const {
|
|
400
|
+
skillId,
|
|
401
|
+
registryUrl,
|
|
402
|
+
skillsDir,
|
|
403
|
+
stagingDir = path.join(os.tmpdir(), `js-eyes-skill-staging-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`),
|
|
404
|
+
force = false,
|
|
405
|
+
logger = console,
|
|
406
|
+
} = options;
|
|
407
|
+
|
|
408
|
+
ensureDir(skillsDir);
|
|
409
|
+
const registry = await fetchSkillsRegistry(registryUrl);
|
|
410
|
+
const skill = registry.skills?.find((entry) => entry.id === skillId);
|
|
411
|
+
if (!skill) {
|
|
412
|
+
const ids = (registry.skills || []).map((entry) => entry.id).join(', ');
|
|
413
|
+
throw new Error(`技能 "${skillId}" 未在注册表中找到。可用技能: ${ids || '无'}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const targetDir = path.join(skillsDir, skillId);
|
|
417
|
+
if (fs.existsSync(targetDir) && !force) {
|
|
418
|
+
throw new Error(`技能 "${skillId}" 已安装在 ${targetDir}(使用 force=true 覆盖)`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!skill.sha256 || typeof skill.sha256 !== 'string') {
|
|
422
|
+
throw new Error(`技能 "${skillId}" 注册表条目缺少 sha256 校验和,拒绝下载`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const candidateUrls = [skill.downloadUrl];
|
|
426
|
+
if (skill.downloadUrlFallback) {
|
|
427
|
+
if (isMainRefUrl(skill.downloadUrlFallback)) {
|
|
428
|
+
logger?.warn?.(`[js-eyes] Ignoring @main fallback URL for ${skillId} (use a tagged URL)`);
|
|
429
|
+
} else {
|
|
430
|
+
candidateUrls.push(skill.downloadUrlFallback);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const { buffer, url } = await downloadBuffer(candidateUrls.filter(Boolean), logger);
|
|
435
|
+
const digest = sha256(buffer);
|
|
436
|
+
if (digest !== skill.sha256.toLowerCase()) {
|
|
437
|
+
throw new Error(`技能 "${skillId}" 哈希不符: expected ${skill.sha256}, got ${digest}`);
|
|
438
|
+
}
|
|
439
|
+
if (typeof skill.size === 'number' && buffer.length !== skill.size) {
|
|
440
|
+
throw new Error(`技能 "${skillId}" 大小不符: expected ${skill.size}, got ${buffer.length}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
ensureDir(stagingDir);
|
|
444
|
+
if (fs.existsSync(stagingDir) && fs.readdirSync(stagingDir).length > 0) {
|
|
445
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
446
|
+
ensureDir(stagingDir);
|
|
447
|
+
}
|
|
448
|
+
extractZipBuffer(buffer, stagingDir);
|
|
449
|
+
|
|
450
|
+
const stagedFiles = listFilesRecursive(stagingDir);
|
|
451
|
+
const declaredTools = Array.isArray(skill.tools) ? skill.tools.slice() : [];
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
plan: {
|
|
455
|
+
skillId,
|
|
456
|
+
registryUrl,
|
|
457
|
+
sourceUrl: url,
|
|
458
|
+
bundleSha256: digest,
|
|
459
|
+
bundleSize: buffer.length,
|
|
460
|
+
targetDir,
|
|
461
|
+
stagingDir,
|
|
462
|
+
declaredTools,
|
|
463
|
+
stagedFiles,
|
|
464
|
+
registryEntry: skill,
|
|
465
|
+
hasLockfile: fs.existsSync(path.join(stagingDir, 'package-lock.json')),
|
|
466
|
+
hasPackageJson: fs.existsSync(path.join(stagingDir, 'package.json')),
|
|
467
|
+
},
|
|
468
|
+
skill,
|
|
469
|
+
registry,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function cleanupStaging(stagingDir) {
|
|
474
|
+
if (stagingDir && fs.existsSync(stagingDir)) {
|
|
475
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function applySkillInstall(plan, options = {}) {
|
|
480
|
+
if (!plan || !plan.stagingDir || !plan.targetDir) {
|
|
481
|
+
throw new Error('applySkillInstall: invalid plan');
|
|
482
|
+
}
|
|
483
|
+
const requireLockfile = options.requireLockfile !== false;
|
|
484
|
+
if (plan.hasPackageJson && requireLockfile && !plan.hasLockfile) {
|
|
485
|
+
cleanupStaging(plan.stagingDir);
|
|
486
|
+
throw new Error('技能包缺少 package-lock.json,拒绝安装(设置 security.requireLockfile=false 可放宽)');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
installSkillDependencies(plan.stagingDir, {
|
|
490
|
+
requireLockfile,
|
|
491
|
+
allowPostinstall: Boolean(options.allowPostinstall),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
if (fs.existsSync(plan.targetDir)) {
|
|
495
|
+
fs.rmSync(plan.targetDir, { recursive: true, force: true });
|
|
496
|
+
}
|
|
497
|
+
ensureDir(path.dirname(plan.targetDir));
|
|
498
|
+
fs.renameSync(plan.stagingDir, plan.targetDir);
|
|
499
|
+
|
|
500
|
+
const integrity = writeIntegrityManifest(plan.targetDir, {
|
|
501
|
+
skillId: plan.skillId,
|
|
502
|
+
sourceUrl: plan.sourceUrl,
|
|
503
|
+
bundleSha256: plan.bundleSha256,
|
|
504
|
+
declaredTools: plan.declaredTools,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const installManifest = {
|
|
508
|
+
skillId: plan.skillId,
|
|
509
|
+
sourceUrl: plan.sourceUrl,
|
|
510
|
+
bundleSha256: plan.bundleSha256,
|
|
511
|
+
bundleSize: plan.bundleSize,
|
|
512
|
+
installedAt: integrity.createdAt,
|
|
513
|
+
declaredTools: plan.declaredTools,
|
|
514
|
+
};
|
|
515
|
+
const installManifestPath = path.join(plan.targetDir, INSTALL_MANIFEST_FILE);
|
|
516
|
+
fs.writeFileSync(installManifestPath, JSON.stringify(installManifest, null, 2) + '\n');
|
|
517
|
+
try { fs.chmodSync(installManifestPath, 0o600); } catch {}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
targetDir: plan.targetDir,
|
|
521
|
+
integrity,
|
|
522
|
+
installManifest,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function installSkillFromRegistry(options) {
|
|
527
|
+
const { plan, skill, registry } = await planSkillInstall(options);
|
|
528
|
+
const apply = applySkillInstall(plan, {
|
|
529
|
+
requireLockfile: options.requireLockfile,
|
|
530
|
+
allowPostinstall: options.allowPostinstall,
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
registry,
|
|
534
|
+
skill,
|
|
535
|
+
targetDir: apply.targetDir,
|
|
536
|
+
integrity: apply.integrity,
|
|
537
|
+
installManifest: apply.installManifest,
|
|
538
|
+
plan,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function runSkillCli(options) {
|
|
543
|
+
const { skillDir, argv = [], stdio = 'inherit', env = process.env } = options;
|
|
544
|
+
const skill = normalizeSkillMetadata(skillDir);
|
|
545
|
+
if (!fs.existsSync(skill.cliEntry)) {
|
|
546
|
+
throw new Error(`技能 ${skill.id} 缺少 CLI 入口: ${skill.cliEntry}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return spawnSync(process.execPath, [skill.cliEntry, ...argv], {
|
|
550
|
+
cwd: skillDir,
|
|
551
|
+
env: { ...env, JS_EYES_SKILL_DIR: skillDir },
|
|
552
|
+
stdio,
|
|
553
|
+
encoding: stdio === 'pipe' ? 'utf8' : undefined,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
module.exports = {
|
|
558
|
+
INSTALL_MANIFEST_FILE,
|
|
559
|
+
INTEGRITY_FILE,
|
|
560
|
+
SKILL_CONTRACT_FILE,
|
|
561
|
+
applySkillInstall,
|
|
562
|
+
cleanupStaging,
|
|
563
|
+
discoverLocalSkills,
|
|
564
|
+
fetchSkillsRegistry,
|
|
565
|
+
getLegacyOpenClawSkillState,
|
|
566
|
+
getOpenClawConfigPath,
|
|
567
|
+
getSkillsState,
|
|
568
|
+
installSkillFromRegistry,
|
|
569
|
+
isSkillEnabled,
|
|
570
|
+
loadSkillContract,
|
|
571
|
+
normalizeSkillMetadata,
|
|
572
|
+
planSkillInstall,
|
|
573
|
+
readSkillById,
|
|
574
|
+
readSkillIntegrity,
|
|
575
|
+
registerOpenClawTools,
|
|
576
|
+
resolveSkillsDir,
|
|
577
|
+
resolveOpenClawPluginEntry,
|
|
578
|
+
runSkillCli,
|
|
579
|
+
verifySkillIntegrity,
|
|
580
|
+
writeIntegrityManifest,
|
|
581
|
+
};
|
package/zip-extract.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const zlib = require('zlib');
|
|
6
|
+
|
|
7
|
+
const EOCD_SIGNATURE = 0x06054b50;
|
|
8
|
+
const ZIP64_EOCD_LOCATOR = 0x07064b50;
|
|
9
|
+
const ZIP64_EOCD = 0x06064b50;
|
|
10
|
+
const CENTRAL_DIR_HEADER = 0x02014b50;
|
|
11
|
+
const LOCAL_HEADER = 0x04034b50;
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MAX_TOTAL_SIZE = 100 * 1024 * 1024;
|
|
14
|
+
const DEFAULT_MAX_FILE_SIZE = 20 * 1024 * 1024;
|
|
15
|
+
const DEFAULT_MAX_ENTRIES = 5000;
|
|
16
|
+
|
|
17
|
+
function readUInt16(buf, off) { return buf.readUInt16LE(off); }
|
|
18
|
+
function readUInt32(buf, off) { return buf.readUInt32LE(off); }
|
|
19
|
+
|
|
20
|
+
function findEndOfCentralDirectory(buf) {
|
|
21
|
+
const minLen = 22;
|
|
22
|
+
if (buf.length < minLen) {
|
|
23
|
+
throw new Error('zip: file too small');
|
|
24
|
+
}
|
|
25
|
+
const maxBack = Math.min(buf.length, 65536 + minLen);
|
|
26
|
+
for (let i = buf.length - minLen; i >= buf.length - maxBack; i--) {
|
|
27
|
+
if (i < 0) break;
|
|
28
|
+
if (readUInt32(buf, i) === EOCD_SIGNATURE) {
|
|
29
|
+
return i;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw new Error('zip: end-of-central-directory not found');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseEntries(buf) {
|
|
36
|
+
const eocdOffset = findEndOfCentralDirectory(buf);
|
|
37
|
+
let cdSize = readUInt32(buf, eocdOffset + 12);
|
|
38
|
+
let cdOffset = readUInt32(buf, eocdOffset + 16);
|
|
39
|
+
let entryCount = readUInt16(buf, eocdOffset + 10);
|
|
40
|
+
|
|
41
|
+
if (cdOffset === 0xffffffff || cdSize === 0xffffffff || entryCount === 0xffff) {
|
|
42
|
+
const locatorOffset = eocdOffset - 20;
|
|
43
|
+
if (locatorOffset >= 0 && readUInt32(buf, locatorOffset) === ZIP64_EOCD_LOCATOR) {
|
|
44
|
+
const zip64Offset = Number(buf.readBigUInt64LE(locatorOffset + 8));
|
|
45
|
+
if (zip64Offset >= 0 && readUInt32(buf, zip64Offset) === ZIP64_EOCD) {
|
|
46
|
+
entryCount = Number(buf.readBigUInt64LE(zip64Offset + 32));
|
|
47
|
+
cdSize = Number(buf.readBigUInt64LE(zip64Offset + 40));
|
|
48
|
+
cdOffset = Number(buf.readBigUInt64LE(zip64Offset + 48));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entries = [];
|
|
54
|
+
let offset = cdOffset;
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < entryCount; i++) {
|
|
57
|
+
if (readUInt32(buf, offset) !== CENTRAL_DIR_HEADER) {
|
|
58
|
+
throw new Error(`zip: bad central directory at entry ${i}`);
|
|
59
|
+
}
|
|
60
|
+
const versionMadeBy = readUInt16(buf, offset + 4);
|
|
61
|
+
const generalFlag = readUInt16(buf, offset + 8);
|
|
62
|
+
const method = readUInt16(buf, offset + 10);
|
|
63
|
+
let compressedSize = readUInt32(buf, offset + 20);
|
|
64
|
+
let uncompressedSize = readUInt32(buf, offset + 24);
|
|
65
|
+
const fileNameLength = readUInt16(buf, offset + 28);
|
|
66
|
+
const extraLength = readUInt16(buf, offset + 30);
|
|
67
|
+
const commentLength = readUInt16(buf, offset + 32);
|
|
68
|
+
const externalAttrs = readUInt32(buf, offset + 38);
|
|
69
|
+
let localHeaderOffset = readUInt32(buf, offset + 42);
|
|
70
|
+
const fileName = buf.slice(offset + 46, offset + 46 + fileNameLength).toString('utf8');
|
|
71
|
+
|
|
72
|
+
let extraOffset = offset + 46 + fileNameLength;
|
|
73
|
+
const extraEnd = extraOffset + extraLength;
|
|
74
|
+
while (extraOffset + 4 <= extraEnd) {
|
|
75
|
+
const id = readUInt16(buf, extraOffset);
|
|
76
|
+
const size = readUInt16(buf, extraOffset + 2);
|
|
77
|
+
if (id === 0x0001) {
|
|
78
|
+
let p = extraOffset + 4;
|
|
79
|
+
if (uncompressedSize === 0xffffffff && p + 8 <= extraEnd) {
|
|
80
|
+
uncompressedSize = Number(buf.readBigUInt64LE(p)); p += 8;
|
|
81
|
+
}
|
|
82
|
+
if (compressedSize === 0xffffffff && p + 8 <= extraEnd) {
|
|
83
|
+
compressedSize = Number(buf.readBigUInt64LE(p)); p += 8;
|
|
84
|
+
}
|
|
85
|
+
if (localHeaderOffset === 0xffffffff && p + 8 <= extraEnd) {
|
|
86
|
+
localHeaderOffset = Number(buf.readBigUInt64LE(p)); p += 8;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
extraOffset += 4 + size;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isUnix = (versionMadeBy >>> 8) === 3;
|
|
93
|
+
const unixMode = isUnix ? (externalAttrs >>> 16) & 0xffff : 0;
|
|
94
|
+
const isSymlink = isUnix && (unixMode & 0xf000) === 0xa000;
|
|
95
|
+
|
|
96
|
+
entries.push({
|
|
97
|
+
fileName,
|
|
98
|
+
method,
|
|
99
|
+
compressedSize,
|
|
100
|
+
uncompressedSize,
|
|
101
|
+
localHeaderOffset,
|
|
102
|
+
generalFlag,
|
|
103
|
+
isSymlink,
|
|
104
|
+
isDirectory: fileName.endsWith('/') || (isUnix && (unixMode & 0xf000) === 0x4000),
|
|
105
|
+
unixMode,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
offset += 46 + fileNameLength + extraLength + commentLength;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return entries;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readEntryData(buf, entry) {
|
|
115
|
+
const lhOffset = entry.localHeaderOffset;
|
|
116
|
+
if (readUInt32(buf, lhOffset) !== LOCAL_HEADER) {
|
|
117
|
+
throw new Error(`zip: bad local header for ${entry.fileName}`);
|
|
118
|
+
}
|
|
119
|
+
const fileNameLen = readUInt16(buf, lhOffset + 26);
|
|
120
|
+
const extraLen = readUInt16(buf, lhOffset + 28);
|
|
121
|
+
const dataStart = lhOffset + 30 + fileNameLen + extraLen;
|
|
122
|
+
const compressed = buf.slice(dataStart, dataStart + entry.compressedSize);
|
|
123
|
+
|
|
124
|
+
if (entry.method === 0) {
|
|
125
|
+
return compressed;
|
|
126
|
+
}
|
|
127
|
+
if (entry.method === 8) {
|
|
128
|
+
return zlib.inflateRawSync(compressed);
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`zip: unsupported compression method ${entry.method} for ${entry.fileName}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isPathInside(parent, child) {
|
|
134
|
+
const rel = path.relative(parent, child);
|
|
135
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function safeJoin(targetDir, fileName) {
|
|
139
|
+
const normalized = fileName.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
140
|
+
if (normalized.split('/').some((segment) => segment === '..')) {
|
|
141
|
+
throw new Error(`zip: refusing path traversal entry ${fileName}`);
|
|
142
|
+
}
|
|
143
|
+
const dest = path.resolve(targetDir, normalized);
|
|
144
|
+
if (!isPathInside(path.resolve(targetDir), dest)) {
|
|
145
|
+
throw new Error(`zip: entry escapes target dir: ${fileName}`);
|
|
146
|
+
}
|
|
147
|
+
return dest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractZipBuffer(buffer, targetDir, options = {}) {
|
|
151
|
+
const maxTotal = options.maxTotalSize || DEFAULT_MAX_TOTAL_SIZE;
|
|
152
|
+
const maxFile = options.maxFileSize || DEFAULT_MAX_FILE_SIZE;
|
|
153
|
+
const maxEntries = options.maxEntries || DEFAULT_MAX_ENTRIES;
|
|
154
|
+
|
|
155
|
+
const entries = parseEntries(buffer);
|
|
156
|
+
if (entries.length > maxEntries) {
|
|
157
|
+
throw new Error(`zip: too many entries (${entries.length} > ${maxEntries})`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let totalSize = 0;
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (entry.uncompressedSize > maxFile) {
|
|
163
|
+
throw new Error(`zip: entry ${entry.fileName} exceeds max file size (${entry.uncompressedSize})`);
|
|
164
|
+
}
|
|
165
|
+
totalSize += entry.uncompressedSize;
|
|
166
|
+
if (totalSize > maxTotal) {
|
|
167
|
+
throw new Error(`zip: total uncompressed size exceeds limit (${totalSize} > ${maxTotal})`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const written = [];
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (entry.isSymlink) {
|
|
176
|
+
throw new Error(`zip: symlinks are not allowed (${entry.fileName})`);
|
|
177
|
+
}
|
|
178
|
+
if (entry.isDirectory) {
|
|
179
|
+
const dest = safeJoin(targetDir, entry.fileName);
|
|
180
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const dest = safeJoin(targetDir, entry.fileName);
|
|
184
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
185
|
+
const data = readEntryData(buffer, entry);
|
|
186
|
+
if (data.length > maxFile) {
|
|
187
|
+
throw new Error(`zip: decompressed entry exceeds limit (${entry.fileName})`);
|
|
188
|
+
}
|
|
189
|
+
fs.writeFileSync(dest, data);
|
|
190
|
+
written.push(path.relative(targetDir, dest));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { entries: entries.length, written };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function extractZipFile(zipPath, targetDir, options = {}) {
|
|
197
|
+
const buffer = fs.readFileSync(zipPath);
|
|
198
|
+
return extractZipBuffer(buffer, targetDir, options);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
DEFAULT_MAX_FILE_SIZE,
|
|
203
|
+
DEFAULT_MAX_TOTAL_SIZE,
|
|
204
|
+
DEFAULT_MAX_ENTRIES,
|
|
205
|
+
extractZipBuffer,
|
|
206
|
+
extractZipFile,
|
|
207
|
+
parseEntries,
|
|
208
|
+
};
|