@js-eyes/protocol 2.6.0 → 2.6.2
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/extra-integrity.js +199 -0
- package/fs-io.js +37 -0
- package/index.js +4 -0
- package/openclaw-paths.js +33 -0
- package/package.json +9 -2
- package/registry-client.js +50 -0
- package/safe-npm.js +150 -0
- package/skill-registry.js +675 -0
- package/skill-runner.js +48 -0
- package/skills.js +15 -111
package/skill-runner.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// skill-runner: launches a sub-skill's own Node CLI entry.
|
|
4
|
+
//
|
|
5
|
+
// Kept out of skills.js so the only `child_process` call in @js-eyes/protocol
|
|
6
|
+
// that lives on skills.js's transitive imports is safe-npm.js (which has its
|
|
7
|
+
// own hardening). This module MUST NOT import `ws`, `http`, `https`, `net`,
|
|
8
|
+
// or any network helper — the invariant is enforced by
|
|
9
|
+
// test/import-boundaries.test.js.
|
|
10
|
+
//
|
|
11
|
+
// Contract:
|
|
12
|
+
// * `process.execPath` is the argv[0] — we never invoke a shell;
|
|
13
|
+
// * argv entries are forwarded verbatim from the caller; spawnSync is
|
|
14
|
+
// always called with `shell: false` and `windowsHide: true`;
|
|
15
|
+
// * the caller's env is inherited (extended with `JS_EYES_SKILL_DIR`).
|
|
16
|
+
// Unlike safe-npm we do not filter env here because the skill CLI
|
|
17
|
+
// legitimately needs the full environment — sub-skills are on-disk code
|
|
18
|
+
// the operator has already linked/approved via the integrity workflow.
|
|
19
|
+
//
|
|
20
|
+
// See SECURITY_SCAN_NOTES.md ("Shell command execution").
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { spawnSync } = require('child_process');
|
|
25
|
+
|
|
26
|
+
function runSkillCli(options) {
|
|
27
|
+
const { skillDir, argv = [], stdio = 'inherit', env = process.env } = options;
|
|
28
|
+
if (!skillDir || typeof skillDir !== 'string') {
|
|
29
|
+
throw new TypeError('runSkillCli: skillDir is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { normalizeSkillMetadata } = require('./skills');
|
|
33
|
+
const skill = normalizeSkillMetadata(skillDir);
|
|
34
|
+
if (!fs.existsSync(skill.cliEntry)) {
|
|
35
|
+
throw new Error(`技能 ${skill.id} 缺少 CLI 入口: ${skill.cliEntry}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return spawnSync(process.execPath, [skill.cliEntry, ...argv], {
|
|
39
|
+
cwd: skillDir,
|
|
40
|
+
env: { ...env, JS_EYES_SKILL_DIR: skillDir },
|
|
41
|
+
stdio,
|
|
42
|
+
shell: false,
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
encoding: stdio === 'pipe' ? 'utf8' : undefined,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { runSkillCli };
|
package/skills.js
CHANGED
|
@@ -4,23 +4,15 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { spawnSync } = require('child_process');
|
|
8
7
|
const { extractZipBuffer } = require('./zip-extract');
|
|
8
|
+
const { ensureDir, readJson, safeStat } = require('./fs-io');
|
|
9
|
+
const { getOpenClawConfigPath } = require('./openclaw-paths');
|
|
10
|
+
const safeNpm = require('./safe-npm');
|
|
9
11
|
|
|
10
12
|
const SKILL_CONTRACT_FILE = 'skill.contract.js';
|
|
11
13
|
const INTEGRITY_FILE = '.integrity.json';
|
|
12
14
|
const INSTALL_MANIFEST_FILE = 'skills-install.json';
|
|
13
15
|
|
|
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
16
|
function loadSkillContract(skillDir) {
|
|
25
17
|
const contractPath = path.resolve(skillDir, SKILL_CONTRACT_FILE);
|
|
26
18
|
if (!fs.existsSync(contractPath)) return null;
|
|
@@ -32,14 +24,6 @@ function hasSkillContract(skillDir) {
|
|
|
32
24
|
return fs.existsSync(path.join(skillDir, SKILL_CONTRACT_FILE));
|
|
33
25
|
}
|
|
34
26
|
|
|
35
|
-
function safeStat(target) {
|
|
36
|
-
try {
|
|
37
|
-
return fs.statSync(target);
|
|
38
|
-
} catch (_) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
27
|
/**
|
|
44
28
|
* 列出某个目录下被视为「候选 skill 目录」的直接子项绝对路径。
|
|
45
29
|
*
|
|
@@ -146,22 +130,6 @@ function resolveSkillSources(input = {}) {
|
|
|
146
130
|
return { primary, extras, invalid };
|
|
147
131
|
}
|
|
148
132
|
|
|
149
|
-
function getOpenClawConfigPath(options = {}) {
|
|
150
|
-
const env = options.env || process.env;
|
|
151
|
-
const home = options.home || os.homedir();
|
|
152
|
-
|
|
153
|
-
if (env.OPENCLAW_CONFIG_PATH) {
|
|
154
|
-
return path.resolve(env.OPENCLAW_CONFIG_PATH);
|
|
155
|
-
}
|
|
156
|
-
if (env.OPENCLAW_STATE_DIR) {
|
|
157
|
-
return path.resolve(env.OPENCLAW_STATE_DIR, 'openclaw.json');
|
|
158
|
-
}
|
|
159
|
-
if (env.OPENCLAW_HOME) {
|
|
160
|
-
return path.resolve(env.OPENCLAW_HOME, '.openclaw', 'openclaw.json');
|
|
161
|
-
}
|
|
162
|
-
return path.join(home, '.openclaw', 'openclaw.json');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
133
|
function normalizeSkillMetadata(skillDir) {
|
|
166
134
|
const contract = loadSkillContract(skillDir);
|
|
167
135
|
const pkg = readJson(path.join(skillDir, 'package.json')) || {};
|
|
@@ -299,15 +267,11 @@ function readSkillByIdFromSources(input = {}) {
|
|
|
299
267
|
return null;
|
|
300
268
|
}
|
|
301
269
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
throw new Error(`HTTP ${response.status}`);
|
|
308
|
-
}
|
|
309
|
-
return response.json();
|
|
310
|
-
}
|
|
270
|
+
// Registry network I/O lives in registry-client.js so `fetch(…)` is not
|
|
271
|
+
// co-located with `fs.readFileSync(…)` / `fs.createReadStream(…)` in this
|
|
272
|
+
// module. Re-exported below for backwards compatibility. See
|
|
273
|
+
// SECURITY_SCAN_NOTES.md.
|
|
274
|
+
const { fetchSkillsRegistry, downloadBuffer } = require('./registry-client');
|
|
311
275
|
|
|
312
276
|
function resolveOpenClawPluginEntry(definition) {
|
|
313
277
|
try {
|
|
@@ -491,61 +455,10 @@ function isMainRefUrl(url) {
|
|
|
491
455
|
return /\/(refs\/heads\/)?main(?=[/?])/.test(url);
|
|
492
456
|
}
|
|
493
457
|
|
|
494
|
-
|
|
495
|
-
let lastError = null;
|
|
496
|
-
for (const url of urls) {
|
|
497
|
-
try {
|
|
498
|
-
const response = await fetch(url);
|
|
499
|
-
if (response.ok) {
|
|
500
|
-
const buf = Buffer.from(await response.arrayBuffer());
|
|
501
|
-
return { buffer: buf, url };
|
|
502
|
-
}
|
|
503
|
-
lastError = new Error(`HTTP ${response.status} (${url})`);
|
|
504
|
-
} catch (error) {
|
|
505
|
-
lastError = error;
|
|
506
|
-
}
|
|
507
|
-
if (logger && typeof logger.warn === 'function') {
|
|
508
|
-
logger.warn(`[js-eyes] Download failed (${url}): ${lastError?.message || 'unknown'}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
throw lastError || new Error('Download failed for all URLs');
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function detectPackageManager(targetDir) {
|
|
515
|
-
if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
516
|
-
if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) return 'yarn';
|
|
517
|
-
if (fs.existsSync(path.join(targetDir, 'package-lock.json'))) return 'npm';
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
458
|
+
const detectPackageManager = safeNpm.detectPackageManager;
|
|
520
459
|
|
|
521
460
|
function installSkillDependencies(targetDir, options = {}) {
|
|
522
|
-
|
|
523
|
-
if (!fs.existsSync(pkgJson)) return { ran: false, manager: null };
|
|
524
|
-
|
|
525
|
-
const requireLockfile = options.requireLockfile !== false;
|
|
526
|
-
const manager = detectPackageManager(targetDir);
|
|
527
|
-
|
|
528
|
-
if (requireLockfile && manager !== 'npm') {
|
|
529
|
-
throw new Error('安装拒绝执行:缺少 package-lock.json(开启 security.requireLockfile=false 可放宽)');
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const ignoreScripts = options.allowPostinstall ? [] : ['--ignore-scripts'];
|
|
533
|
-
const command = manager === 'npm' ? 'ci' : 'install';
|
|
534
|
-
const args = [command, ...ignoreScripts, '--no-audit', '--no-fund'];
|
|
535
|
-
|
|
536
|
-
const result = spawnSync('npm', args, {
|
|
537
|
-
cwd: targetDir,
|
|
538
|
-
stdio: options.stdio || 'pipe',
|
|
539
|
-
windowsHide: true,
|
|
540
|
-
env: { ...process.env, npm_config_ignore_scripts: options.allowPostinstall ? 'false' : 'true' },
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
if (result.status !== 0) {
|
|
544
|
-
const stderr = result.stderr ? String(result.stderr) : '';
|
|
545
|
-
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${stderr.slice(0, 500)}`);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return { ran: true, manager: manager || 'npm', allowPostinstall: Boolean(options.allowPostinstall) };
|
|
461
|
+
return safeNpm.installSkillDependencies(targetDir, options);
|
|
549
462
|
}
|
|
550
463
|
|
|
551
464
|
function listFilesRecursive(dir) {
|
|
@@ -779,20 +692,11 @@ async function installSkillFromRegistry(options) {
|
|
|
779
692
|
};
|
|
780
693
|
}
|
|
781
694
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
return spawnSync(process.execPath, [skill.cliEntry, ...argv], {
|
|
790
|
-
cwd: skillDir,
|
|
791
|
-
env: { ...env, JS_EYES_SKILL_DIR: skillDir },
|
|
792
|
-
stdio,
|
|
793
|
-
encoding: stdio === 'pipe' ? 'utf8' : undefined,
|
|
794
|
-
});
|
|
795
|
-
}
|
|
695
|
+
// `runSkillCli` is the only remaining child_process caller in @js-eyes/protocol
|
|
696
|
+
// outside the hardened `safe-npm.js` module. It lives in its own file so the
|
|
697
|
+
// child_process import is not co-located with `fetch(registryUrl)` / other
|
|
698
|
+
// network code in this module. See SECURITY_SCAN_NOTES.md.
|
|
699
|
+
const { runSkillCli } = require('./skill-runner');
|
|
796
700
|
|
|
797
701
|
// Assign to (rather than replace) module.exports so that modules which have
|
|
798
702
|
// already captured a reference during circular require — notably
|