@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.
@@ -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
- async function fetchSkillsRegistry(registryUrl) {
303
- const response = await fetch(registryUrl, {
304
- headers: { Accept: 'application/json' },
305
- });
306
- if (!response.ok) {
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
- async function downloadBuffer(urls, logger = console) {
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
- const pkgJson = path.join(targetDir, 'package.json');
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
- function runSkillCli(options) {
783
- const { skillDir, argv = [], stdio = 'inherit', env = process.env } = options;
784
- const skill = normalizeSkillMetadata(skillDir);
785
- if (!fs.existsSync(skill.cliEntry)) {
786
- throw new Error(`技能 ${skill.id} 缺少 CLI 入口: ${skill.cliEntry}`);
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