Skip to main content
Glama
systempromptio

SystemPrompt Coding Agent

Official
host-bridge-daemon.ts13 kB
#!/usr/bin/env node /** * Host Bridge Daemon - TypeScript Version * Bridges Docker containers to host CLI tools (Claude, etc.) * Provides secure execution of host commands from isolated containers */ import * as net from 'net'; import { spawn, ChildProcess } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; // Get __dirname equivalent in ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Types interface DaemonConfig { tools: { claude?: string; [key: string]: string | undefined; }; shellPath: string; port: number; logFile: string; pidFile: string; } interface BridgeMessage { tool: 'claude' | string; command: string; workingDirectory?: string; env?: Record<string, string>; } interface BridgeResponse { type: 'stream' | 'error' | 'complete' | 'pid'; data?: string; code?: number; exitCode?: number; pid?: number; } // Environment validation class EnvironmentValidator { static validate(): DaemonConfig { const errors: string[] = []; const tools: DaemonConfig['tools'] = {}; // Check for available tools const claudePath = process.env.CLAUDE_PATH || ''; if (claudePath && fs.existsSync(claudePath)) { tools.claude = claudePath; } // Check shell path const shellPath = process.env.SHELL_PATH || '/bin/bash'; if (!fs.existsSync(shellPath)) { errors.push(`Shell not found at: ${shellPath}`); } // Check port - support both env var names for compatibility const port = parseInt(process.env.CLAUDE_PROXY_PORT || process.env.HOST_BRIDGE_PORT || '9876', 10); if (isNaN(port) || port < 1 || port > 65535) { errors.push('Invalid port: must be between 1 and 65535'); } // Setup paths - use daemon/logs directory const logsDir = path.join(__dirname, '..', 'logs'); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } const config: DaemonConfig = { tools, shellPath, port, logFile: path.join(logsDir, 'host-bridge.log'), pidFile: path.join(logsDir, 'daemon.pid') }; // Log validation results console.log('[Host Bridge Daemon] Environment validation:'); console.log(`- Available tools: ${Object.keys(tools).join(', ') || 'none'}`); console.log(`- Shell: ${shellPath}`); console.log(`- Port: ${port}`); if (Object.keys(tools).length === 0) { console.warn('\n[Host Bridge Daemon] Warning: No tools available'); console.warn('Set CLAUDE_PATH to enable tool execution'); } if (errors.length > 0) { console.error('\n[Host Bridge Daemon] Validation errors:'); errors.forEach(err => console.error(` - ${err}`)); } return config; } } // Logger class class Logger { private logStream: fs.WriteStream; constructor(logFile: string) { this.logStream = fs.createWriteStream(logFile, { flags: 'a' }); } log(message: string): void { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}`; console.log(message); this.logStream.write(logMessage + '\n'); } error(message: string, error?: Error): void { this.log(`ERROR: ${message}`); if (error) { this.log(`Stack: ${error.stack}`); } } close(): void { this.logStream.end(); } } // Main daemon class class HostBridgeDaemon { private config: DaemonConfig; private logger: Logger; private server: net.Server | null = null; private activeProcesses: Map<string, ChildProcess> = new Map(); constructor(config: DaemonConfig) { this.config = config; this.logger = new Logger(config.logFile); } start(): void { this.logger.log('[Host Bridge Daemon] Starting with configuration:'); this.logger.log(`- Available tools: ${Object.keys(this.config.tools).join(', ')}`); this.logger.log(`- Shell: ${this.config.shellPath}`); this.logger.log(`- Port: ${this.config.port}`); this.logger.log(`- Working directory: ${process.cwd()}`); this.logger.log(`- Node version: ${process.version}`); this.server = net.createServer((socket) => this.handleConnection(socket)); this.server.on('error', (err) => { this.logger.error('Server error', err); process.exit(1); }); this.server.listen(this.config.port, '0.0.0.0', () => { this.logger.log(`[Host Bridge Daemon] Server listening on port ${this.config.port}`); // Write PID file fs.writeFileSync(this.config.pidFile, process.pid.toString()); // Handle graceful shutdown process.on('SIGTERM', () => this.shutdown()); process.on('SIGINT', () => this.shutdown()); }); } private handleConnection(socket: net.Socket): void { const connectionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; this.logger.log(`[Host Bridge Daemon] Client connected (${connectionId})`); let buffer = ''; let currentProcess: ChildProcess | null = null; // Keep socket alive socket.setKeepAlive(true, 1000); socket.setTimeout(0); // No timeout socket.on('data', (data) => { buffer += data.toString(); this.logger.log(`[Host Bridge Daemon] Received data: ${buffer.length} bytes`); // Check for complete JSON message try { const message: BridgeMessage = JSON.parse(buffer); buffer = ''; this.logger.log(`[Host Bridge Daemon] Parsed message: ${JSON.stringify(message)}`); // Kill any existing process for this connection if (currentProcess && !currentProcess.killed) { this.logger.log('[Host Bridge Daemon] Killing existing process'); currentProcess.kill('SIGTERM'); this.activeProcesses.delete(connectionId); } // Check if tool is available const toolPath = this.config.tools[message.tool]; if (!toolPath) { const errorResponse: BridgeResponse = { type: 'error', data: `Tool '${message.tool}' is not available. Available tools: ${Object.keys(this.config.tools).join(', ')}` }; socket.write(JSON.stringify(errorResponse) + '\n'); return; } // Execute command currentProcess = this.executeCommand(message, toolPath, socket, connectionId); this.activeProcesses.set(connectionId, currentProcess); } catch (e) { // Wait for more data if JSON is incomplete if (buffer.length > 10000) { this.logger.log('[Host Bridge Daemon] Buffer overflow, clearing'); const errorResponse: BridgeResponse = { type: 'error', data: 'Message too large' }; socket.write(JSON.stringify(errorResponse) + '\n'); buffer = ''; } } }); socket.on('end', () => { this.logger.log(`[Host Bridge Daemon] Client disconnected (${connectionId})`); this.cleanupConnection(connectionId); }); socket.on('close', () => { this.logger.log(`[Host Bridge Daemon] Client closed (${connectionId})`); this.cleanupConnection(connectionId); }); socket.on('error', (err) => { this.logger.error(`Socket error (${connectionId})`, err); this.cleanupConnection(connectionId); }); } private executeCommand( message: BridgeMessage, toolPath: string, socket: net.Socket, connectionId: string ): ChildProcess { // Build command args based on tool let args: string[] = []; let command: string; if (message.tool === 'claude') { args = [ '-p', '--output-format', 'json', '--dangerously-skip-permissions', '--max-turns', '5', message.command ]; command = `${toolPath} ${args.map(arg => `'${arg.replace(/'/g, "'\\''")}'`).join(' ')}`; } else if (message.tool === 'bash') { // For bash, execute the command directly command = message.command; this.logger.log(`[Host Bridge Daemon] Executing bash command: ${command}`); } else { // Generic command execution args = [message.command]; command = `${toolPath} ${args.map(arg => `'${arg.replace(/'/g, "'\\''")}'`).join(' ')}`; } this.logger.log(`[Host Bridge Daemon] Running ${message.tool} with command: ${command}`); const options = { cwd: message.workingDirectory || process.cwd(), env: message.env ? { ...process.env, ...message.env } : process.env, shell: this.config.shellPath, stdio: ['pipe', 'pipe', 'pipe'] as ['pipe', 'pipe', 'pipe'] }; const childProcess = spawn(command, [], options); this.logger.log(`[Host Bridge Daemon] Spawned ${message.tool} process with PID: ${childProcess.pid}`); // Send PID event if (childProcess.pid) { const pidResponse: BridgeResponse = { type: 'pid', pid: childProcess.pid }; if (socket.writable && !socket.destroyed) { socket.write(JSON.stringify(pidResponse) + '\n'); } } // Close stdin immediately since we're not sending any input if (childProcess.stdin) { this.logger.log('[Host Bridge Daemon] Closing stdin to signal no more input'); childProcess.stdin.end(); } // Handle stdout if (childProcess.stdout) { childProcess.stdout.on('data', (chunk: Buffer) => { const data = chunk.toString(); this.logger.log(`[Host Bridge Daemon] Streaming stdout chunk (${chunk.length} bytes)`); const streamResponse: BridgeResponse = { type: 'stream', data: data }; if (socket.writable && !socket.destroyed) { socket.write(JSON.stringify(streamResponse) + '\n'); this.logger.log('[Host Bridge Daemon] Data sent to client'); } else { this.logger.log('[Host Bridge Daemon] Socket not writable - data lost!'); } }); } // Handle stderr if (childProcess.stderr) { childProcess.stderr.on('data', (chunk: Buffer) => { const data = chunk.toString(); this.logger.log(`[Host Bridge Daemon] stderr: ${data}`); const errorResponse: BridgeResponse = { type: 'error', data: data }; if (socket.writable && !socket.destroyed) { socket.write(JSON.stringify(errorResponse) + '\n'); } }); } // Handle process close childProcess.on('close', (code: number | null) => { this.logger.log(`[Host Bridge Daemon] ${message.tool} process closed with code: ${code}`); const completeResponse: BridgeResponse = { type: 'complete', exitCode: code || 0 }; if (socket.writable && !socket.destroyed) { socket.write(JSON.stringify(completeResponse) + '\n'); } this.activeProcesses.delete(connectionId); }); // Handle process error childProcess.on('error', (err: Error) => { this.logger.error(`Process error (${connectionId})`, err); const errorResponse: BridgeResponse = { type: 'error', data: `Failed to start ${message.tool}: ${err.message}` }; if (socket.writable && !socket.destroyed) { socket.write(JSON.stringify(errorResponse) + '\n'); } this.activeProcesses.delete(connectionId); }); return childProcess; } private cleanupConnection(connectionId: string): void { const process = this.activeProcesses.get(connectionId); if (process && !process.killed) { this.logger.log(`[Host Bridge Daemon] Killing process for connection ${connectionId}`); process.kill('SIGTERM'); this.activeProcesses.delete(connectionId); } } private shutdown(): void { this.logger.log('[Host Bridge Daemon] Shutting down...'); // Kill all active processes this.activeProcesses.forEach((process, id) => { if (!process.killed) { this.logger.log(`[Host Bridge Daemon] Killing process ${id}`); process.kill('SIGTERM'); } }); // Close server if (this.server) { this.server.close(() => { this.logger.log('[Host Bridge Daemon] Server closed'); // Remove PID file if (fs.existsSync(this.config.pidFile)) { fs.unlinkSync(this.config.pidFile); } this.logger.close(); process.exit(0); }); } } } // Export config for Docker startup export function getValidatedConfig(): DaemonConfig { return EnvironmentValidator.validate(); } // Main entry point if (import.meta.url === `file://${process.argv[1]}`) { const config = EnvironmentValidator.validate(); const daemon = new HostBridgeDaemon(config); daemon.start(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systempromptio/systemprompt-code-orchestrator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server