morpheus-cli 0.7.4 → 0.7.6
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/README.md +4 -0
- package/dist/channels/discord.js +111 -1
- package/dist/channels/telegram.js +2 -1
- package/dist/cli/commands/start.js +22 -0
- package/dist/config/manager.js +34 -0
- package/dist/config/schemas.js +12 -0
- package/dist/devkit/registry.js +18 -3
- package/dist/devkit/tools/browser.js +1 -1
- package/dist/devkit/tools/filesystem.js +25 -9
- package/dist/devkit/tools/git.js +19 -3
- package/dist/devkit/tools/network.js +9 -2
- package/dist/devkit/tools/packages.js +1 -1
- package/dist/devkit/tools/processes.js +1 -1
- package/dist/devkit/tools/shell.js +15 -3
- package/dist/devkit/tools/system.js +1 -1
- package/dist/http/api.js +37 -2
- package/dist/runtime/apoc.js +11 -5
- package/dist/runtime/hot-reload.js +96 -0
- package/dist/runtime/keymaker.js +10 -4
- package/dist/runtime/oracle.js +16 -1
- package/dist/runtime/providers/factory.js +14 -0
- package/dist/runtime/tools/__tests__/construtor.test.js +40 -23
- package/dist/runtime/tools/apoc-tool.js +6 -0
- package/dist/runtime/tools/cache.js +227 -0
- package/dist/runtime/tools/factory.js +38 -116
- package/dist/runtime/tools/morpheus-tools.js +9 -0
- package/dist/runtime/tools/neo-tool.js +6 -0
- package/dist/runtime/tools/trinity-tool.js +6 -0
- package/dist/runtime/trinity-connector.js +40 -1
- package/dist/types/config.js +12 -1
- package/dist/ui/assets/{index-Dz_qYlIb.css → index-B6deYCij.css} +1 -1
- package/dist/ui/assets/{index-CsMDzmtQ.js → index-BTQ0jjvm.js} +52 -52
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -240,6 +240,10 @@ Discord bot responds to **DMs only** from authorized user IDs (`allowedUsers`).
|
|
|
240
240
|
| `/status` | Check Morpheus status |
|
|
241
241
|
| `/stats` | Token usage statistics |
|
|
242
242
|
| `/newsession` | Start a new session |
|
|
243
|
+
| `/mcps` | List MCP servers with tool counts |
|
|
244
|
+
| `/mcpreload` | Reload MCP connections and tools |
|
|
245
|
+
| `/mcp_enable name:` | Enable an MCP server |
|
|
246
|
+
| `/mcp_disable name:` | Disable an MCP server |
|
|
243
247
|
| `/chronos prompt: time:` | Schedule a job |
|
|
244
248
|
| `/chronos_list` | List all scheduled jobs |
|
|
245
249
|
| `/chronos_view id:` | View job + executions |
|
package/dist/channels/discord.js
CHANGED
|
@@ -74,6 +74,24 @@ const SLASH_COMMANDS = [
|
|
|
74
74
|
.setDescription('Disable a skill')
|
|
75
75
|
.addStringOption(opt => opt.setName('name').setDescription('Skill name').setRequired(true))
|
|
76
76
|
.setDMPermission(true),
|
|
77
|
+
new SlashCommandBuilder()
|
|
78
|
+
.setName('mcps')
|
|
79
|
+
.setDescription('List MCP servers and their status')
|
|
80
|
+
.setDMPermission(true),
|
|
81
|
+
new SlashCommandBuilder()
|
|
82
|
+
.setName('mcpreload')
|
|
83
|
+
.setDescription('Reload MCP tools from servers')
|
|
84
|
+
.setDMPermission(true),
|
|
85
|
+
new SlashCommandBuilder()
|
|
86
|
+
.setName('mcp_enable')
|
|
87
|
+
.setDescription('Enable an MCP server')
|
|
88
|
+
.addStringOption(opt => opt.setName('name').setDescription('MCP server name').setRequired(true))
|
|
89
|
+
.setDMPermission(true),
|
|
90
|
+
new SlashCommandBuilder()
|
|
91
|
+
.setName('mcp_disable')
|
|
92
|
+
.setDescription('Disable an MCP server')
|
|
93
|
+
.addStringOption(opt => opt.setName('name').setDescription('MCP server name').setRequired(true))
|
|
94
|
+
.setDMPermission(true),
|
|
77
95
|
].map(cmd => cmd.toJSON());
|
|
78
96
|
// ─── Adapter ──────────────────────────────────────────────────────────────────
|
|
79
97
|
export class DiscordAdapter {
|
|
@@ -367,6 +385,18 @@ export class DiscordAdapter {
|
|
|
367
385
|
case 'skill_disable':
|
|
368
386
|
await this.cmdSkillDisable(interaction);
|
|
369
387
|
break;
|
|
388
|
+
case 'mcps':
|
|
389
|
+
await this.cmdMcps(interaction);
|
|
390
|
+
break;
|
|
391
|
+
case 'mcpreload':
|
|
392
|
+
await this.cmdMcpReload(interaction);
|
|
393
|
+
break;
|
|
394
|
+
case 'mcp_enable':
|
|
395
|
+
await this.cmdMcpEnable(interaction);
|
|
396
|
+
break;
|
|
397
|
+
case 'mcp_disable':
|
|
398
|
+
await this.cmdMcpDisable(interaction);
|
|
399
|
+
break;
|
|
370
400
|
}
|
|
371
401
|
}
|
|
372
402
|
async cmdHelp(interaction) {
|
|
@@ -392,7 +422,13 @@ export class DiscordAdapter {
|
|
|
392
422
|
'`/skill_enable name:` — Enable a skill',
|
|
393
423
|
'`/skill_disable name:` — Disable a skill',
|
|
394
424
|
'',
|
|
395
|
-
'
|
|
425
|
+
'**MCP Servers**',
|
|
426
|
+
'`/mcps` — List MCP servers and status',
|
|
427
|
+
'`/mcpreload` — Reload MCP tools from servers',
|
|
428
|
+
'`/mcp_enable name:` — Enable an MCP server',
|
|
429
|
+
'`/mcp_disable name:` — Disable an MCP server',
|
|
430
|
+
'',
|
|
431
|
+
'You can also send text or voice messages to chat with the Oracle.'
|
|
396
432
|
].join('\n');
|
|
397
433
|
await interaction.reply({ content });
|
|
398
434
|
}
|
|
@@ -657,6 +693,80 @@ export class DiscordAdapter {
|
|
|
657
693
|
await interaction.reply({ content: `Error: ${err.message}` });
|
|
658
694
|
}
|
|
659
695
|
}
|
|
696
|
+
// ─── MCP Commands ─────────────────────────────────────────────────────────
|
|
697
|
+
async cmdMcps(interaction) {
|
|
698
|
+
try {
|
|
699
|
+
const { MCPManager } = await import('../config/mcp-manager.js');
|
|
700
|
+
const { Construtor } = await import('../runtime/tools/factory.js');
|
|
701
|
+
const [servers, stats] = await Promise.all([
|
|
702
|
+
MCPManager.listServers(),
|
|
703
|
+
Promise.resolve(Construtor.getStats()),
|
|
704
|
+
]);
|
|
705
|
+
if (!servers.length) {
|
|
706
|
+
await interaction.reply({ content: 'No MCP servers configured.' });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const statsMap = new Map(stats.servers.map(s => [s.name, s]));
|
|
710
|
+
const lines = servers.map(s => {
|
|
711
|
+
const status = s.enabled ? '🟢' : '🔴';
|
|
712
|
+
const serverStats = statsMap.get(s.name);
|
|
713
|
+
const toolCount = serverStats?.ok ? `(${serverStats.toolCount} tools)` :
|
|
714
|
+
serverStats?.error ? '(failed)' : '(not loaded)';
|
|
715
|
+
const transport = s.config.transport.toUpperCase();
|
|
716
|
+
return `${status} **${s.name}** ${toolCount}\n _${transport}_`;
|
|
717
|
+
});
|
|
718
|
+
const enabled = servers.filter(s => s.enabled).length;
|
|
719
|
+
const totalTools = stats.totalTools;
|
|
720
|
+
await interaction.reply({
|
|
721
|
+
content: `**MCP Servers** (${enabled}/${servers.length} enabled, ${totalTools} tools cached)\n\n${lines.join('\n')}`
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
await interaction.reply({ content: `Error: ${err.message}` });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async cmdMcpReload(interaction) {
|
|
729
|
+
await interaction.deferReply();
|
|
730
|
+
try {
|
|
731
|
+
await this.oracle.reloadTools();
|
|
732
|
+
const { Construtor } = await import('../runtime/tools/factory.js');
|
|
733
|
+
const stats = Construtor.getStats();
|
|
734
|
+
await interaction.editReply({
|
|
735
|
+
content: `✅ MCP tools reloaded: ${stats.totalTools} tools from ${stats.servers.length} servers.`
|
|
736
|
+
});
|
|
737
|
+
this.display.log(`MCP reload triggered by Discord user`, { source: 'Discord', level: 'info' });
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
await interaction.editReply({ content: `❌ Failed to reload MCP tools: ${err.message}` });
|
|
741
|
+
this.display.log(`MCP reload failed: ${err.message}`, { source: 'Discord', level: 'error' });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async cmdMcpEnable(interaction) {
|
|
745
|
+
const name = interaction.options.getString('name', true);
|
|
746
|
+
try {
|
|
747
|
+
const { MCPManager } = await import('../config/mcp-manager.js');
|
|
748
|
+
await MCPManager.setServerEnabled(name, true);
|
|
749
|
+
await interaction.reply({
|
|
750
|
+
content: `MCP server \`${name}\` enabled. Use \`/mcpreload\` to apply changes.`
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
await interaction.reply({ content: `Error: ${err.message}` });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async cmdMcpDisable(interaction) {
|
|
758
|
+
const name = interaction.options.getString('name', true);
|
|
759
|
+
try {
|
|
760
|
+
const { MCPManager } = await import('../config/mcp-manager.js');
|
|
761
|
+
await MCPManager.setServerEnabled(name, false);
|
|
762
|
+
await interaction.reply({
|
|
763
|
+
content: `MCP server \`${name}\` disabled. Use \`/mcpreload\` to apply changes.`
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
await interaction.reply({ content: `Error: ${err.message}` });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
660
770
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
661
771
|
isAuthorized(userId) {
|
|
662
772
|
return this.allowedUsers.includes(userId);
|
|
@@ -1591,7 +1591,8 @@ How can I assist you today?`;
|
|
|
1591
1591
|
try {
|
|
1592
1592
|
await ctx.reply('🔄 Reloading MCP servers...');
|
|
1593
1593
|
await this.oracle.reloadTools();
|
|
1594
|
-
|
|
1594
|
+
const stats = Construtor.getStats();
|
|
1595
|
+
await ctx.reply(`✅ MCP tools reloaded: ${stats.totalTools} tools from ${stats.servers.length} servers.`);
|
|
1595
1596
|
this.display.log(`MCP reload triggered by @${user}`, { source: 'Telegram', level: 'info' });
|
|
1596
1597
|
}
|
|
1597
1598
|
catch (error) {
|
|
@@ -12,6 +12,7 @@ import { TelegramAdapter } from '../../channels/telegram.js';
|
|
|
12
12
|
import { DiscordAdapter } from '../../channels/discord.js';
|
|
13
13
|
import { ChannelRegistry } from '../../channels/registry.js';
|
|
14
14
|
import { WebhookDispatcher } from '../../runtime/webhooks/dispatcher.js';
|
|
15
|
+
import { registerOracleForHotReload } from '../../runtime/hot-reload.js';
|
|
15
16
|
import { PATHS } from '../../config/paths.js';
|
|
16
17
|
import { Oracle } from '../../runtime/oracle.js';
|
|
17
18
|
import { ProviderError } from '../../runtime/errors.js';
|
|
@@ -23,6 +24,7 @@ import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
|
23
24
|
import { ChronosWorker } from '../../runtime/chronos/worker.js';
|
|
24
25
|
import { ChronosRepository } from '../../runtime/chronos/repository.js';
|
|
25
26
|
import { SkillRegistry } from '../../runtime/skills/index.js';
|
|
27
|
+
import { MCPToolCache } from '../../runtime/tools/cache.js';
|
|
26
28
|
// Load .env file explicitly in start command
|
|
27
29
|
const envPath = path.join(process.cwd(), '.env');
|
|
28
30
|
if (fs.existsSync(envPath)) {
|
|
@@ -138,6 +140,24 @@ export const startCommand = new Command('start')
|
|
|
138
140
|
catch (err) {
|
|
139
141
|
display.log(chalk.yellow(`Skills initialization warning: ${err.message}`), { source: 'Skills' });
|
|
140
142
|
}
|
|
143
|
+
// Initialize MCP Tool Cache before Oracle (so agents get cached tools)
|
|
144
|
+
try {
|
|
145
|
+
display.startSpinner('Loading MCP tools...');
|
|
146
|
+
const mcpCache = MCPToolCache.getInstance();
|
|
147
|
+
await mcpCache.load();
|
|
148
|
+
const stats = mcpCache.getStats();
|
|
149
|
+
display.stopSpinner();
|
|
150
|
+
if (stats.totalTools > 0) {
|
|
151
|
+
display.log(chalk.green(`✓ MCP tools cached: ${stats.totalTools} tools from ${stats.servers.length} servers`), { source: 'MCP' });
|
|
152
|
+
}
|
|
153
|
+
else if (stats.servers.length > 0) {
|
|
154
|
+
display.log(chalk.yellow(`⚠ MCP servers configured but no tools loaded`), { source: 'MCP' });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
display.stopSpinner();
|
|
159
|
+
display.log(chalk.yellow(`MCP cache warning: ${err.message}`), { source: 'MCP' });
|
|
160
|
+
}
|
|
141
161
|
// Initialize Oracle
|
|
142
162
|
const oracle = new Oracle(config);
|
|
143
163
|
try {
|
|
@@ -145,6 +165,8 @@ export const startCommand = new Command('start')
|
|
|
145
165
|
await oracle.initialize();
|
|
146
166
|
display.stopSpinner();
|
|
147
167
|
display.log(chalk.green('✓ Oracle initialized'), { source: 'Oracle' });
|
|
168
|
+
// Register Oracle for hot-reload
|
|
169
|
+
registerOracleForHotReload(oracle);
|
|
148
170
|
}
|
|
149
171
|
catch (err) {
|
|
150
172
|
display.stopSpinner();
|
package/dist/config/manager.js
CHANGED
|
@@ -287,6 +287,22 @@ export class ConfigManager {
|
|
|
287
287
|
max_active_jobs: resolveNumeric('MORPHEUS_CHRONOS_MAX_ACTIVE_JOBS', config.chronos.max_active_jobs, 100),
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
|
+
// Apply precedence to DevKit config
|
|
291
|
+
// Migration: if devkit is absent but apoc.working_dir exists, migrate it
|
|
292
|
+
const rawDevKit = config.devkit ?? {};
|
|
293
|
+
const migratedSandboxDir = rawDevKit.sandbox_dir || config.apoc?.working_dir || undefined;
|
|
294
|
+
const devkitConfig = {
|
|
295
|
+
sandbox_dir: resolveString('MORPHEUS_DEVKIT_SANDBOX_DIR', migratedSandboxDir, process.cwd()),
|
|
296
|
+
readonly_mode: resolveBoolean('MORPHEUS_DEVKIT_READONLY_MODE', rawDevKit.readonly_mode, false),
|
|
297
|
+
allowed_shell_commands: process.env.MORPHEUS_DEVKIT_ALLOWED_SHELL_COMMANDS
|
|
298
|
+
? process.env.MORPHEUS_DEVKIT_ALLOWED_SHELL_COMMANDS.split(',').map(s => s.trim()).filter(Boolean)
|
|
299
|
+
: (rawDevKit.allowed_shell_commands ?? []),
|
|
300
|
+
enable_filesystem: resolveBoolean('MORPHEUS_DEVKIT_ENABLE_FILESYSTEM', rawDevKit.enable_filesystem, true),
|
|
301
|
+
enable_shell: resolveBoolean('MORPHEUS_DEVKIT_ENABLE_SHELL', rawDevKit.enable_shell, true),
|
|
302
|
+
enable_git: resolveBoolean('MORPHEUS_DEVKIT_ENABLE_GIT', rawDevKit.enable_git, true),
|
|
303
|
+
enable_network: resolveBoolean('MORPHEUS_DEVKIT_ENABLE_NETWORK', rawDevKit.enable_network, true),
|
|
304
|
+
timeout_ms: resolveNumeric('MORPHEUS_DEVKIT_TIMEOUT_MS', rawDevKit.timeout_ms, 30_000),
|
|
305
|
+
};
|
|
290
306
|
return {
|
|
291
307
|
agent: agentConfig,
|
|
292
308
|
llm: llmConfig,
|
|
@@ -300,6 +316,8 @@ export class ConfigManager {
|
|
|
300
316
|
logging: loggingConfig,
|
|
301
317
|
memory: memoryConfig,
|
|
302
318
|
chronos: chronosConfig,
|
|
319
|
+
devkit: devkitConfig,
|
|
320
|
+
verbose_mode: resolveBoolean('MORPHEUS_VERBOSE_MODE', config.verbose_mode, true),
|
|
303
321
|
};
|
|
304
322
|
}
|
|
305
323
|
get() {
|
|
@@ -383,6 +401,22 @@ export class ConfigManager {
|
|
|
383
401
|
}
|
|
384
402
|
return defaults;
|
|
385
403
|
}
|
|
404
|
+
getDevKitConfig() {
|
|
405
|
+
const defaults = {
|
|
406
|
+
sandbox_dir: process.cwd(),
|
|
407
|
+
readonly_mode: false,
|
|
408
|
+
allowed_shell_commands: [],
|
|
409
|
+
enable_filesystem: true,
|
|
410
|
+
enable_shell: true,
|
|
411
|
+
enable_git: true,
|
|
412
|
+
enable_network: true,
|
|
413
|
+
timeout_ms: 30_000,
|
|
414
|
+
};
|
|
415
|
+
if (this.config.devkit) {
|
|
416
|
+
return { ...defaults, ...this.config.devkit };
|
|
417
|
+
}
|
|
418
|
+
return defaults;
|
|
419
|
+
}
|
|
386
420
|
/**
|
|
387
421
|
* Returns encryption status for all agent API keys.
|
|
388
422
|
*/
|
package/dist/config/schemas.js
CHANGED
|
@@ -44,6 +44,16 @@ export const ChronosConfigSchema = z.object({
|
|
|
44
44
|
check_interval_ms: z.number().min(60000).default(60000),
|
|
45
45
|
max_active_jobs: z.number().min(1).max(1000).default(100),
|
|
46
46
|
});
|
|
47
|
+
export const DevKitConfigSchema = z.object({
|
|
48
|
+
sandbox_dir: z.string().optional(),
|
|
49
|
+
readonly_mode: z.boolean().default(false),
|
|
50
|
+
allowed_shell_commands: z.array(z.string()).default([]),
|
|
51
|
+
enable_filesystem: z.boolean().default(true),
|
|
52
|
+
enable_shell: z.boolean().default(true),
|
|
53
|
+
enable_git: z.boolean().default(true),
|
|
54
|
+
enable_network: z.boolean().default(true),
|
|
55
|
+
timeout_ms: z.number().int().positive().default(30000),
|
|
56
|
+
});
|
|
47
57
|
// Zod Schema matching MorpheusConfig interface
|
|
48
58
|
export const ConfigSchema = z.object({
|
|
49
59
|
agent: z.object({
|
|
@@ -67,6 +77,8 @@ export const ConfigSchema = z.object({
|
|
|
67
77
|
}).default(DEFAULT_CONFIG.runtime?.async_tasks ?? { enabled: true }),
|
|
68
78
|
}).optional(),
|
|
69
79
|
chronos: ChronosConfigSchema.optional(),
|
|
80
|
+
devkit: DevKitConfigSchema.optional(),
|
|
81
|
+
verbose_mode: z.boolean().default(true),
|
|
70
82
|
channels: z.object({
|
|
71
83
|
telegram: z.object({
|
|
72
84
|
enabled: z.boolean().default(false),
|
package/dist/devkit/registry.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
const factories = [];
|
|
2
|
-
export function registerToolFactory(factory) {
|
|
3
|
-
factories.push(factory);
|
|
2
|
+
export function registerToolFactory(factory, category = 'system') {
|
|
3
|
+
factories.push({ category, factory });
|
|
4
4
|
}
|
|
5
|
+
/** Categories that can be toggled off via DevKit config */
|
|
6
|
+
const TOGGLEABLE_CATEGORIES = {
|
|
7
|
+
filesystem: 'enable_filesystem',
|
|
8
|
+
shell: 'enable_shell',
|
|
9
|
+
git: 'enable_git',
|
|
10
|
+
network: 'enable_network',
|
|
11
|
+
};
|
|
5
12
|
/**
|
|
6
13
|
* Builds the full DevKit tool set for a given context.
|
|
7
14
|
* Each factory receives the context (working_dir, allowed_commands, etc.)
|
|
8
15
|
* and returns tools with the context captured in closure.
|
|
16
|
+
* Disabled categories are filtered out based on context flags.
|
|
9
17
|
*/
|
|
10
18
|
export function buildDevKit(ctx) {
|
|
11
|
-
return factories
|
|
19
|
+
return factories
|
|
20
|
+
.filter(({ category }) => {
|
|
21
|
+
const ctxKey = TOGGLEABLE_CATEGORIES[category];
|
|
22
|
+
if (!ctxKey)
|
|
23
|
+
return true; // non-toggleable categories always load
|
|
24
|
+
return ctx[ctxKey] !== false;
|
|
25
|
+
})
|
|
26
|
+
.flatMap(({ factory }) => factory(ctx));
|
|
12
27
|
}
|
|
@@ -6,23 +6,31 @@ import { glob } from 'glob';
|
|
|
6
6
|
import { truncateOutput, isWithinDir } from '../utils.js';
|
|
7
7
|
import { registerToolFactory } from '../registry.js';
|
|
8
8
|
function resolveSafe(ctx, filePath) {
|
|
9
|
-
//
|
|
10
|
-
const
|
|
9
|
+
// Resolve relative to sandbox_dir (preferred) or working_dir
|
|
10
|
+
const base = ctx.sandbox_dir || ctx.working_dir;
|
|
11
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(base, filePath);
|
|
11
12
|
return resolved;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Guards a resolved path against the sandbox directory.
|
|
16
|
+
* When sandbox_dir is set, ALL paths (read and write) must be within it.
|
|
17
|
+
* When readonly_mode is true, destructive operations are blocked.
|
|
18
|
+
*/
|
|
13
19
|
function guardPath(ctx, resolved, destructive = false) {
|
|
14
|
-
//
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
// Enforce readonly_mode for destructive operations
|
|
21
|
+
if (destructive && ctx.readonly_mode) {
|
|
22
|
+
throw new Error(`Operation denied: DevKit is in read-only mode. Write/delete operations are blocked.`);
|
|
23
|
+
}
|
|
24
|
+
// Enforce sandbox_dir for ALL operations (read and write)
|
|
25
|
+
if (ctx.sandbox_dir && !isWithinDir(resolved, ctx.sandbox_dir)) {
|
|
26
|
+
throw new Error(`Path '${resolved}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.`);
|
|
20
27
|
}
|
|
21
28
|
}
|
|
22
29
|
export function createFilesystemTools(ctx) {
|
|
23
30
|
return [
|
|
24
31
|
tool(async ({ file_path, encoding, start_line, end_line }) => {
|
|
25
32
|
const resolved = resolveSafe(ctx, file_path);
|
|
33
|
+
guardPath(ctx, resolved);
|
|
26
34
|
const content = await fs.readFile(resolved, encoding ?? 'utf8');
|
|
27
35
|
const lines = content.split('\n');
|
|
28
36
|
const sliced = (start_line || end_line)
|
|
@@ -80,6 +88,7 @@ export function createFilesystemTools(ctx) {
|
|
|
80
88
|
tool(async ({ source, destination }) => {
|
|
81
89
|
const src = resolveSafe(ctx, source);
|
|
82
90
|
const dest = resolveSafe(ctx, destination);
|
|
91
|
+
guardPath(ctx, src, true);
|
|
83
92
|
guardPath(ctx, dest, true);
|
|
84
93
|
await fs.ensureDir(path.dirname(dest));
|
|
85
94
|
await fs.move(src, dest, { overwrite: true });
|
|
@@ -95,6 +104,8 @@ export function createFilesystemTools(ctx) {
|
|
|
95
104
|
tool(async ({ source, destination }) => {
|
|
96
105
|
const src = resolveSafe(ctx, source);
|
|
97
106
|
const dest = resolveSafe(ctx, destination);
|
|
107
|
+
guardPath(ctx, src);
|
|
108
|
+
guardPath(ctx, dest, true);
|
|
98
109
|
await fs.ensureDir(path.dirname(dest));
|
|
99
110
|
await fs.copy(src, dest);
|
|
100
111
|
return JSON.stringify({ success: true, from: src, to: dest });
|
|
@@ -108,6 +119,7 @@ export function createFilesystemTools(ctx) {
|
|
|
108
119
|
}),
|
|
109
120
|
tool(async ({ dir_path, recursive, pattern }) => {
|
|
110
121
|
const resolved = resolveSafe(ctx, dir_path ?? '.');
|
|
122
|
+
guardPath(ctx, resolved);
|
|
111
123
|
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
112
124
|
let results = entries.map(e => ({
|
|
113
125
|
name: e.name,
|
|
@@ -145,6 +157,7 @@ export function createFilesystemTools(ctx) {
|
|
|
145
157
|
}),
|
|
146
158
|
tool(async ({ dir_path }) => {
|
|
147
159
|
const resolved = resolveSafe(ctx, dir_path);
|
|
160
|
+
guardPath(ctx, resolved, true);
|
|
148
161
|
await fs.ensureDir(resolved);
|
|
149
162
|
return JSON.stringify({ success: true, path: resolved });
|
|
150
163
|
}, {
|
|
@@ -154,6 +167,7 @@ export function createFilesystemTools(ctx) {
|
|
|
154
167
|
}),
|
|
155
168
|
tool(async ({ file_path }) => {
|
|
156
169
|
const resolved = resolveSafe(ctx, file_path);
|
|
170
|
+
guardPath(ctx, resolved);
|
|
157
171
|
const stat = await fs.stat(resolved);
|
|
158
172
|
return JSON.stringify({
|
|
159
173
|
path: resolved,
|
|
@@ -171,6 +185,7 @@ export function createFilesystemTools(ctx) {
|
|
|
171
185
|
}),
|
|
172
186
|
tool(async ({ pattern, search_path, regex, case_insensitive, max_results }) => {
|
|
173
187
|
const base = resolveSafe(ctx, search_path ?? '.');
|
|
188
|
+
guardPath(ctx, base);
|
|
174
189
|
const files = await glob('**/*', { cwd: base, nodir: true, absolute: true });
|
|
175
190
|
const re = new RegExp(pattern, case_insensitive ? 'i' : undefined);
|
|
176
191
|
const results = [];
|
|
@@ -204,6 +219,7 @@ export function createFilesystemTools(ctx) {
|
|
|
204
219
|
}),
|
|
205
220
|
tool(async ({ pattern, search_path }) => {
|
|
206
221
|
const base = resolveSafe(ctx, search_path ?? '.');
|
|
222
|
+
guardPath(ctx, base);
|
|
207
223
|
const files = await glob(pattern, { cwd: base, absolute: true });
|
|
208
224
|
return truncateOutput(JSON.stringify(files.map(f => path.relative(base, f)), null, 2));
|
|
209
225
|
}, {
|
|
@@ -216,4 +232,4 @@ export function createFilesystemTools(ctx) {
|
|
|
216
232
|
}),
|
|
217
233
|
];
|
|
218
234
|
}
|
|
219
|
-
registerToolFactory(createFilesystemTools);
|
|
235
|
+
registerToolFactory(createFilesystemTools, 'filesystem');
|
package/dist/devkit/tools/git.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { tool } from '@langchain/core/tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
import { ShellAdapter } from '../adapters/shell.js';
|
|
4
|
-
import { truncateOutput, isCommandAllowed } from '../utils.js';
|
|
5
|
+
import { truncateOutput, isCommandAllowed, isWithinDir } from '../utils.js';
|
|
5
6
|
import { registerToolFactory } from '../registry.js';
|
|
6
7
|
export function createGitTools(ctx) {
|
|
7
8
|
const shell = ShellAdapter.create();
|
|
@@ -176,8 +177,16 @@ export function createGitTools(ctx) {
|
|
|
176
177
|
}),
|
|
177
178
|
tool(async ({ url, destination, depth }) => {
|
|
178
179
|
const args = ['clone', url];
|
|
179
|
-
if (destination)
|
|
180
|
+
if (destination) {
|
|
181
|
+
// Enforce sandbox_dir on clone destination
|
|
182
|
+
if (ctx.sandbox_dir) {
|
|
183
|
+
const resolvedDest = path.isAbsolute(destination) ? destination : path.resolve(ctx.working_dir, destination);
|
|
184
|
+
if (!isWithinDir(resolvedDest, ctx.sandbox_dir)) {
|
|
185
|
+
return JSON.stringify({ success: false, output: `Clone destination '${resolvedDest}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.` });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
180
188
|
args.push(destination);
|
|
189
|
+
}
|
|
181
190
|
if (depth)
|
|
182
191
|
args.push('--depth', String(depth));
|
|
183
192
|
const r = await git(args);
|
|
@@ -192,6 +201,13 @@ export function createGitTools(ctx) {
|
|
|
192
201
|
}),
|
|
193
202
|
}),
|
|
194
203
|
tool(async ({ path: worktreePath, branch }) => {
|
|
204
|
+
// Enforce sandbox_dir on worktree path
|
|
205
|
+
if (ctx.sandbox_dir) {
|
|
206
|
+
const resolvedPath = path.isAbsolute(worktreePath) ? worktreePath : path.resolve(ctx.working_dir, worktreePath);
|
|
207
|
+
if (!isWithinDir(resolvedPath, ctx.sandbox_dir)) {
|
|
208
|
+
return JSON.stringify({ success: false, output: `Worktree path '${resolvedPath}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.` });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
195
211
|
const args = ['worktree', 'add', worktreePath];
|
|
196
212
|
if (branch)
|
|
197
213
|
args.push('-b', branch);
|
|
@@ -207,4 +223,4 @@ export function createGitTools(ctx) {
|
|
|
207
223
|
}),
|
|
208
224
|
];
|
|
209
225
|
}
|
|
210
|
-
registerToolFactory(createGitTools);
|
|
226
|
+
registerToolFactory(createGitTools, 'git');
|
|
@@ -4,7 +4,7 @@ import net from 'net';
|
|
|
4
4
|
import dns from 'dns';
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
6
|
import path from 'path';
|
|
7
|
-
import { truncateOutput } from '../utils.js';
|
|
7
|
+
import { truncateOutput, isWithinDir } from '../utils.js';
|
|
8
8
|
import { registerToolFactory } from '../registry.js';
|
|
9
9
|
export function createNetworkTools(ctx) {
|
|
10
10
|
return [
|
|
@@ -121,6 +121,13 @@ export function createNetworkTools(ctx) {
|
|
|
121
121
|
const destPath = path.isAbsolute(destination)
|
|
122
122
|
? destination
|
|
123
123
|
: path.resolve(ctx.working_dir, destination);
|
|
124
|
+
// Enforce sandbox_dir on download destination
|
|
125
|
+
if (ctx.sandbox_dir && !isWithinDir(destPath, ctx.sandbox_dir)) {
|
|
126
|
+
return JSON.stringify({
|
|
127
|
+
success: false,
|
|
128
|
+
error: `Download destination '${destPath}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
124
131
|
await fs.ensureDir(path.dirname(destPath));
|
|
125
132
|
const controller = new AbortController();
|
|
126
133
|
const timer = setTimeout(() => controller.abort(), timeout_ms ?? 60_000);
|
|
@@ -155,4 +162,4 @@ export function createNetworkTools(ctx) {
|
|
|
155
162
|
}),
|
|
156
163
|
];
|
|
157
164
|
}
|
|
158
|
-
registerToolFactory(createNetworkTools);
|
|
165
|
+
registerToolFactory(createNetworkTools, 'network');
|
|
@@ -5,7 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { ShellAdapter } from '../adapters/shell.js';
|
|
8
|
-
import { truncateOutput, isCommandAllowed } from '../utils.js';
|
|
8
|
+
import { truncateOutput, isCommandAllowed, isWithinDir } from '../utils.js';
|
|
9
9
|
import { registerToolFactory } from '../registry.js';
|
|
10
10
|
export function createShellTools(ctx) {
|
|
11
11
|
const shell = ShellAdapter.create();
|
|
@@ -17,8 +17,20 @@ export function createShellTools(ctx) {
|
|
|
17
17
|
error: `Command '${command}' is not in the allowed_commands list for this project. Allowed: [${ctx.allowed_commands.join(', ')}]`,
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
+
// Enforce sandbox_dir: override cwd to stay within sandbox
|
|
21
|
+
let effectiveCwd = cwd ?? ctx.working_dir;
|
|
22
|
+
if (ctx.sandbox_dir) {
|
|
23
|
+
const resolvedCwd = path.isAbsolute(effectiveCwd) ? effectiveCwd : path.resolve(ctx.sandbox_dir, effectiveCwd);
|
|
24
|
+
if (!isWithinDir(resolvedCwd, ctx.sandbox_dir)) {
|
|
25
|
+
return JSON.stringify({
|
|
26
|
+
success: false,
|
|
27
|
+
error: `Working directory '${resolvedCwd}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
effectiveCwd = resolvedCwd;
|
|
31
|
+
}
|
|
20
32
|
const result = await shell.run(command, args ?? [], {
|
|
21
|
-
cwd:
|
|
33
|
+
cwd: effectiveCwd,
|
|
22
34
|
timeout_ms: timeout_ms ?? ctx.timeout_ms ?? 30_000,
|
|
23
35
|
});
|
|
24
36
|
return JSON.stringify({
|
|
@@ -91,4 +103,4 @@ export function createShellTools(ctx) {
|
|
|
91
103
|
}),
|
|
92
104
|
];
|
|
93
105
|
}
|
|
94
|
-
registerToolFactory(createShellTools);
|
|
106
|
+
registerToolFactory(createShellTools, 'shell');
|
package/dist/http/api.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ChronosWorker } from '../runtime/chronos/worker.js';
|
|
|
20
20
|
import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
|
|
21
21
|
import { createSkillsRouter } from './routers/skills.js';
|
|
22
22
|
import { getActiveEnvOverrides } from '../config/precedence.js';
|
|
23
|
+
import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
|
|
23
24
|
async function readLastLines(filePath, n) {
|
|
24
25
|
try {
|
|
25
26
|
const content = await fs.readFile(filePath, 'utf8');
|
|
@@ -479,7 +480,14 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
479
480
|
level: 'info'
|
|
480
481
|
});
|
|
481
482
|
}
|
|
482
|
-
|
|
483
|
+
// Hot-reload agents with new config
|
|
484
|
+
const hotReloadResult = await hotReloadConfig();
|
|
485
|
+
const restartRequired = getRestartRequiredChanges(oldConfig, newConfig);
|
|
486
|
+
res.json({
|
|
487
|
+
...newConfig,
|
|
488
|
+
_hotReload: hotReloadResult,
|
|
489
|
+
_restartRequired: restartRequired
|
|
490
|
+
});
|
|
483
491
|
}
|
|
484
492
|
catch (error) {
|
|
485
493
|
if (error.name === 'ZodError') {
|
|
@@ -509,6 +517,8 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
509
517
|
source: 'Zaion',
|
|
510
518
|
level: 'info'
|
|
511
519
|
});
|
|
520
|
+
// Hot-reload agents
|
|
521
|
+
await hotReloadConfig();
|
|
512
522
|
res.json({ success: true });
|
|
513
523
|
}
|
|
514
524
|
catch (error) {
|
|
@@ -565,6 +575,8 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
565
575
|
source: 'Zaion',
|
|
566
576
|
level: 'info'
|
|
567
577
|
});
|
|
578
|
+
// Hot-reload agents
|
|
579
|
+
await hotReloadConfig();
|
|
568
580
|
res.json({ success: true });
|
|
569
581
|
}
|
|
570
582
|
catch (error) {
|
|
@@ -601,6 +613,8 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
601
613
|
source: 'Zaion',
|
|
602
614
|
level: 'info'
|
|
603
615
|
});
|
|
616
|
+
// Hot-reload agents
|
|
617
|
+
await hotReloadConfig();
|
|
604
618
|
res.json({ success: true });
|
|
605
619
|
}
|
|
606
620
|
catch (error) {
|
|
@@ -752,8 +766,17 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
752
766
|
});
|
|
753
767
|
router.post('/mcp/reload', async (_req, res) => {
|
|
754
768
|
try {
|
|
769
|
+
// First reload the MCP tool cache from servers
|
|
770
|
+
await Construtor.reload();
|
|
771
|
+
// Then reinitialize agents with the new cached tools
|
|
755
772
|
await oracle.reloadTools();
|
|
756
|
-
|
|
773
|
+
const stats = Construtor.getStats();
|
|
774
|
+
res.json({
|
|
775
|
+
ok: true,
|
|
776
|
+
message: 'MCP tools reloaded successfully.',
|
|
777
|
+
totalTools: stats.totalTools,
|
|
778
|
+
servers: stats.servers,
|
|
779
|
+
});
|
|
757
780
|
}
|
|
758
781
|
catch (error) {
|
|
759
782
|
res.status(500).json({ error: 'Failed to reload MCP tools.', details: String(error) });
|
|
@@ -768,6 +791,16 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
768
791
|
res.status(500).json({ error: 'Failed to probe MCP servers.', details: String(error) });
|
|
769
792
|
}
|
|
770
793
|
});
|
|
794
|
+
// Get MCP tool cache stats (fast, no server connection)
|
|
795
|
+
router.get('/mcp/stats', async (_req, res) => {
|
|
796
|
+
try {
|
|
797
|
+
const stats = Construtor.getStats();
|
|
798
|
+
res.json(stats);
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
res.status(500).json({ error: 'Failed to get MCP stats.', details: String(error) });
|
|
802
|
+
}
|
|
803
|
+
});
|
|
771
804
|
// Keep PUT for backward compatibility if needed, or remove.
|
|
772
805
|
// Tasks says Implement POST. I'll remove PUT to avoid confusion or redirect it.
|
|
773
806
|
router.put('/config', async (req, res) => {
|
|
@@ -790,6 +823,8 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
790
823
|
await configManager.save({ ...config, trinity: req.body });
|
|
791
824
|
const display = DisplayManager.getInstance();
|
|
792
825
|
display.log('Trinity configuration updated via UI', { source: 'Zaion', level: 'info' });
|
|
826
|
+
// Hot-reload agents
|
|
827
|
+
await hotReloadConfig();
|
|
793
828
|
res.json({ success: true });
|
|
794
829
|
}
|
|
795
830
|
catch (error) {
|