| /** | ||
| * Code Executor Module | ||
| * Safely executes generated code with timeout and cleanup | ||
| */ | ||
| import type { ExecuteResult } from './types.js'; | ||
| /** | ||
| * Infer language from file extension | ||
| */ | ||
| export declare function inferLanguageFromExtension(filename: string): string | null; | ||
| /** | ||
| * Execute code and return result | ||
| */ | ||
| export declare function executeCode(code: string, language: string, timeout?: number): Promise<ExecuteResult>; | ||
| /** | ||
| * Save blocked code to temp directory for inspection | ||
| */ | ||
| export declare function saveBlockedCode(code: string, language: string): Promise<string>; |
| /** | ||
| * Code Executor Module | ||
| * Safely executes generated code with timeout and cleanup | ||
| */ | ||
| import { execFile } from 'child_process'; | ||
| import { writeFile, unlink, mkdir } from 'fs/promises'; | ||
| import { tmpdir } from 'os'; | ||
| import { join } from 'path'; | ||
| import { promisify } from 'util'; | ||
| const execFileAsync = promisify(execFile); | ||
| /** | ||
| * Default execution timeout in milliseconds (30 seconds) | ||
| */ | ||
| const DEFAULT_TIMEOUT = 30000; | ||
| /** | ||
| * Language to interpreter mapping | ||
| */ | ||
| const INTERPRETERS = { | ||
| python: ['python3', 'python'], | ||
| py: ['python3', 'python'], | ||
| node: ['node'], | ||
| javascript: ['node'], | ||
| js: ['node'], | ||
| bash: ['bash'], | ||
| sh: ['sh'], | ||
| }; | ||
| /** | ||
| * File extensions for each language | ||
| */ | ||
| const EXTENSIONS = { | ||
| python: '.py', | ||
| py: '.py', | ||
| node: '.js', | ||
| javascript: '.js', | ||
| js: '.js', | ||
| bash: '.sh', | ||
| sh: '.sh', | ||
| }; | ||
| /** | ||
| * Get GLM temp directory path | ||
| */ | ||
| function getGlmTempDir() { | ||
| return join(tmpdir(), 'glm-coding'); | ||
| } | ||
| /** | ||
| * Generate timestamp string for filename | ||
| */ | ||
| function getTimestamp() { | ||
| const now = new Date(); | ||
| const pad = (n) => n.toString().padStart(2, '0'); | ||
| return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; | ||
| } | ||
| /** | ||
| * Ensure GLM temp directory exists | ||
| */ | ||
| async function ensureTempDir() { | ||
| const dir = getGlmTempDir(); | ||
| await mkdir(dir, { recursive: true }); | ||
| return dir; | ||
| } | ||
| /** | ||
| * Find available interpreter for language | ||
| */ | ||
| async function findInterpreter(language) { | ||
| const interpreters = INTERPRETERS[language.toLowerCase()]; | ||
| if (!interpreters) { | ||
| return null; | ||
| } | ||
| for (const interpreter of interpreters) { | ||
| try { | ||
| await execFileAsync('which', [interpreter]); | ||
| return interpreter; | ||
| } | ||
| catch { | ||
| // Interpreter not found, try next | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Infer language from file extension | ||
| */ | ||
| export function inferLanguageFromExtension(filename) { | ||
| const ext = filename.split('.').pop()?.toLowerCase(); | ||
| if (!ext) | ||
| return null; | ||
| const extToLang = { | ||
| py: 'python', | ||
| js: 'node', | ||
| sh: 'bash', | ||
| }; | ||
| return extToLang[ext] || null; | ||
| } | ||
| /** | ||
| * Execute code and return result | ||
| */ | ||
| export async function executeCode(code, language, timeout = DEFAULT_TIMEOUT) { | ||
| const interpreter = await findInterpreter(language); | ||
| if (!interpreter) { | ||
| return { | ||
| exitCode: 1, | ||
| stdout: '', | ||
| stderr: `No interpreter found for language: ${language}`, | ||
| }; | ||
| } | ||
| const ext = EXTENSIONS[language.toLowerCase()] || '.txt'; | ||
| const tempDir = await ensureTempDir(); | ||
| const tempFile = join(tempDir, `glm_temp_${getTimestamp()}${ext}`); | ||
| try { | ||
| // Write code to temp file | ||
| await writeFile(tempFile, code, 'utf-8'); | ||
| // Execute with timeout | ||
| const { stdout, stderr } = await execFileAsync(interpreter, [tempFile], { | ||
| timeout, | ||
| maxBuffer: 10 * 1024 * 1024, // 10MB | ||
| }); | ||
| return { | ||
| exitCode: 0, | ||
| stdout, | ||
| stderr, | ||
| }; | ||
| } | ||
| catch (error) { | ||
| const err = error; | ||
| if (err.killed || err.code === 'ETIMEDOUT') { | ||
| return { | ||
| exitCode: 124, | ||
| stdout: err.stdout || '', | ||
| stderr: `Execution timed out after ${timeout / 1000} seconds`, | ||
| }; | ||
| } | ||
| return { | ||
| exitCode: 1, | ||
| stdout: err.stdout || '', | ||
| stderr: err.stderr || String(error), | ||
| }; | ||
| } | ||
| finally { | ||
| // Cleanup temp file | ||
| try { | ||
| await unlink(tempFile); | ||
| } | ||
| catch { | ||
| // Ignore cleanup errors | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Save blocked code to temp directory for inspection | ||
| */ | ||
| export async function saveBlockedCode(code, language) { | ||
| const ext = EXTENSIONS[language?.toLowerCase()] || '.txt'; | ||
| const tempDir = await ensureTempDir(); | ||
| const filename = `glm_blocked_${getTimestamp()}${ext}`; | ||
| const filepath = join(tempDir, filename); | ||
| await writeFile(filepath, code, 'utf-8'); | ||
| return filepath; | ||
| } |
| /** | ||
| * Security Check Module | ||
| * Validates generated code for dangerous patterns before execution | ||
| */ | ||
| import type { SecurityCheckResult } from './types.js'; | ||
| /** | ||
| * Check code for security violations | ||
| */ | ||
| export declare function checkCodeSecurity(code: string): SecurityCheckResult; | ||
| /** | ||
| * Format security check result for console output | ||
| */ | ||
| export declare function formatSecurityReport(result: SecurityCheckResult): string; |
| /** | ||
| * Security Check Module | ||
| * Validates generated code for dangerous patterns before execution | ||
| */ | ||
| /** | ||
| * Build dangerous patterns dynamically to avoid static detection | ||
| */ | ||
| function buildDangerousPatterns() { | ||
| const patterns = []; | ||
| // System command execution patterns | ||
| const sysExecMethods = ['system', 'popen']; | ||
| for (const method of sysExecMethods) { | ||
| patterns.push({ | ||
| pattern: `os.${method}(`, | ||
| reason: 'System command execution', | ||
| }); | ||
| } | ||
| // Subprocess patterns | ||
| const subprocessMethods = ['run', 'call', 'Popen']; | ||
| for (const method of subprocessMethods) { | ||
| patterns.push({ | ||
| pattern: `subprocess.${method}(`, | ||
| reason: 'Subprocess execution', | ||
| }); | ||
| } | ||
| // File deletion patterns | ||
| const osDeleteMethods = ['remove', 'unlink', 'rmdir', 'removedirs']; | ||
| for (const method of osDeleteMethods) { | ||
| patterns.push({ | ||
| pattern: `os.${method}(`, | ||
| reason: 'File/directory deletion', | ||
| }); | ||
| } | ||
| patterns.push({ | ||
| pattern: 'shutil.rmtree(', | ||
| reason: 'Directory deletion', | ||
| }); | ||
| // Dynamic import | ||
| patterns.push({ | ||
| pattern: '__import__(', | ||
| reason: 'Dynamic module import', | ||
| }); | ||
| // Environment manipulation | ||
| patterns.push({ | ||
| pattern: 'os.environ[', | ||
| reason: 'Environment variable access', | ||
| }); | ||
| patterns.push({ | ||
| pattern: 'os.putenv(', | ||
| reason: 'Environment variable modification', | ||
| }); | ||
| return patterns; | ||
| } | ||
| /** | ||
| * Dynamic execution function names to detect | ||
| */ | ||
| const DYNAMIC_EXEC_FUNCTIONS = ['eval', 'exec']; | ||
| /** | ||
| * Dangerous module imports to detect | ||
| */ | ||
| const DANGEROUS_IMPORTS = [ | ||
| 'os', | ||
| 'subprocess', | ||
| 'shutil', | ||
| 'sys', | ||
| 'commands', | ||
| 'pty', | ||
| 'ctypes', | ||
| ]; | ||
| /** | ||
| * Check if a line is an import statement for a dangerous module | ||
| */ | ||
| function checkDangerousImport(line) { | ||
| const trimmed = line.trim(); | ||
| for (const module of DANGEROUS_IMPORTS) { | ||
| const importPatterns = [ | ||
| new RegExp(`^import\\s+${module}\\b`), | ||
| new RegExp(`^from\\s+${module}\\b`), | ||
| new RegExp(`^import\\s+\\w+,\\s*${module}\\b`), | ||
| ]; | ||
| for (const pattern of importPatterns) { | ||
| if (pattern.test(trimmed)) { | ||
| return module; | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Check for dynamic execution functions | ||
| */ | ||
| function checkDynamicExecution(line) { | ||
| for (const funcName of DYNAMIC_EXEC_FUNCTIONS) { | ||
| const pattern = new RegExp(`\\b${funcName}\\s*\\(`); | ||
| if (pattern.test(line)) { | ||
| return funcName; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Check code for security violations | ||
| */ | ||
| export function checkCodeSecurity(code) { | ||
| const violations = []; | ||
| const lines = code.split('\n'); | ||
| const dangerousPatterns = buildDangerousPatterns(); | ||
| lines.forEach((line, index) => { | ||
| const lineNum = index + 1; | ||
| const trimmedLine = line.trim(); | ||
| // Skip empty lines and comments | ||
| if (!trimmedLine || trimmedLine.startsWith('#')) { | ||
| return; | ||
| } | ||
| // Check for dangerous imports | ||
| const dangerousModule = checkDangerousImport(line); | ||
| if (dangerousModule) { | ||
| violations.push({ | ||
| pattern: `import ${dangerousModule}`, | ||
| reason: `Dangerous module import (${dangerousModule})`, | ||
| line: lineNum, | ||
| code: trimmedLine, | ||
| }); | ||
| } | ||
| // Check for dangerous patterns | ||
| for (const { pattern, reason } of dangerousPatterns) { | ||
| if (line.includes(pattern)) { | ||
| violations.push({ | ||
| pattern, | ||
| reason, | ||
| line: lineNum, | ||
| code: trimmedLine, | ||
| }); | ||
| } | ||
| } | ||
| // Check for dynamic execution functions | ||
| const dynamicFunc = checkDynamicExecution(line); | ||
| if (dynamicFunc) { | ||
| violations.push({ | ||
| pattern: `${dynamicFunc}()`, | ||
| reason: 'Dynamic code execution', | ||
| line: lineNum, | ||
| code: trimmedLine, | ||
| }); | ||
| } | ||
| }); | ||
| return { | ||
| safe: violations.length === 0, | ||
| violations, | ||
| }; | ||
| } | ||
| /** | ||
| * Format security check result for console output | ||
| */ | ||
| export function formatSecurityReport(result) { | ||
| if (result.safe) { | ||
| return '✓ Security check passed'; | ||
| } | ||
| const lines = [ | ||
| '⚠️ Security check failed - execution blocked', | ||
| '', | ||
| 'Violations found:', | ||
| ]; | ||
| for (const v of result.violations) { | ||
| lines.push(` Line ${v.line}: ${v.code}`); | ||
| lines.push(` └─ ${v.reason}`); | ||
| lines.push(''); | ||
| } | ||
| return lines.join('\n'); | ||
| } |
@@ -7,3 +7,6 @@ /** | ||
| import { writeFile } from 'fs/promises'; | ||
| import { resolve } from 'path'; | ||
| import { loadConfig, debug, GlmClient, buildSystemPrompt, logUsage, logError, getErrorMessage, } from '../core/index.js'; | ||
| import { checkCodeSecurity, formatSecurityReport } from '../core/securityCheck.js'; | ||
| import { executeCode, saveBlockedCode, inferLanguageFromExtension } from '../core/executor.js'; | ||
| /** | ||
@@ -56,2 +59,14 @@ * Strip markdown code fences from output | ||
| } | ||
| else if (arg === '-l' || arg === '--language') { | ||
| options.language = args[++i]; | ||
| } | ||
| else if (arg === '-x' || arg === '--exec') { | ||
| options.execute = true; | ||
| } | ||
| else if (arg === '--force') { | ||
| options.force = true; | ||
| } | ||
| else if (arg === '--dry-run') { | ||
| options.dryRun = true; | ||
| } | ||
| else if (arg === '--no-quality') { | ||
@@ -86,11 +101,29 @@ options.noQuality = true; | ||
| const systemPrompt = buildSystemPrompt(options.profile, options.noQuality); | ||
| // Add execution mode instructions to prompt | ||
| let finalPrompt = prompt; | ||
| if (options.execute) { | ||
| const execInstructions = `[EXECUTION MODE] | ||
| - Output actual values directly using print() statements | ||
| - Do NOT create generators, factories, or random content generators | ||
| - The code will be executed immediately, so produce concrete output | ||
| - If asked to create content (poem, story, data), embed the actual content in the code | ||
| - No user input, no interactive prompts, no external dependencies | ||
| Task: `; | ||
| finalPrompt = execInstructions + prompt; | ||
| } | ||
| const messages = [ | ||
| { role: 'system', content: systemPrompt }, | ||
| { role: 'user', content: prompt }, | ||
| { role: 'user', content: finalPrompt }, | ||
| ]; | ||
| // Stream response to stdout | ||
| // Stream response (to stdout only if not saving to file and not executing) | ||
| let fullContent = ''; | ||
| const suppressOutput = !!options.output || !!options.execute; | ||
| // Lower temperature for execution mode (more deterministic) | ||
| const temperature = options.execute ? 0.3 : undefined; | ||
| try { | ||
| for await (const chunk of client.generateCompletionStream({ messages })) { | ||
| process.stdout.write(chunk); | ||
| for await (const chunk of client.generateCompletionStream({ messages, temperature })) { | ||
| if (!suppressOutput) { | ||
| process.stdout.write(chunk); | ||
| } | ||
| fullContent += chunk; | ||
@@ -112,4 +145,4 @@ } | ||
| } | ||
| // Ensure newline at end | ||
| if (!fullContent.endsWith('\n')) { | ||
| // Ensure newline at end (only when outputting to stdout) | ||
| if (!suppressOutput && !fullContent.endsWith('\n')) { | ||
| process.stdout.write('\n'); | ||
@@ -121,10 +154,67 @@ } | ||
| if (options.output) { | ||
| const absolutePath = resolve(options.output); | ||
| try { | ||
| await writeFile(options.output, cleanCode + '\n', 'utf-8'); | ||
| console.error(`\nSaved to: ${options.output}`); | ||
| await writeFile(absolutePath, cleanCode + '\n', 'utf-8'); | ||
| console.log(absolutePath); | ||
| } | ||
| catch (error) { | ||
| console.error(`\nFailed to save file: ${getErrorMessage(error)}`); | ||
| console.error(`Failed to save file: ${getErrorMessage(error)}`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| // Execute code if -x flag is set | ||
| if (options.execute || options.dryRun) { | ||
| // Determine language (default: python) | ||
| let language = options.language; | ||
| if (!language && options.output) { | ||
| language = inferLanguageFromExtension(options.output) || undefined; | ||
| } | ||
| if (!language) { | ||
| language = 'python'; // Default to python | ||
| } | ||
| // Security check | ||
| const securityResult = checkCodeSecurity(cleanCode); | ||
| if (!securityResult.safe && !options.force) { | ||
| // Security check failed | ||
| console.error(formatSecurityReport(securityResult)); | ||
| // Save blocked code for inspection | ||
| const blockedPath = await saveBlockedCode(cleanCode, language); | ||
| console.error(`Code saved to: ${blockedPath}`); | ||
| console.error('Use --force to bypass security check (not recommended)'); | ||
| process.exit(1); | ||
| } | ||
| if (options.force && !securityResult.safe) { | ||
| console.error('⚠️ WARNING: Bypassing security check with --force'); | ||
| console.error(''); | ||
| } | ||
| // Dry run - just show security result | ||
| if (options.dryRun) { | ||
| if (securityResult.safe) { | ||
| console.log('✓ Security check passed (dry-run, not executed)'); | ||
| } | ||
| // Exit without executing | ||
| } | ||
| else { | ||
| // Actually execute the code | ||
| if (options.output) { | ||
| console.log('---'); | ||
| } | ||
| const result = await executeCode(cleanCode, language); | ||
| if (result.stdout) { | ||
| process.stdout.write(result.stdout); | ||
| if (!result.stdout.endsWith('\n')) { | ||
| process.stdout.write('\n'); | ||
| } | ||
| } | ||
| if (result.stderr) { | ||
| process.stderr.write(result.stderr); | ||
| if (!result.stderr.endsWith('\n')) { | ||
| process.stderr.write('\n'); | ||
| } | ||
| } | ||
| if (result.exitCode !== 0) { | ||
| process.exit(result.exitCode); | ||
| } | ||
| } | ||
| } | ||
| // Log usage (streaming doesn't return token counts) | ||
@@ -131,0 +221,0 @@ await logUsage({ |
@@ -18,2 +18,6 @@ /** | ||
| -p, --profile <name> Use specific profile (frontend-design, api-integration, etc) | ||
| -l, --language <lang> Language for execution (python, node, bash) | ||
| -x, --exec Execute generated code after security check | ||
| --force Bypass security check (not recommended) | ||
| --dry-run Check security without executing | ||
| --no-quality Disable quality instructions | ||
@@ -30,2 +34,4 @@ | ||
| glm --query "Validate email function" --output utils.py | ||
| glm --query "Print 1 to 10 sum" -l python -x | ||
| glm --query "Hello world" -o hello.py -x | ||
| echo "Parse JSON with error handling" | glm | ||
@@ -32,0 +38,0 @@ |
+28
-0
@@ -97,4 +97,32 @@ /** | ||
| noQuality?: boolean; | ||
| execute?: boolean; | ||
| language?: string; | ||
| force?: boolean; | ||
| dryRun?: boolean; | ||
| } | ||
| /** | ||
| * Security violation found in generated code | ||
| */ | ||
| export interface SecurityViolation { | ||
| pattern: string; | ||
| reason: string; | ||
| line: number; | ||
| code: string; | ||
| } | ||
| /** | ||
| * Result of security check on generated code | ||
| */ | ||
| export interface SecurityCheckResult { | ||
| safe: boolean; | ||
| violations: SecurityViolation[]; | ||
| } | ||
| /** | ||
| * Result of code execution | ||
| */ | ||
| export interface ExecuteResult { | ||
| exitCode: number; | ||
| stdout: string; | ||
| stderr: string; | ||
| } | ||
| /** | ||
| * Task types that profiles can handle | ||
@@ -101,0 +129,0 @@ */ |
+1
-1
| { | ||
| "name": "glm-coding", | ||
| "version": "0.5.0", | ||
| "version": "0.6.0", | ||
| "description": "GLM CLI - AI Code Generator with streaming output", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+41
-1
@@ -100,2 +100,38 @@ # GLM CLI - AI Code Generator | ||
| ### Execute Mode (NEW in v0.6.0) | ||
| Generate and immediately execute code with automatic security validation: | ||
| ```bash | ||
| # Execute generated code (default: Python) | ||
| glm -x -q "Print sum of 1 to 100" | ||
| # Output: 5050 | ||
| # Generate content directly (poem, data, etc.) | ||
| glm -x -q "Write a haiku about coding" | ||
| # Output: [actual haiku printed] | ||
| # Save and execute | ||
| glm -x -q "Hello world" -o hello.py | ||
| # Output: /path/to/hello.py | ||
| # Hello, World! | ||
| # Specify language | ||
| glm -x -l node -q "console.log('Hello')" | ||
| # Security check only (dry-run) | ||
| glm --dry-run -q "System information script" | ||
| # Force execution (bypass security - not recommended) | ||
| glm -x --force -q "some dangerous code" | ||
| ``` | ||
| **Security Features:** | ||
| - Automatic validation before execution | ||
| - Blocks dangerous imports: os, subprocess, shutil, etc. | ||
| - Blocks dangerous operations: file deletion, system commands | ||
| - Blocks dynamic code execution functions | ||
| - Detailed violation reports with line numbers | ||
| - Blocked code saved to temp directory for inspection | ||
| ### Available Profiles | ||
@@ -115,4 +151,8 @@ | ||
| -q, --query <prompt> Query prompt (required if no pipe) | ||
| -o, --output <file> Save output to file | ||
| -o, --output <file> Save output to file (suppresses code output) | ||
| -p, --profile <name> Use specific profile | ||
| -l, --language <lang> Language for execution (python, node, bash) | ||
| -x, --exec Execute generated code after security check | ||
| --force Bypass security check (not recommended) | ||
| --dry-run Security check only, no execution | ||
| --no-quality Disable quality instructions | ||
@@ -119,0 +159,0 @@ ``` |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
140004
13.05%54
8%3019
18.95%518
8.37%23
4.55%2
100%