/** * Restricted Docker CLI command execution. * Set PEARDOCK_UNRESTRICTED_CLI=1 to allow broader docker CLI (still admin-gated via roles). */ import { spawn } from 'child_process' import * as validation from '../utils/validation.js' import { Pushes } from '../../shared/protocol.js' import { Roles } from '../../shared/protocol.js' const UNRESTRICTED = process.env.PEARDOCK_UNRESTRICTED_CLI === '1' || process.env.PEARDOCK_UNRESTRICTED_CLI === 'true' const DANGEROUS_PATTERNS = ['exec', 'run', 'rm -f', 'prune', 'system prune', 'swarm', 'plugin'] export function registerDockerCliHandlers(session) { session.respond('dockerCommand', async (args) => { const commandStr = validation.sanitizeString(args.data || args.command, 500) if (!commandStr || !commandStr.startsWith('docker ')) { throw new Error('Invalid command format') } if (UNRESTRICTED) { if (session.role !== Roles.admin) { throw new Error('Unrestricted Docker CLI requires admin role') } } else if (DANGEROUS_PATTERNS.some((pattern) => commandStr.includes(pattern))) { throw new Error( 'Command not allowed for security reasons (set PEARDOCK_UNRESTRICTED_CLI=1 for admin full CLI)' ) } const parts = commandStr.split(' ') const executable = parts[0] const cmdArgs = parts.slice(1) if (executable !== 'docker') { throw new Error('Only docker commands are allowed') } const connectionId = args.connectionId return new Promise((resolve, reject) => { const child = spawn(executable, cmdArgs) let settled = false child.stdout.on('data', (data) => { session.push(Pushes.dockerOutput, { type: 'dockerOutput', connectionId, data: data.toString('base64'), encoding: 'base64', }) }) child.stderr.on('data', (data) => { session.push(Pushes.dockerOutput, { type: 'dockerOutput', connectionId, data: Buffer.from(`[ERROR] ${data.toString()}`).toString('base64'), encoding: 'base64', }) }) child.on('close', (code) => { session.push(Pushes.dockerOutput, { type: 'dockerOutput', connectionId, data: `[INFO] Command exited with code ${code}`, }) if (!settled) { settled = true resolve({ success: true, exitCode: code }) } }) child.on('error', (err) => { if (!settled) { settled = true reject(err) } }) }) }) session.respond('dockerTerminalResize', async () => { // No PTY for docker CLI yet — acknowledge for UI return { success: true } }) }