@js-eyes/protocol 2.6.2 → 2.8.1
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 +12 -14
- package/package.json +1 -1
- package/safe-npm.js +14 -6
- package/skill-registry.js +78 -8
- package/skills.js +20 -1
package/index.js
CHANGED
|
@@ -24,13 +24,12 @@ const FORWARDABLE_ACTIONS = [
|
|
|
24
24
|
];
|
|
25
25
|
|
|
26
26
|
const SENSITIVE_TOOL_NAMES = Object.freeze([
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'js_eyes_install_skill',
|
|
27
|
+
'browser/execute-script',
|
|
28
|
+
'browser/get-cookies',
|
|
29
|
+
'browser/get-cookies-by-domain',
|
|
30
|
+
'browser/inject-css',
|
|
31
|
+
'browser/upload-file',
|
|
32
|
+
'skills/plan-install',
|
|
34
33
|
]);
|
|
35
34
|
|
|
36
35
|
const LOOPBACK_HOSTS = Object.freeze(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1', '0:0:0:0:0:0:0:1']);
|
|
@@ -79,13 +78,12 @@ const DEFAULT_SECURITY_CONFIG = Object.freeze({
|
|
|
79
78
|
taint: { ...DEFAULT_TAINT_CONFIG },
|
|
80
79
|
profile: { ...DEFAULT_PROFILE_CONFIG },
|
|
81
80
|
toolPolicies: {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
js_eyes_install_skill: 'confirm',
|
|
81
|
+
'browser/execute-script': 'confirm',
|
|
82
|
+
'browser/get-cookies': 'confirm',
|
|
83
|
+
'browser/get-cookies-by-domain': 'confirm',
|
|
84
|
+
'browser/inject-css': 'confirm',
|
|
85
|
+
'browser/upload-file': 'confirm',
|
|
86
|
+
'skills/plan-install': 'confirm',
|
|
89
87
|
},
|
|
90
88
|
sensitiveCookieDomains: [
|
|
91
89
|
'bank',
|
package/package.json
CHANGED
package/safe-npm.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
// process.env (tokens, OAuth state, etc.) never leak into the npm run;
|
|
12
12
|
// * postinstall scripts are disabled unless the caller explicitly opts in.
|
|
13
13
|
//
|
|
14
|
-
// The
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// The only allowed binary is npm (or the fixed cmd.exe/npm.cmd wrapper Windows
|
|
15
|
+
// requires for command shims). No wildcards or user-controlled argv: every npm
|
|
16
|
+
// argument still comes from immutable constants below.
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
@@ -26,6 +26,7 @@ const ALLOWED_SUBCOMMANDS = Object.freeze({
|
|
|
26
26
|
|
|
27
27
|
const SAFE_ENV_KEYS = Object.freeze([
|
|
28
28
|
'PATH',
|
|
29
|
+
'Path',
|
|
29
30
|
'HOME',
|
|
30
31
|
'USERPROFILE',
|
|
31
32
|
'APPDATA',
|
|
@@ -82,7 +83,12 @@ function runNpm(subcommand, targetDir, options = {}) {
|
|
|
82
83
|
npm_config_ignore_scripts: allowPostinstall ? 'false' : 'true',
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
const
|
|
86
|
+
const bin = process.platform === 'win32' ? 'cmd.exe' : 'npm';
|
|
87
|
+
const args = process.platform === 'win32'
|
|
88
|
+
? ['/d', '/s', '/c', 'npm.cmd', ...baseArgs]
|
|
89
|
+
: baseArgs;
|
|
90
|
+
|
|
91
|
+
const result = spawnSync(bin, args, {
|
|
86
92
|
cwd: targetDir,
|
|
87
93
|
stdio: options.stdio || 'pipe',
|
|
88
94
|
shell: false,
|
|
@@ -97,7 +103,8 @@ function safeNpmCi(targetDir, options = {}) {
|
|
|
97
103
|
const { result, args } = runNpm('ci', targetDir, options);
|
|
98
104
|
if (result.status !== 0) {
|
|
99
105
|
const stderr = result.stderr ? String(result.stderr) : '';
|
|
100
|
-
|
|
106
|
+
const reason = result.error ? result.error.message : stderr;
|
|
107
|
+
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
|
|
101
108
|
}
|
|
102
109
|
return { ran: true, manager: 'npm', args };
|
|
103
110
|
}
|
|
@@ -106,7 +113,8 @@ function safeNpmInstall(targetDir, options = {}) {
|
|
|
106
113
|
const { result, args } = runNpm('install', targetDir, options);
|
|
107
114
|
if (result.status !== 0) {
|
|
108
115
|
const stderr = result.stderr ? String(result.stderr) : '';
|
|
109
|
-
|
|
116
|
+
const reason = result.error ? result.error.message : stderr;
|
|
117
|
+
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
|
|
110
118
|
}
|
|
111
119
|
return { ran: true, manager: 'npm', args };
|
|
112
120
|
}
|
package/skill-registry.js
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* SkillRegistry — js-eyes 技能运行时注册表
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 通过两种绑定模式实现零重启热加载:
|
|
7
|
+
* - routerMode=true(OpenClaw 单工具模式):只维护 actionBindings,由 `js-eyes`
|
|
8
|
+
* 总线工具按 `skill/<skillId>/<action>` 委派执行,不向宿主注册子技能工具;
|
|
9
|
+
* - routerMode=false(兼容/测试模式):保留工具级 dispatcher 间接层。
|
|
10
|
+
*
|
|
11
|
+
* 兼容 dispatcher 模式:
|
|
7
12
|
* - 插件启动时 api.registerTool(name, dispatcher) 仅对每个 tool name 注册一次稳定闭包;
|
|
8
13
|
* - 每个 dispatcher 在调用时从 toolBindings.get(name) 查当前实现并委派;
|
|
9
14
|
* - 热加载只改 toolBindings 映射,不触碰宿主注册表。
|
|
@@ -15,6 +20,8 @@
|
|
|
15
20
|
* 让 OpenClaw / LLM 看到正确的入参约束(required / properties 等)。热加载时若 contract
|
|
16
21
|
* 改了 schema,会按**引用** mutate 已注册的 dispatcher 对象;若某些 OpenClaw 宿主对 tool
|
|
17
22
|
* 对象做了深拷贝/快照,则 mutate 不会生效,但首次注册的 schema 仍然是正确的。
|
|
23
|
+
* routerMode 下 OpenClaw 只看到 `js-eyes` 的总线 schema,子技能 schema 作为内部 definition
|
|
24
|
+
* 保留给后续 introspection / 文档输出使用。
|
|
18
25
|
*/
|
|
19
26
|
|
|
20
27
|
const fs = require('fs');
|
|
@@ -30,6 +37,7 @@ function isSkillEnabled(...args) { return skillsApi.isSkillEnabled(...args); }
|
|
|
30
37
|
function loadSkillContract(...args) { return skillsApi.loadSkillContract(...args); }
|
|
31
38
|
function readSkillIntegrity(...args) { return skillsApi.readSkillIntegrity(...args); }
|
|
32
39
|
function resolveSkillSources(...args) { return skillsApi.resolveSkillSources(...args); }
|
|
40
|
+
function skillToolActionName(...args) { return skillsApi.skillToolActionName(...args); }
|
|
33
41
|
function verifySkillIntegrity(...args) { return skillsApi.verifySkillIntegrity(...args); }
|
|
34
42
|
|
|
35
43
|
const { verifyExtraDir: verifyExtraSkillDir } = require('./extra-integrity');
|
|
@@ -51,6 +59,17 @@ function makeLogger(candidate) {
|
|
|
51
59
|
};
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
function isBundledPrimarySkill(skill) {
|
|
63
|
+
if (!skill || skill.source !== 'primary') return false;
|
|
64
|
+
const sourcePath = skill.sourcePath || '';
|
|
65
|
+
if (!sourcePath || path.basename(sourcePath) !== 'skills') return false;
|
|
66
|
+
const bundleRoot = path.dirname(sourcePath);
|
|
67
|
+
return (
|
|
68
|
+
fs.existsSync(path.join(bundleRoot, 'openclaw-plugin'))
|
|
69
|
+
&& fs.existsSync(path.join(bundleRoot, 'skills'))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
/**
|
|
55
74
|
* 计算 skillDir 内"驱动热更"的关键文件的指纹(mtime 组合)。
|
|
56
75
|
* 任一文件缺失按 0 处理;出错时退化为空字符串(此时 reload 语义保守:会认为"没变")。
|
|
@@ -118,6 +137,7 @@ function createSkillRegistry(options = {}) {
|
|
|
118
137
|
pluginConfig = {},
|
|
119
138
|
wrapSensitiveTool = null,
|
|
120
139
|
builtinToolNames = [],
|
|
140
|
+
routerMode = false,
|
|
121
141
|
// When true (default when a watcher is active), we set suppressNextReload
|
|
122
142
|
// after our own setConfigValue writes so the chokidar echo doesn't cause a
|
|
123
143
|
// duplicate reload. Leave this false in test harnesses that drive reload()
|
|
@@ -125,11 +145,11 @@ function createSkillRegistry(options = {}) {
|
|
|
125
145
|
suppressSelfWrites = true,
|
|
126
146
|
} = options;
|
|
127
147
|
|
|
128
|
-
if (!api || typeof api.registerTool !== 'function') {
|
|
148
|
+
if (!routerMode && (!api || typeof api.registerTool !== 'function')) {
|
|
129
149
|
throw new Error('createSkillRegistry: api.registerTool is required');
|
|
130
150
|
}
|
|
131
151
|
|
|
132
|
-
const logger = makeLogger(options.logger || api.logger);
|
|
152
|
+
const logger = makeLogger(options.logger || (api && api.logger));
|
|
133
153
|
const configLoader = typeof options.configLoader === 'function'
|
|
134
154
|
? options.configLoader
|
|
135
155
|
: () => ({});
|
|
@@ -148,6 +168,8 @@ function createSkillRegistry(options = {}) {
|
|
|
148
168
|
const skills = new Map();
|
|
149
169
|
// toolName -> { skillId, definition, optional }
|
|
150
170
|
const toolBindings = new Map();
|
|
171
|
+
// action -> { skillId, toolName, definition, optional }
|
|
172
|
+
const actionBindings = new Map();
|
|
151
173
|
// toolName -> dispatcher object (registered once per name; kept by reference so we can
|
|
152
174
|
// mutate `description`/`parameters`/`label` on hot-reload to keep OpenClaw-visible schema in sync)
|
|
153
175
|
const dispatchers = new Map();
|
|
@@ -255,6 +277,11 @@ function createSkillRegistry(options = {}) {
|
|
|
255
277
|
removed.push(name);
|
|
256
278
|
}
|
|
257
279
|
}
|
|
280
|
+
for (const [action, binding] of actionBindings) {
|
|
281
|
+
if (binding.skillId === skillId) {
|
|
282
|
+
actionBindings.delete(action);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
258
285
|
return removed;
|
|
259
286
|
}
|
|
260
287
|
|
|
@@ -336,6 +363,7 @@ function createSkillRegistry(options = {}) {
|
|
|
336
363
|
contract,
|
|
337
364
|
adapter,
|
|
338
365
|
toolNames: toolDefs.map((t) => t.toolName),
|
|
366
|
+
actionNames: toolDefs.map((t) => skillToolActionName(skill.id, t.toolName)),
|
|
339
367
|
// runtime.dispose is used by hot-unload to drain WS, clear intervals, etc.
|
|
340
368
|
dispose: async () => {
|
|
341
369
|
const runtime = adapter && adapter.runtime;
|
|
@@ -354,17 +382,35 @@ function createSkillRegistry(options = {}) {
|
|
|
354
382
|
|
|
355
383
|
function applyBindings(state) {
|
|
356
384
|
const failedDispatchers = [];
|
|
385
|
+
const localActions = new Set();
|
|
357
386
|
for (const entry of state.toolDefs) {
|
|
358
|
-
|
|
359
|
-
|
|
387
|
+
if (!routerMode) {
|
|
388
|
+
const dispatched = ensureDispatcher(entry.toolName, entry.optional, entry.definition);
|
|
389
|
+
if (!dispatched.ok) {
|
|
390
|
+
failedDispatchers.push(entry.toolName);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const actionName = skillToolActionName(state.id, entry.toolName);
|
|
395
|
+
if (localActions.has(actionName)) {
|
|
396
|
+
logger.warn(
|
|
397
|
+
`[js-eyes] Skill "${state.id}" skipped tool "${entry.toolName}" because action "${actionName}" duplicates another tool after slug normalization`,
|
|
398
|
+
);
|
|
360
399
|
failedDispatchers.push(entry.toolName);
|
|
361
400
|
continue;
|
|
362
401
|
}
|
|
402
|
+
localActions.add(actionName);
|
|
363
403
|
toolBindings.set(entry.toolName, {
|
|
364
404
|
skillId: state.id,
|
|
365
405
|
definition: entry.definition,
|
|
366
406
|
optional: entry.optional,
|
|
367
407
|
});
|
|
408
|
+
actionBindings.set(actionName, {
|
|
409
|
+
skillId: state.id,
|
|
410
|
+
toolName: entry.toolName,
|
|
411
|
+
definition: entry.definition,
|
|
412
|
+
optional: entry.optional,
|
|
413
|
+
});
|
|
368
414
|
}
|
|
369
415
|
return { failedDispatchers };
|
|
370
416
|
}
|
|
@@ -475,9 +521,15 @@ function createSkillRegistry(options = {}) {
|
|
|
475
521
|
return { ok: false };
|
|
476
522
|
}
|
|
477
523
|
if (!integrity.hasIntegrity) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
524
|
+
if (isBundledPrimarySkill(skill)) {
|
|
525
|
+
logger.info(
|
|
526
|
+
`[js-eyes] Skill "${skill.id}" has no .integrity.json because it is loaded from the bundled/source primary skills directory (${skill.sourcePath}); load allowed. Registry-installed primary skills should carry .integrity.json for tamper checks.`,
|
|
527
|
+
);
|
|
528
|
+
} else {
|
|
529
|
+
logger.warn(
|
|
530
|
+
`[js-eyes] Skill "${skill.id}" has no .integrity.json (legacy primary install); load allowed, but reinstall via \`js-eyes skills install ${skill.id}\` to restore tamper-check metadata`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
481
533
|
}
|
|
482
534
|
return { ok: true };
|
|
483
535
|
}
|
|
@@ -646,12 +698,27 @@ function createSkillRegistry(options = {}) {
|
|
|
646
698
|
sourcePath: s.sourcePath,
|
|
647
699
|
skillDir: s.skillDir,
|
|
648
700
|
tools: s.toolNames.slice(),
|
|
701
|
+
actions: s.actionNames.slice(),
|
|
649
702
|
})),
|
|
650
703
|
toolBindings: Array.from(toolBindings.keys()),
|
|
704
|
+
actionBindings: Array.from(actionBindings.keys()),
|
|
651
705
|
dispatchersRegistered: Array.from(dispatchers.keys()),
|
|
652
706
|
};
|
|
653
707
|
}
|
|
654
708
|
|
|
709
|
+
async function executeAction(action, toolCallId, params) {
|
|
710
|
+
const binding = actionBindings.get(action);
|
|
711
|
+
if (!binding) {
|
|
712
|
+
return { content: [{ type: 'text', text: DEFAULT_UNAVAILABLE_MESSAGE(action) }] };
|
|
713
|
+
}
|
|
714
|
+
return binding.definition.execute(toolCallId, params);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getActionDefinition(action) {
|
|
718
|
+
const binding = actionBindings.get(action);
|
|
719
|
+
return binding ? binding.definition : null;
|
|
720
|
+
}
|
|
721
|
+
|
|
655
722
|
return {
|
|
656
723
|
init,
|
|
657
724
|
reload,
|
|
@@ -659,9 +726,12 @@ function createSkillRegistry(options = {}) {
|
|
|
659
726
|
disposeAll,
|
|
660
727
|
snapshot,
|
|
661
728
|
getState,
|
|
729
|
+
executeAction,
|
|
730
|
+
getActionDefinition,
|
|
662
731
|
// Testing / plugin integration helpers
|
|
663
732
|
_internals: {
|
|
664
733
|
toolBindings,
|
|
734
|
+
actionBindings,
|
|
665
735
|
skills,
|
|
666
736
|
dispatchers,
|
|
667
737
|
setSuppressNextReload(v) { suppressNextReload = Boolean(v); },
|
package/skills.js
CHANGED
|
@@ -13,6 +13,20 @@ const SKILL_CONTRACT_FILE = 'skill.contract.js';
|
|
|
13
13
|
const INTEGRITY_FILE = '.integrity.json';
|
|
14
14
|
const INSTALL_MANIFEST_FILE = 'skills-install.json';
|
|
15
15
|
|
|
16
|
+
function toolNameToActionSegment(name) {
|
|
17
|
+
return String(name || '')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
20
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '')
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function skillToolActionName(skillId, toolName) {
|
|
26
|
+
const action = toolNameToActionSegment(toolName);
|
|
27
|
+
return `skill/${skillId}/${action || 'run'}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
function loadSkillContract(skillDir) {
|
|
17
31
|
const contractPath = path.resolve(skillDir, SKILL_CONTRACT_FILE);
|
|
18
32
|
if (!fs.existsSync(contractPath)) return null;
|
|
@@ -142,8 +156,10 @@ function normalizeSkillMetadata(skillDir) {
|
|
|
142
156
|
? cli.commands.map((command) => command.name)
|
|
143
157
|
: [];
|
|
144
158
|
|
|
159
|
+
const id = contract?.id || pkg.name || path.basename(skillDir);
|
|
160
|
+
|
|
145
161
|
return {
|
|
146
|
-
id
|
|
162
|
+
id,
|
|
147
163
|
name: contract?.name || pkg.name || path.basename(skillDir),
|
|
148
164
|
version: contract?.version || pkg.version || '1.0.0',
|
|
149
165
|
description: contract?.description || pkg.description || '',
|
|
@@ -151,6 +167,7 @@ function normalizeSkillMetadata(skillDir) {
|
|
|
151
167
|
cliEntry: cli.entry ? path.resolve(skillDir, cli.entry) : path.join(skillDir, 'index.js'),
|
|
152
168
|
commands,
|
|
153
169
|
tools,
|
|
170
|
+
actions: tools.map((tool) => skillToolActionName(id, tool)),
|
|
154
171
|
runtime: contract?.runtime || {},
|
|
155
172
|
contract,
|
|
156
173
|
};
|
|
@@ -728,6 +745,8 @@ Object.assign(module.exports, {
|
|
|
728
745
|
resolveSkillsDir,
|
|
729
746
|
resolveOpenClawPluginEntry,
|
|
730
747
|
runSkillCli,
|
|
748
|
+
skillToolActionName,
|
|
749
|
+
toolNameToActionSegment,
|
|
731
750
|
verifySkillIntegrity,
|
|
732
751
|
writeIntegrityManifest,
|
|
733
752
|
});
|