neoagent 3.0.0 → 3.0.1-beta.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/flutter_app/lib/main_controller.dart +34 -0
- package/flutter_app/lib/main_security.dart +19 -0
- package/lib/schema_migrations.js +224 -0
- package/package.json +1 -1
- package/server/admin/admin.css +29 -0
- package/server/admin/admin.js +81 -1
- package/server/admin/index.html +97 -9
- package/server/db/database.js +57 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +32788 -32752
- package/server/routes/security.js +19 -0
- package/server/routes/settings.js +1 -0
- package/server/routes/skills.js +9 -5
- package/server/services/ai/hooks.js +3 -2
- package/server/services/ai/learning.js +8 -3
- package/server/services/ai/loop/agent_engine_core.js +67 -0
- package/server/services/ai/loop/conversation_loop.js +49 -13
- package/server/services/ai/settings.js +8 -0
- package/server/services/ai/systemPrompt.js +18 -0
- package/server/services/ai/toolEvidence.js +24 -0
- package/server/services/ai/toolRunner.js +262 -55
- package/server/services/ai/tools.js +5 -5
- package/server/services/cli/shell_worker_pool.js +87 -5
- package/server/services/manager.js +16 -0
- package/server/services/memory/manager.js +39 -24
- package/server/services/runtime/manager.js +6 -2
- package/server/services/security/approval_gate_service.js +108 -5
- package/server/services/security/tool_categories.js +113 -4
- package/server/services/security/tool_security_hook.js +7 -2
- package/server/services/skills/runtime.js +2 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const fs = require('fs');
|
|
2
4
|
const path = require('path');
|
|
3
5
|
const db = require('../../db/database');
|
|
4
6
|
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
5
7
|
|
|
6
8
|
const SKILLS_DIR = path.join(AGENT_DATA_DIR, 'skills');
|
|
9
|
+
const USER_SKILLS_DIR = path.join(SKILLS_DIR, 'users');
|
|
10
|
+
const LEGACY_SKILL_USER_ID = 0;
|
|
7
11
|
|
|
8
12
|
function shellEscape(value) {
|
|
9
13
|
const text = String(value ?? '');
|
|
@@ -30,13 +34,56 @@ function clampText(value, maxChars) {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
function isValidUserId(userId) {
|
|
37
|
+
return normalizeRuntimeUserId(userId) !== null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeRuntimeUserId(userId) {
|
|
33
41
|
if (typeof userId === 'number') {
|
|
34
|
-
return Number.isInteger(userId) && userId > 0;
|
|
42
|
+
return Number.isInteger(userId) && userId > 0 ? userId : null;
|
|
35
43
|
}
|
|
36
44
|
if (typeof userId === 'string') {
|
|
37
|
-
|
|
45
|
+
const trimmed = userId.trim();
|
|
46
|
+
if (!trimmed) return null;
|
|
47
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
48
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeStoredUserId(userId) {
|
|
54
|
+
if (typeof userId === 'number' && Number.isInteger(userId)) {
|
|
55
|
+
return userId;
|
|
56
|
+
}
|
|
57
|
+
if (typeof userId === 'string') {
|
|
58
|
+
const trimmed = userId.trim();
|
|
59
|
+
if (!trimmed) return LEGACY_SKILL_USER_ID;
|
|
60
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
61
|
+
return Number.isInteger(parsed) ? parsed : LEGACY_SKILL_USER_ID;
|
|
62
|
+
}
|
|
63
|
+
return LEGACY_SKILL_USER_ID;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeSkillName(name) {
|
|
67
|
+
return String(name || '')
|
|
68
|
+
.trim()
|
|
69
|
+
.replace(/[^a-z0-9-]/gi, '-')
|
|
70
|
+
.replace(/-+/g, '-')
|
|
71
|
+
.replace(/^-|-$/g, '')
|
|
72
|
+
.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildSkillKey(ownerType, ownerId, name) {
|
|
76
|
+
return `${ownerType}:${ownerId ?? 'global'}:${name}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseMetadataJson(value) {
|
|
80
|
+
if (!value || typeof value !== 'string') return {};
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(value);
|
|
83
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
84
|
+
} catch {
|
|
85
|
+
return {};
|
|
38
86
|
}
|
|
39
|
-
return false;
|
|
40
87
|
}
|
|
41
88
|
|
|
42
89
|
class SkillRunner {
|
|
@@ -47,47 +94,67 @@ class SkillRunner {
|
|
|
47
94
|
|
|
48
95
|
async loadSkills() {
|
|
49
96
|
this.skills.clear();
|
|
50
|
-
|
|
97
|
+
const dbSkills = db.prepare('SELECT * FROM skills').all();
|
|
98
|
+
const dbSkillPaths = new Set(
|
|
99
|
+
dbSkills
|
|
100
|
+
.map((skill) => path.resolve(String(skill.file_path || '')))
|
|
101
|
+
.filter(Boolean),
|
|
102
|
+
);
|
|
103
|
+
const loadedPaths = new Set();
|
|
51
104
|
|
|
52
105
|
const loadDir = (dir) => {
|
|
53
106
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
54
107
|
for (const entry of entries) {
|
|
55
108
|
const fullPath = path.join(dir, entry.name);
|
|
109
|
+
if (path.resolve(fullPath) === path.resolve(USER_SKILLS_DIR)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
56
112
|
if (entry.isDirectory()) {
|
|
57
113
|
const skillFile = path.join(fullPath, 'SKILL.md');
|
|
58
|
-
if (fs.existsSync(skillFile)) {
|
|
59
|
-
this.loadSkillFile(skillFile);
|
|
114
|
+
if (fs.existsSync(skillFile) && !dbSkillPaths.has(path.resolve(skillFile))) {
|
|
115
|
+
this.loadSkillFile(skillFile, { ownerType: 'global' });
|
|
116
|
+
loadedPaths.add(path.resolve(skillFile));
|
|
60
117
|
}
|
|
61
118
|
loadDir(fullPath);
|
|
62
|
-
} else if (
|
|
63
|
-
|
|
119
|
+
} else if (
|
|
120
|
+
entry.name.endsWith('.md')
|
|
121
|
+
&& !loadedPaths.has(path.resolve(fullPath))
|
|
122
|
+
&& !dbSkillPaths.has(path.resolve(fullPath))
|
|
123
|
+
) {
|
|
124
|
+
this.loadSkillFile(fullPath, { ownerType: 'global' });
|
|
125
|
+
loadedPaths.add(path.resolve(fullPath));
|
|
64
126
|
}
|
|
65
127
|
}
|
|
66
128
|
};
|
|
67
129
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
130
|
+
if (fs.existsSync(SKILLS_DIR)) {
|
|
131
|
+
loadDir(SKILLS_DIR);
|
|
132
|
+
}
|
|
71
133
|
for (const skill of dbSkills) {
|
|
72
134
|
if (fs.existsSync(skill.file_path)) {
|
|
73
|
-
this.loadSkillFile(skill.file_path
|
|
135
|
+
this.loadSkillFile(skill.file_path, {
|
|
136
|
+
ownerType: normalizeStoredUserId(skill.user_id) > 0 ? 'user' : 'legacy',
|
|
137
|
+
userId: normalizeStoredUserId(skill.user_id),
|
|
138
|
+
enabled: skill.enabled !== 0,
|
|
139
|
+
metadata: parseMetadataJson(skill.metadata),
|
|
140
|
+
});
|
|
74
141
|
}
|
|
75
142
|
}
|
|
76
143
|
}
|
|
77
144
|
|
|
78
|
-
loadSkillFile(filePath) {
|
|
145
|
+
loadSkillFile(filePath, options = {}) {
|
|
79
146
|
try {
|
|
80
147
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
81
|
-
const skill = this.parseSkillMd(content, filePath);
|
|
148
|
+
const skill = this.parseSkillMd(content, filePath, options);
|
|
82
149
|
if (skill) {
|
|
83
|
-
this.skills.set(skill.
|
|
150
|
+
this.skills.set(skill.key, skill);
|
|
84
151
|
}
|
|
85
152
|
} catch (err) {
|
|
86
153
|
console.error(`Failed to load skill from ${filePath}:`, err.message);
|
|
87
154
|
}
|
|
88
155
|
}
|
|
89
156
|
|
|
90
|
-
parseSkillMd(content, filePath) {
|
|
157
|
+
parseSkillMd(content, filePath, options = {}) {
|
|
91
158
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)/);
|
|
92
159
|
if (!frontmatterMatch) return null;
|
|
93
160
|
|
|
@@ -109,15 +176,32 @@ class SkillRunner {
|
|
|
109
176
|
}
|
|
110
177
|
}
|
|
111
178
|
|
|
112
|
-
|
|
179
|
+
const name = normalizeSkillName(metadata.name);
|
|
180
|
+
if (!name) return null;
|
|
181
|
+
|
|
182
|
+
const ownerType = options.ownerType || 'global';
|
|
183
|
+
const userId = ownerType === 'user' || ownerType === 'legacy'
|
|
184
|
+
? normalizeStoredUserId(options.userId)
|
|
185
|
+
: null;
|
|
186
|
+
const mergedMetadata = {
|
|
187
|
+
...metadata,
|
|
188
|
+
...(options.metadata && typeof options.metadata === 'object' ? options.metadata : {}),
|
|
189
|
+
};
|
|
190
|
+
if (typeof options.enabled === 'boolean') {
|
|
191
|
+
mergedMetadata.enabled = options.enabled;
|
|
192
|
+
}
|
|
113
193
|
|
|
114
194
|
return {
|
|
115
|
-
|
|
195
|
+
key: buildSkillKey(ownerType, userId, name),
|
|
196
|
+
name,
|
|
116
197
|
description: metadata.description || '',
|
|
117
|
-
metadata,
|
|
198
|
+
metadata: mergedMetadata,
|
|
118
199
|
instructions: body.trim(),
|
|
119
200
|
filePath,
|
|
120
|
-
dir: path.dirname(filePath)
|
|
201
|
+
dir: path.dirname(filePath),
|
|
202
|
+
ownerType,
|
|
203
|
+
userId,
|
|
204
|
+
readOnly: ownerType !== 'user',
|
|
121
205
|
};
|
|
122
206
|
}
|
|
123
207
|
|
|
@@ -125,7 +209,7 @@ class SkillRunner {
|
|
|
125
209
|
const maxTotalChars = options.maxTotalChars || 9000;
|
|
126
210
|
const maxDescriptionChars = options.maxDescriptionChars || 220;
|
|
127
211
|
const maxTriggerChars = options.maxTriggerChars || 120;
|
|
128
|
-
const skills =
|
|
212
|
+
const skills = this.getAll(options.userId)
|
|
129
213
|
.filter((skill) => skill.metadata.enabled !== false)
|
|
130
214
|
.sort((a, b) => {
|
|
131
215
|
const categoryCompare = String(a.metadata?.category || 'general')
|
|
@@ -166,9 +250,9 @@ class SkillRunner {
|
|
|
166
250
|
return `\n${lines.join('\n')}`;
|
|
167
251
|
}
|
|
168
252
|
|
|
169
|
-
getToolDefinitions() {
|
|
253
|
+
getToolDefinitions(options = {}) {
|
|
170
254
|
const tools = [];
|
|
171
|
-
for (const skill of this.
|
|
255
|
+
for (const skill of this.getAll(options.userId)) {
|
|
172
256
|
if (skill.metadata.enabled !== false && skill.metadata.tool) {
|
|
173
257
|
tools.push({
|
|
174
258
|
name: skill.name,
|
|
@@ -181,7 +265,7 @@ class SkillRunner {
|
|
|
181
265
|
}
|
|
182
266
|
|
|
183
267
|
async executeTool(toolName, args, context = {}) {
|
|
184
|
-
const skill = this.
|
|
268
|
+
const skill = this.getSkill(toolName, context.userId);
|
|
185
269
|
if (!skill) return null;
|
|
186
270
|
const metricContext = {
|
|
187
271
|
userId: context.userId,
|
|
@@ -272,28 +356,70 @@ class SkillRunner {
|
|
|
272
356
|
);
|
|
273
357
|
}
|
|
274
358
|
|
|
275
|
-
createSkill(name, description, instructions, metadata = {}) {
|
|
276
|
-
const
|
|
277
|
-
|
|
359
|
+
createSkill(userId, name, description, instructions, metadata = {}) {
|
|
360
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
361
|
+
if (ownerId === null) {
|
|
362
|
+
return { error: 'Missing or invalid userId' };
|
|
363
|
+
}
|
|
364
|
+
const safeName = normalizeSkillName(name);
|
|
365
|
+
if (!safeName) {
|
|
366
|
+
return { error: 'Skill name is required' };
|
|
367
|
+
}
|
|
368
|
+
if (this._findOwnedSkill(safeName, ownerId)) {
|
|
369
|
+
return { error: `Skill '${safeName}' already exists` };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const metaToWrite = metadata && typeof metadata === 'object' ? metadata : {};
|
|
373
|
+
const skillDir = path.join(USER_SKILLS_DIR, String(ownerId), safeName);
|
|
278
374
|
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
279
375
|
|
|
280
|
-
const frontmatter = this._buildFrontmatter(safeName, description,
|
|
376
|
+
const frontmatter = this._buildFrontmatter(safeName, description, metaToWrite);
|
|
281
377
|
const filePath = path.join(skillDir, 'SKILL.md');
|
|
282
378
|
fs.writeFileSync(filePath, frontmatter + `\n\n${instructions}`);
|
|
283
379
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
380
|
+
try {
|
|
381
|
+
db.prepare(`
|
|
382
|
+
INSERT INTO skills (user_id, name, description, file_path, metadata, enabled, auto_created, updated_at)
|
|
383
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, datetime('now'))
|
|
384
|
+
`).run(
|
|
385
|
+
ownerId,
|
|
386
|
+
safeName,
|
|
387
|
+
description,
|
|
388
|
+
filePath,
|
|
389
|
+
JSON.stringify(metaToWrite),
|
|
390
|
+
metaToWrite.enabled === false ? 0 : 1,
|
|
391
|
+
);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
try {
|
|
394
|
+
fs.unlinkSync(filePath);
|
|
395
|
+
} catch {}
|
|
396
|
+
try {
|
|
397
|
+
const remaining = fs.readdirSync(skillDir);
|
|
398
|
+
if (remaining.length === 0) fs.rmdirSync(skillDir);
|
|
399
|
+
} catch {}
|
|
400
|
+
if (String(err?.message || '').includes('UNIQUE')) {
|
|
401
|
+
return { error: `Skill '${safeName}' already exists` };
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
288
405
|
|
|
289
|
-
this.loadSkillFile(filePath
|
|
406
|
+
this.loadSkillFile(filePath, {
|
|
407
|
+
ownerType: 'user',
|
|
408
|
+
userId: ownerId,
|
|
409
|
+
enabled: metaToWrite.enabled !== false,
|
|
410
|
+
metadata: metaToWrite,
|
|
411
|
+
});
|
|
290
412
|
|
|
291
413
|
return { success: true, name: safeName, path: filePath };
|
|
292
414
|
}
|
|
293
415
|
|
|
294
|
-
updateSkill(name, { description, instructions, metadata } = {}) {
|
|
295
|
-
const
|
|
416
|
+
updateSkill(userId, name, { description, instructions, metadata } = {}) {
|
|
417
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
418
|
+
const skill = this.getSkill(name, ownerId);
|
|
296
419
|
if (!skill) return { error: `Skill '${name}' not found` };
|
|
420
|
+
if (!this._canEditSkill(skill, ownerId)) {
|
|
421
|
+
return { error: `Skill '${skill.name}' is read-only`, code: 'forbidden' };
|
|
422
|
+
}
|
|
297
423
|
|
|
298
424
|
const newDesc = description !== undefined ? description : skill.description;
|
|
299
425
|
const newInstructions = instructions !== undefined ? instructions : skill.instructions;
|
|
@@ -308,29 +434,57 @@ class SkillRunner {
|
|
|
308
434
|
metaToWrite = existing;
|
|
309
435
|
}
|
|
310
436
|
|
|
311
|
-
const frontmatter = this._buildFrontmatter(name, newDesc, metaToWrite);
|
|
437
|
+
const frontmatter = this._buildFrontmatter(skill.name, newDesc, metaToWrite);
|
|
312
438
|
fs.writeFileSync(skill.filePath, frontmatter + `\n\n${newInstructions}`);
|
|
313
|
-
db.prepare(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
439
|
+
db.prepare(
|
|
440
|
+
`UPDATE skills
|
|
441
|
+
SET description = ?, metadata = ?, enabled = ?, updated_at = datetime('now')
|
|
442
|
+
WHERE user_id = ? AND name = ?`
|
|
443
|
+
).run(
|
|
444
|
+
newDesc,
|
|
445
|
+
JSON.stringify(metaToWrite || {}),
|
|
446
|
+
metaToWrite?.enabled === false ? 0 : 1,
|
|
447
|
+
ownerId,
|
|
448
|
+
skill.name,
|
|
449
|
+
);
|
|
450
|
+
this.loadSkillFile(skill.filePath, {
|
|
451
|
+
ownerType: 'user',
|
|
452
|
+
userId: ownerId,
|
|
453
|
+
enabled: metaToWrite?.enabled !== false,
|
|
454
|
+
metadata: metaToWrite,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
return { success: true, name: skill.name, path: skill.filePath };
|
|
318
458
|
}
|
|
319
459
|
|
|
320
|
-
getSkill(name) {
|
|
321
|
-
|
|
460
|
+
getSkill(name, userId = null) {
|
|
461
|
+
const skillName = normalizeSkillName(name);
|
|
462
|
+
if (!skillName) return null;
|
|
463
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
464
|
+
if (ownerId !== null) {
|
|
465
|
+
const owned = this._findOwnedSkill(skillName, ownerId);
|
|
466
|
+
if (owned) return owned;
|
|
467
|
+
}
|
|
468
|
+
return this._findVisibleSkill(skillName, ownerId);
|
|
322
469
|
}
|
|
323
470
|
|
|
324
|
-
setSkillEnabled(name, enabled) {
|
|
325
|
-
const skill = this.
|
|
471
|
+
setSkillEnabled(userId, name, enabled) {
|
|
472
|
+
const skill = this.getSkill(name, userId);
|
|
326
473
|
if (!skill) return { error: `Skill '${name}' not found` };
|
|
474
|
+
if (!this._canEditSkill(skill, normalizeRuntimeUserId(userId))) {
|
|
475
|
+
return { error: `Skill '${skill.name}' is read-only`, code: 'forbidden' };
|
|
476
|
+
}
|
|
327
477
|
const metadata = { ...skill.metadata, enabled: !!enabled };
|
|
328
|
-
return this.updateSkill(name, { metadata });
|
|
478
|
+
return this.updateSkill(userId, skill.name, { metadata });
|
|
329
479
|
}
|
|
330
480
|
|
|
331
|
-
deleteSkill(name) {
|
|
332
|
-
const
|
|
481
|
+
deleteSkill(userId, name) {
|
|
482
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
483
|
+
const skill = this.getSkill(name, ownerId);
|
|
333
484
|
if (!skill) return { error: `Skill '${name}' not found` };
|
|
485
|
+
if (!this._canEditSkill(skill, ownerId)) {
|
|
486
|
+
return { error: `Skill '${skill.name}' is read-only`, code: 'forbidden' };
|
|
487
|
+
}
|
|
334
488
|
|
|
335
489
|
try {
|
|
336
490
|
fs.unlinkSync(skill.filePath);
|
|
@@ -341,10 +495,10 @@ class SkillRunner {
|
|
|
341
495
|
}
|
|
342
496
|
} catch (e) { /* ignore */ }
|
|
343
497
|
|
|
344
|
-
db.prepare('DELETE FROM skills WHERE name = ?').run(name);
|
|
345
|
-
this.skills.delete(
|
|
498
|
+
db.prepare('DELETE FROM skills WHERE user_id = ? AND name = ?').run(ownerId, skill.name);
|
|
499
|
+
this.skills.delete(skill.key);
|
|
346
500
|
|
|
347
|
-
return { success: true, deleted: name };
|
|
501
|
+
return { success: true, deleted: skill.name };
|
|
348
502
|
}
|
|
349
503
|
|
|
350
504
|
_buildFrontmatter(name, description, metadata = {}) {
|
|
@@ -361,15 +515,68 @@ class SkillRunner {
|
|
|
361
515
|
return fm;
|
|
362
516
|
}
|
|
363
517
|
|
|
364
|
-
getAll() {
|
|
365
|
-
return
|
|
518
|
+
getAll(userId = null) {
|
|
519
|
+
return this._getVisibleSkills(userId).map((s) => ({
|
|
366
520
|
name: s.name,
|
|
367
521
|
description: s.description,
|
|
368
522
|
metadata: s.metadata,
|
|
369
523
|
filePath: s.filePath,
|
|
370
|
-
enabled: s.metadata.enabled !== false
|
|
524
|
+
enabled: s.metadata.enabled !== false,
|
|
525
|
+
userId: s.userId,
|
|
526
|
+
ownerType: s.ownerType,
|
|
527
|
+
readOnly: s.readOnly,
|
|
371
528
|
}));
|
|
372
529
|
}
|
|
530
|
+
|
|
531
|
+
findSkillByWorkflowSignature(userId, workflowSignature) {
|
|
532
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
533
|
+
if (ownerId === null || !workflowSignature) return null;
|
|
534
|
+
return Array.from(this.skills.values()).find(
|
|
535
|
+
(skill) => skill.ownerType === 'user'
|
|
536
|
+
&& skill.userId === ownerId
|
|
537
|
+
&& skill.metadata?.workflow_signature === workflowSignature,
|
|
538
|
+
) || null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
_findOwnedSkill(name, userId) {
|
|
542
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
543
|
+
if (ownerId === null) return null;
|
|
544
|
+
return this.skills.get(buildSkillKey('user', ownerId, normalizeSkillName(name))) || null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
_findVisibleSkill(name, userId) {
|
|
548
|
+
const skillName = normalizeSkillName(name);
|
|
549
|
+
return this._getVisibleSkills(userId).find((skill) => skill.name === skillName) || null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
_getVisibleSkills(userId = null) {
|
|
553
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
554
|
+
const visible = Array.from(this.skills.values()).filter((skill) => {
|
|
555
|
+
if (ownerId === null) return true;
|
|
556
|
+
if (skill.ownerType === 'user') return skill.userId === ownerId;
|
|
557
|
+
return true;
|
|
558
|
+
});
|
|
559
|
+
if (ownerId === null) return visible;
|
|
560
|
+
const ownedNames = new Set(
|
|
561
|
+
visible
|
|
562
|
+
.filter((skill) => skill.ownerType === 'user' && skill.userId === ownerId)
|
|
563
|
+
.map((skill) => skill.name),
|
|
564
|
+
);
|
|
565
|
+
return visible.filter((skill) => {
|
|
566
|
+
if (skill.ownerType === 'user') return skill.userId === ownerId;
|
|
567
|
+
return !ownedNames.has(skill.name);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
_canEditSkill(skill, userId) {
|
|
572
|
+
const ownerId = normalizeRuntimeUserId(userId);
|
|
573
|
+
return skill?.ownerType === 'user' && skill.userId === ownerId;
|
|
574
|
+
}
|
|
373
575
|
}
|
|
374
576
|
|
|
375
|
-
module.exports = {
|
|
577
|
+
module.exports = {
|
|
578
|
+
SkillRunner,
|
|
579
|
+
LEGACY_SKILL_USER_ID,
|
|
580
|
+
USER_SKILLS_DIR,
|
|
581
|
+
normalizeSkillName,
|
|
582
|
+
};
|
|
@@ -2588,25 +2588,25 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2588
2588
|
const { SkillRunner } = require('./toolRunner');
|
|
2589
2589
|
const sharedRunner = sk();
|
|
2590
2590
|
if (sharedRunner) {
|
|
2591
|
-
const result = sharedRunner.createSkill(args.name, args.description, args.instructions, args.metadata);
|
|
2591
|
+
const result = sharedRunner.createSkill(userId, args.name, args.description, args.instructions, args.metadata);
|
|
2592
2592
|
return result;
|
|
2593
2593
|
}
|
|
2594
2594
|
const runner = new SkillRunner();
|
|
2595
2595
|
await runner.loadSkills();
|
|
2596
|
-
return runner.createSkill(args.name, args.description, args.instructions, args.metadata);
|
|
2596
|
+
return runner.createSkill(userId, args.name, args.description, args.instructions, args.metadata);
|
|
2597
2597
|
}
|
|
2598
2598
|
|
|
2599
2599
|
case 'list_skills': {
|
|
2600
2600
|
const skillRunner = sk();
|
|
2601
2601
|
if (!skillRunner) return { error: 'Skill runner not available' };
|
|
2602
|
-
const all = skillRunner.getAll();
|
|
2602
|
+
const all = skillRunner.getAll(userId);
|
|
2603
2603
|
return { skills: all, count: all.length };
|
|
2604
2604
|
}
|
|
2605
2605
|
|
|
2606
2606
|
case 'update_skill': {
|
|
2607
2607
|
const skillRunner = sk();
|
|
2608
2608
|
if (!skillRunner) return { error: 'Skill runner not available' };
|
|
2609
|
-
return skillRunner.updateSkill(args.name, {
|
|
2609
|
+
return skillRunner.updateSkill(userId, args.name, {
|
|
2610
2610
|
description: args.description,
|
|
2611
2611
|
instructions: args.instructions,
|
|
2612
2612
|
metadata: args.metadata
|
|
@@ -2616,7 +2616,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2616
2616
|
case 'delete_skill': {
|
|
2617
2617
|
const skillRunner = sk();
|
|
2618
2618
|
if (!skillRunner) return { error: 'Skill runner not available' };
|
|
2619
|
-
return skillRunner.deleteSkill(args.name);
|
|
2619
|
+
return skillRunner.deleteSkill(userId, args.name);
|
|
2620
2620
|
}
|
|
2621
2621
|
|
|
2622
2622
|
case 'think': {
|
|
@@ -5,6 +5,60 @@ const { randomUUID } = require('crypto');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const WORKER_SCRIPT = path.resolve(__dirname, 'shell_worker.js');
|
|
8
|
+
const DEFAULT_WORKER_ENV_KEYS = Object.freeze([
|
|
9
|
+
'HOME',
|
|
10
|
+
'PATH',
|
|
11
|
+
'SHELL',
|
|
12
|
+
'TMPDIR',
|
|
13
|
+
'TMP',
|
|
14
|
+
'TEMP',
|
|
15
|
+
'TERM',
|
|
16
|
+
'LANG',
|
|
17
|
+
'LC_ALL',
|
|
18
|
+
'LC_CTYPE',
|
|
19
|
+
'USER',
|
|
20
|
+
'LOGNAME',
|
|
21
|
+
'COLORTERM',
|
|
22
|
+
'SystemRoot',
|
|
23
|
+
'ComSpec',
|
|
24
|
+
'PATHEXT',
|
|
25
|
+
'WINDIR',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalizeExtraEnvKeys(extraEnvKeys = process.env.NEOAGENT_SHELL_WORKER_ENV_ALLOWLIST || '') {
|
|
29
|
+
if (Array.isArray(extraEnvKeys)) {
|
|
30
|
+
return extraEnvKeys.map((key) => String(key || '').trim()).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
return String(extraEnvKeys || '')
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((key) => key.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildWorkerEnv(baseEnv = process.env, extraEnvKeys) {
|
|
39
|
+
const allowedKeys = new Set([
|
|
40
|
+
...DEFAULT_WORKER_ENV_KEYS,
|
|
41
|
+
...normalizeExtraEnvKeys(extraEnvKeys),
|
|
42
|
+
]);
|
|
43
|
+
const workerEnv = {};
|
|
44
|
+
for (const key of allowedKeys) {
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(baseEnv, key) && baseEnv[key] != null) {
|
|
46
|
+
workerEnv[key] = String(baseEnv[key]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return workerEnv;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createShutdownResult(message, killed = false) {
|
|
53
|
+
return {
|
|
54
|
+
exitCode: -1,
|
|
55
|
+
stdout: '',
|
|
56
|
+
stderr: message,
|
|
57
|
+
killed,
|
|
58
|
+
timedOut: false,
|
|
59
|
+
durationMs: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
8
62
|
|
|
9
63
|
class ShellWorkerPool {
|
|
10
64
|
/**
|
|
@@ -21,6 +75,7 @@ class ShellWorkerPool {
|
|
|
21
75
|
this._queue = [];
|
|
22
76
|
/** @type {Map<string, Function>} requestId → resolve */
|
|
23
77
|
this._pending = new Map();
|
|
78
|
+
this._shuttingDown = false;
|
|
24
79
|
|
|
25
80
|
for (let i = 0; i < this._size; i++) {
|
|
26
81
|
this._spawnWorker();
|
|
@@ -31,7 +86,7 @@ class ShellWorkerPool {
|
|
|
31
86
|
const proc = fork(this._workerScript, [], {
|
|
32
87
|
detached: false,
|
|
33
88
|
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
34
|
-
env:
|
|
89
|
+
env: buildWorkerEnv(process.env),
|
|
35
90
|
});
|
|
36
91
|
|
|
37
92
|
const workerEntry = { proc, busy: false, pendingRequestId: null };
|
|
@@ -52,7 +107,6 @@ class ShellWorkerPool {
|
|
|
52
107
|
});
|
|
53
108
|
|
|
54
109
|
proc.on('exit', (code) => {
|
|
55
|
-
console.warn(`[ShellWorkerPool] Worker exited (code=${code}), respawning`);
|
|
56
110
|
const idx = this._workers.indexOf(workerEntry);
|
|
57
111
|
if (idx !== -1) this._workers.splice(idx, 1);
|
|
58
112
|
|
|
@@ -61,10 +115,14 @@ class ShellWorkerPool {
|
|
|
61
115
|
const resolve = this._pending.get(workerEntry.pendingRequestId);
|
|
62
116
|
if (resolve) {
|
|
63
117
|
this._pending.delete(workerEntry.pendingRequestId);
|
|
64
|
-
|
|
118
|
+
const message = this._shuttingDown ? 'Server shutting down' : 'Worker process crashed';
|
|
119
|
+
resolve(createShutdownResult(message, true));
|
|
65
120
|
}
|
|
66
121
|
}
|
|
67
122
|
|
|
123
|
+
if (this._shuttingDown) return;
|
|
124
|
+
|
|
125
|
+
console.warn(`[ShellWorkerPool] Worker exited (code=${code}), respawning`);
|
|
68
126
|
this._spawnWorker();
|
|
69
127
|
});
|
|
70
128
|
|
|
@@ -113,13 +171,37 @@ class ShellWorkerPool {
|
|
|
113
171
|
|
|
114
172
|
/** Gracefully terminate all workers. */
|
|
115
173
|
shutdown() {
|
|
174
|
+
if (this._shuttingDown) return;
|
|
175
|
+
this._shuttingDown = true;
|
|
176
|
+
|
|
177
|
+
const shutdownResult = createShutdownResult('Server shutting down', true);
|
|
178
|
+
for (const job of this._queue) {
|
|
179
|
+
job.resolve(shutdownResult);
|
|
180
|
+
}
|
|
181
|
+
this._queue = [];
|
|
182
|
+
|
|
183
|
+
for (const w of this._workers) {
|
|
184
|
+
if (w.pendingRequestId) {
|
|
185
|
+
const resolve = this._pending.get(w.pendingRequestId);
|
|
186
|
+
if (resolve) {
|
|
187
|
+
this._pending.delete(w.pendingRequestId);
|
|
188
|
+
resolve(shutdownResult);
|
|
189
|
+
}
|
|
190
|
+
w.pendingRequestId = null;
|
|
191
|
+
w.busy = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
116
195
|
for (const w of this._workers) {
|
|
117
196
|
try { w.proc.kill(); } catch {}
|
|
118
197
|
}
|
|
119
198
|
this._workers = [];
|
|
120
|
-
this._queue = [];
|
|
121
199
|
this._pending.clear();
|
|
122
200
|
}
|
|
123
201
|
}
|
|
124
202
|
|
|
125
|
-
module.exports = {
|
|
203
|
+
module.exports = {
|
|
204
|
+
ShellWorkerPool,
|
|
205
|
+
DEFAULT_WORKER_ENV_KEYS,
|
|
206
|
+
buildWorkerEnv,
|
|
207
|
+
};
|
|
@@ -571,6 +571,22 @@ async function startServices(app, io) {
|
|
|
571
571
|
async function stopServices(app) {
|
|
572
572
|
const tasks = [];
|
|
573
573
|
console.log('[Services] Stopping services');
|
|
574
|
+
if (app.locals.approvalGateService && typeof app.locals.approvalGateService.shutdown === 'function') {
|
|
575
|
+
try {
|
|
576
|
+
app.locals.approvalGateService.shutdown();
|
|
577
|
+
logServiceReady('Pending approvals expired');
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.error('[ApprovalGate] Shutdown error:', getErrorMessage(err));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (app.locals.agentEngine && typeof app.locals.agentEngine.interruptAllActiveRuns === 'function') {
|
|
583
|
+
try {
|
|
584
|
+
app.locals.agentEngine.interruptAllActiveRuns();
|
|
585
|
+
logServiceReady('Active runs marked interrupted');
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error('[AgentEngine] Interrupt error:', getErrorMessage(err));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
574
590
|
if (app.locals.memoryManager) {
|
|
575
591
|
app.locals.memoryManager.stopEmbeddingIndexBackfill();
|
|
576
592
|
}
|