#!/usr/bin/env node import { Command } from 'commander'; import { SandboxManager } from './index.js'; import { spawn } from 'child_process'; import { logForDebugging } from './utils/debug.js'; import { loadConfig, loadConfigFromString } from './utils/config-loader.js'; import * as readline from 'readline'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; /** * Get default config path */ function getDefaultConfigPath() { return path.join(os.homedir(), '.srt-settings.json'); } /** * Create a minimal default config if no config file exists */ function getDefaultConfig() { return { network: { allowedDomains: [], deniedDomains: [], }, filesystem: { denyRead: [], allowRead: [], allowWrite: [], denyWrite: [], }, }; } async function main() { const program = new Command(); program .name('srt') .description('Run commands in a sandbox with network and filesystem restrictions') .version(process.env.npm_package_version || '1.0.0'); // Default command - run command in sandbox program .argument('[command...]', 'command to run in the sandbox') .option('-d, --debug', 'enable debug logging') .option('-s, --settings ', 'path to config file (default: ~/.srt-settings.json)') .option('-c ', 'run command string directly (like sh -c), no escaping applied') .option('--control-fd ', 'read config updates from file descriptor (JSON lines protocol)', parseInt) .allowUnknownOption() .action(async (commandArgs, options) => { try { // Enable debug logging if requested if (options.debug) { process.env.DEBUG = 'true'; } // Load config from file const configPath = options.settings || getDefaultConfigPath(); let runtimeConfig = loadConfig(configPath); if (!runtimeConfig) { logForDebugging(`No config found at ${configPath}, using default config`); runtimeConfig = getDefaultConfig(); } // Initialize sandbox with config logForDebugging('Initializing sandbox...'); await SandboxManager.initialize(runtimeConfig); // Set up control fd for dynamic config updates if specified let controlReader = null; if (options.controlFd !== undefined) { try { const controlStream = fs.createReadStream('', { fd: options.controlFd, }); controlReader = readline.createInterface({ input: controlStream, crlfDelay: Infinity, }); controlReader.on('line', line => { const newConfig = loadConfigFromString(line); if (newConfig) { logForDebugging(`Config updated from control fd: ${JSON.stringify(newConfig)}`); SandboxManager.updateConfig(newConfig); } else if (line.trim()) { // Only log non-empty lines that failed to parse logForDebugging(`Invalid config on control fd (ignored): ${line}`); } }); controlReader.on('error', err => { logForDebugging(`Control fd error: ${err.message}`); }); logForDebugging(`Listening for config updates on fd ${options.controlFd}`); } catch (err) { logForDebugging(`Failed to open control fd ${options.controlFd}: ${err instanceof Error ? err.message : String(err)}`); } } // Cleanup control reader on exit process.on('exit', () => { controlReader?.close(); }); // Determine command string based on mode let command; if (options.c) { // -c mode: use command string directly, no escaping command = options.c; logForDebugging(`Command string mode (-c): ${command}`); } else if (commandArgs.length > 0) { // Default mode: simple join command = commandArgs.join(' '); logForDebugging(`Original command: ${command}`); } else { console.error('Error: No command specified. Use -c or provide command arguments.'); process.exit(1); } logForDebugging(JSON.stringify(SandboxManager.getNetworkRestrictionConfig(), null, 2)); // Wrap the command with sandbox restrictions const sandboxedCommand = await SandboxManager.wrapWithSandbox(command); // Execute the sandboxed command const child = spawn(sandboxedCommand, { shell: true, stdio: 'inherit', }); // Handle process exit child.on('exit', (code, signal) => { // Clean up bwrap mount point artifacts before exiting. // On Linux, bwrap creates empty files on the host when protecting // non-existent deny paths. This removes them. SandboxManager.cleanupAfterCommand(); if (signal) { if (signal === 'SIGINT' || signal === 'SIGTERM') { process.exit(0); } else { console.error(`Process killed by signal: ${signal}`); process.exit(1); } } process.exit(code ?? 0); }); child.on('error', error => { console.error(`Failed to execute command: ${error.message}`); process.exit(1); }); // Handle cleanup on interrupt process.on('SIGINT', () => { child.kill('SIGINT'); }); process.on('SIGTERM', () => { child.kill('SIGTERM'); }); } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); program.parse(); } main().catch(error => { console.error('Fatal error:', error); process.exit(1); }); //# sourceMappingURL=cli.js.map