Skip to content

feat: MindServer HUD, Socket.IO API, Discord bot, agent process manager#725

Open
Z0mb13V1 wants to merge 1 commit intomindcraft-bots:developfrom
Z0mb13V1:feat/pr6-web-ui-mindserver
Open

feat: MindServer HUD, Socket.IO API, Discord bot, agent process manager#725
Z0mb13V1 wants to merge 1 commit intomindcraft-bots:developfrom
Z0mb13V1:feat/pr6-web-ui-mindserver

Conversation

@Z0mb13V1
Copy link

@Z0mb13V1 Z0mb13V1 commented Mar 4, 2026

Live browser HUD, WebSocket agent registry, Discord remote control, and robust agent process management. See branch for full diff.

- mindserver.js: Express + Socket.IO server — agent registry, HUD websocket,
  CORS/security headers, health endpoint, live agent state streaming
- mindcraft.js: core coordinator — agent lifecycle, MindServer integration
- public/index.html: browser HUD — live bot status, log viewer, controls
- agent_process.js: child process manager with crash detection and restart
- init_agent.js: standardized agent bootstrap (profile load, key inject, start)
- discord-bot.js: Discord slash commands for remote bot control; path traversal
  guard on profile names; cross-platform path.sep validation
Copilot AI review requested due to automatic review settings March 4, 2026 01:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a live browser HUD to the MindServer dashboard, a WebSocket-based remote agent API, a Discord bot for remote control of Mindcraft agents, and improvements to the agent process manager with more robust crash recovery and restart logic.

Changes:

  • Adds a Discord bot (discord-bot.js) supporting multi-agent monitoring, admin commands, auto-fix via Gemini AI, and two-MindServer support
  • Extends the MindServer with CORS configuration, security headers, remote agent registration, usage reporting endpoints, and a /health API
  • Enhances the agent process manager with exponential backoff crash recovery, exit code 88 (name-conflict) handling, and remote-mode subprocess support

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
discord-bot.js New 1258-line Discord bot providing agent monitoring, chat relay, mode-switching, auto-fix, and admin commands
src/mindcraft/mindserver.js Adds CORS config, security headers, remote agent registration, get-agent-usage/get-all-usage endpoints, login time tracking, and /health endpoint
src/mindcraft/public/index.html Adds HUD runtime badge, task/goal display panel, command log, and viewer toolbar with placeholder
src/mindcraft/mindcraft.js Adds createRemoteAgent() function for spawning agents pointing to a remote MindServer
src/process/agent_process.js Extends constructor with remote URL/settings-file params; improves crash recovery with exponential backoff and exit code 88 handling
src/process/init_agent.js Adds global non-fatal error handler and --url/--settings_file CLI options for remote mode

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

function reloadViewer(name) {
const iframe = document.getElementById(`viewer-${name}`);
if (iframe) { iframe.src = iframe.src; }
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reloadViewer function is defined as a local function inside the <script> block (line 1032) but is never assigned to window.reloadViewer. All other inline-onclick functions (clearCommandLog, toggleCommandLog, openAgentSettings, etc.) are explicitly exposed via window.functionName = functionName. Since inline onclick handlers look up names in the global scope, calling reloadViewer('...') from the HTML button onclick will throw a ReferenceError: reloadViewer is not defined. Add window.reloadViewer = reloadViewer; after the function definition.

Suggested change
}
}
window.reloadViewer = reloadViewer;

Copilot uses AI. Check for mistakes.
Comment on lines +298 to +309
socket.on('get-agent-usage', (agentName, callback) => {
const conn = agent_connections[agentName];
// If agent is in-game, query live data with disk fallback on timeout
if (conn && conn.socket && conn.in_game) {
const timeout = setTimeout(() => {
const diskData = readUsageFromDisk(agentName);
callback(diskData ? { usage: diskData } : { error: 'Timeout' });
}, 5000);
conn.socket.emit('get-usage', (data) => {
clearTimeout(timeout);
callback({ usage: data });
});
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get-usage socket event emitted on the agent's socket has no corresponding handler registered in src/agent/mindserver_proxy.js. The MindServer emits get-usage expecting an acknowledgement callback with usage data, but the agent-side proxy never registers a socket.on('get-usage', ...) listener. As a result, the callback will never be called and the 5-second timeout will always fire for in-game agents, falling back to disk data.

Copilot uses AI. Check for mistakes.
import { fileURLToPath } from 'url';
import { validateDiscordMessage } from './src/utils/message_validator.js';
import { RateLimiter } from './src/utils/rate_limiter.js';
import { deepSanitize } from './settings.js';
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deepSanitize function is imported from ./settings.js but that file does not export any such function — it only exports a default settings object. This import will fail at module load time, crashing the Discord bot immediately on startup. The deepSanitize function needs to be either defined in settings.js and exported, or defined directly in discord-bot.js.

Copilot uses AI. Check for mistakes.
return;
}

if (code !== 0 && signal !== 'SIGINT') {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the agent process exits due to a signal other than SIGINT (e.g., SIGTERM or SIGKILL), code is null and signal is the signal name. The check code !== 0 evaluates to null !== 0 which is true, so the restart logic will incorrectly attempt to restart the agent even when it was intentionally killed with SIGTERM. The condition should also check signal === null (or code !== null) to avoid restarting on signal-based exits.

Suggested change
if (code !== 0 && signal !== 'SIGINT') {
if (code !== 0 && signal === null) {

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +73
// Wait 15s before restarting to let MC server release the old session
const RESTART_DELAY_MS = 30000;
console.log(`Restarting agent in ${RESTART_DELAY_MS / 1000}s...`);
setTimeout(() => {
this.start(true, 'Agent process restarted.', count_id, this.port);
last_restart = Date.now();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last_restart = Date.now() assignment on line 73 (inside the setTimeout callback) is dead code. this.start() on line 72 creates a new invocation of start() which establishes its own let last_restart = Date.now() closure variable. Updating the old last_restart after calling this.start() has no effect on the new process's exit handler. The comment on line 68 also says "Wait 15s" but the actual delay is 30s (RESTART_DELAY_MS = 30000). Remove the last_restart = Date.now() line inside the timeout.

Suggested change
// Wait 15s before restarting to let MC server release the old session
const RESTART_DELAY_MS = 30000;
console.log(`Restarting agent in ${RESTART_DELAY_MS / 1000}s...`);
setTimeout(() => {
this.start(true, 'Agent process restarted.', count_id, this.port);
last_restart = Date.now();
// Wait 30s before restarting to let MC server release the old session
const RESTART_DELAY_MS = 30000;
console.log(`Restarting agent in ${RESTART_DELAY_MS / 1000}s...`);
setTimeout(() => {
this.start(true, 'Agent process restarted.', count_id, this.port);

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +63
this._quickExits = (this._quickExits || 0) + 1
if (this._quickExits > 3) {
console.error('Agent crashed 3+ times rapidly. Stopping restarts.')
return
}
const delay = Math.max(20000, this._quickExits * 10000)
console.log(`Agent exited quickly (${this._quickExits}/3). Retrying in ${delay / 1000}s...`)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quick-exit threshold logic is inconsistent. The condition this._quickExits > 3 stops restarts, but the log message says (${this._quickExits}/3) and the error message says "3+ times", implying the cap is 3. In practice it allows 4 quick exits before stopping (increments to 4, then 4 > 3 triggers the stop). The condition should be >= 3 to match the "3+ times" message and /3 log indicator, or the messages should be updated to reflect the actual threshold of 4.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +90
const ACTIVE_PROFILES = ['cloud-persistent', 'local-research', 'claude-explorer'];

// ── Admin Authorization ──────────────────────────────────────
// Comma-separated Discord user IDs allowed to run destructive commands.
// If empty, only users with the admin role (default "admin") are allowed.
const ADMIN_USER_IDS = (process.env.DISCORD_ADMIN_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
const DISCORD_ADMIN_ROLE = (process.env.DISCORD_ADMIN_ROLE || 'admin').toLowerCase();
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY || '';

if (ADMIN_USER_IDS.length === 0) {
console.warn('[Auth] DISCORD_ADMIN_IDS is empty — only users with the admin role can run destructive commands.');
}

function isAdmin(userId, member) {
// Check explicit user ID list
if (ADMIN_USER_IDS.includes(userId)) return true;
// Check Discord server role (guild messages only)
if (member?.roles?.cache?.some(r => r.name.toLowerCase() === DISCORD_ADMIN_ROLE)) return true;
return false;
}

// ── Profile Name Validation ──────────────────────────────────
// Only allow alphanumeric, underscore, and hyphen characters to prevent
// path traversal attacks when profile names are used in file paths.
function isValidProfileName(name) {
return typeof name === 'string' && /^[a-zA-Z0-9_-]+$/.test(name) && name.length <= 64;
}

function safeProfilePath(name) {
if (!isValidProfileName(name)) {
throw new Error(`Invalid profile name: "${name}"`);
}
const filePath = join(PROFILES_DIR, `${name}.json`);
const resolvedDir = resolve(PROFILES_DIR);
const resolvedFile = resolve(filePath);
if (!resolvedFile.startsWith(resolvedDir + sep)) {
throw new Error(`Path traversal detected for profile: "${name}"`);
}
return filePath;
}

// ── Bot Groups (name → agent names) ─────────────────────────
// Reads agent names from profile JSON files at startup so the map
// stays in sync with whatever profiles are configured.
function loadProfileAgentMap() {
const map = {};
for (const profileName of ACTIVE_PROFILES) {
try {
const filePath = safeProfilePath(profileName);
const profile = deepSanitize(JSON.parse(readFileSync(filePath, 'utf8')));
if (profile.name) map[profileName] = profile.name;
} catch { /* profile may not exist yet */ }
}
return map;
}
const PROFILE_AGENT_MAP = loadProfileAgentMap();
const allAgentNames = Object.values(PROFILE_AGENT_MAP);
const BOT_GROUPS = {
all: allAgentNames.length > 0 ? allAgentNames : ['CloudGrok', 'LocalAndy'],
cloud: allAgentNames.filter(n => PROFILE_AGENT_MAP['cloud-persistent'] === n),
local: allAgentNames.filter(n => PROFILE_AGENT_MAP['local-research'] === n),
research: allAgentNames.length > 0 ? [...allAgentNames] : ['LocalAndy', 'CloudGrok'],
};
console.log('[Boot] Profile→Agent map:', JSON.stringify(PROFILE_AGENT_MAP));

// ── Aliases (shorthand → canonical agent name) ──────────────
// Build aliases dynamically from discovered names, with static fallbacks
const cloudAgent = PROFILE_AGENT_MAP['cloud-persistent'] || 'CloudGrok';
const localAgent = PROFILE_AGENT_MAP['local-research'] || 'LocalAndy';
const AGENT_ALIASES = {
'cloud': cloudAgent,
'cg': cloudAgent,
'grok': cloudAgent,
'local': localAgent,
'la': localAgent,
'andy': localAgent,
};
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ACTIVE_PROFILES constant on line 14 is hardcoded to ['cloud-persistent', 'local-research', 'claude-explorer'], and the AGENT_ALIASES map on lines 83-90 contains hardcoded aliases (CloudGrok, LocalAndy). These names are project-specific and must be changed manually for any other deployment. This makes the bot hard to reuse without code changes. These values should be configured via environment variables or a configuration file, similar to how MINDSERVER_HOST and DISCORD_BOT_TOKEN are already loaded from the environment.

Copilot uses AI. Check for mistakes.
Comment on lines +1168 to +1177
// ── Direct chat with Mindcraft Bot Manager (DM or "bot: <message>" prefix) ──
const botPrefixMatch = content.match(/^bot:\s*([\s\S]+)/i);
if (isDM || botPrefixMatch) {
const question = (botPrefixMatch ? botPrefixMatch[1] : content).trim();
if (!question) return;
await message.channel.sendTyping();
const reply = await handleDirectChat(question);
const chunks = reply.match(/[\s\S]{1,1990}/g) || [reply];
for (const chunk of chunks) await message.reply(chunk);
return;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleDirectChat function is accessible to any user who DMs the bot or uses the bot: prefix, without any admin check. This exposes live agent state data (health, position, etc.) and recent in-game chat to any Discord user who can DM the bot. Consider adding an authorization check (similar to isAdmin()) for the direct chat feature, especially since the system prompt says "Help the admin monitor and manage their bots."

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,1258 @@

import { Client, GatewayIntentBits, Partials } from 'discord.js';
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discord.js is used throughout this file but is not listed as a dependency in package.json. Users will get a module-not-found error when trying to run the bot unless they manually install it. The dependency needs to be added to package.json.

Copilot uses AI. Check for mistakes.

function readUsageFromDisk(agentName) {
try {
const filePath = path.join(__dirname, `../../bots/${agentName}/usage.json`);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readUsageFromDisk function constructs a file path using agentName without sanitizing it. Since agentName originates from agent_connections keys, which can be set by the register-remote-agent socket event (with only a name check for truthiness), a malicious agent name like ../../etc/passwd could cause path traversal and read arbitrary files. The agentName should be sanitized with path.basename() or a regex allow-list before constructing the path.

Suggested change
const filePath = path.join(__dirname, `../../bots/${agentName}/usage.json`);
const safeAgentName = typeof agentName === 'string' ? path.basename(agentName) : '';
if (!safeAgentName) return null;
const filePath = path.join(__dirname, '..', '..', 'bots', safeAgentName, 'usage.json');

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants