godcrm/backend/routes/v3/code.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

381 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Code Execution API Routes
* ADR-032: Code Execution Engine — Piston Integration
*
* Provides endpoints for executing code via Piston:
* - POST /execute - Execute code synchronously
* - POST /execute-loop - Execute with Agent callback
* - POST /validate - Validate syntax without execution
* - GET /languages - List supported languages
*
* Piston API: https://github.com/engineer-man/piston
*/
/**
* @swagger
* components:
* schemas:
* CodeExecutionRequest:
* type: object
* required: [source_code]
* properties:
* source_code:
* type: string
* language:
* type: string
* default: python
* stdin:
* type: string
* args:
* type: array
* items:
* type: string
* timeout:
* type: integer
* default: 3000
* CodeExecutionResult:
* type: object
* properties:
* stdout:
* type: string
* stderr:
* type: string
* exit_code:
* type: integer
*/
import { Router } from 'express';
import axios from 'axios';
import { apiLogger } from '../../utils/logger.js';
import { success, badRequest, error } from '../../utils/response.js';
const router = Router();
const PISTON_URL = process.env.PISTON_URL || 'http://localhost:2000';
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:5001';
/**
* Language aliases → Piston language/version
* Maps user-friendly names to Piston runtime specifications
*/
const LANGUAGE_ALIASES = {
python: { language: 'python', version: '3.10.0' },
python3: { language: 'python', version: '3.10.0' },
py: { language: 'python', version: '3.10.0' },
javascript: { language: 'javascript', version: '20.11.1' },
js: { language: 'javascript', version: '20.11.1' },
node: { language: 'javascript', version: '20.11.1' },
nodejs: { language: 'javascript', version: '20.11.1' },
typescript: { language: 'typescript', version: '5.0.3' },
ts: { language: 'typescript', version: '5.0.3' },
bash: { language: 'bash', version: '5.2.0' },
sh: { language: 'bash', version: '5.2.0' },
shell: { language: 'bash', version: '5.2.0' },
};
/**
* GET /api/v3/code/languages
* List supported programming languages from Piston
* @swagger
* /api/v3/code/languages:
* get:
* summary: List supported programming languages
* tags: [Code Execution]
* responses:
* 200:
* description: List of available languages
*/
router.get('/languages', async (req, res) => {
try {
// Get runtimes from Piston
const response = await axios.get(`${PISTON_URL}/api/v2/runtimes`);
return res.json(response.data);
} catch (err) {
apiLogger.debug({ err: err.message }, 'Piston unavailable, using local language list');
// Fallback to local aliases
const localList = Object.entries(LANGUAGE_ALIASES)
.filter(([alias], index, arr) => {
// Remove duplicate aliases, keep base names
const baseNames = ['python', 'javascript', 'typescript', 'bash'];
return baseNames.includes(alias);
})
.map(([alias, config]) => ({
language: config.language,
version: config.version,
aliases: [alias]
}));
return res.json(localList);
}
});
/**
* POST /api/v3/code/execute
* Execute code synchronously via Piston
* @swagger
* /api/v3/code/execute:
* post:
* summary: Execute code synchronously
* tags: [Code Execution]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CodeExecutionRequest'
* responses:
* 200:
* description: Execution result
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CodeExecutionResult'
*/
router.post('/execute', async (req, res) => {
try {
const {
source_code,
language = 'python',
stdin = '',
args = [],
timeout = 3000, // milliseconds (Piston max is 3000)
} = req.body;
// Ensure timeout doesn't exceed Piston limit
const safeTimeout = Math.min(timeout, 3000);
if (!source_code) {
return badRequest(res, 'source_code is required');
}
// Resolve language alias to Piston runtime
const langConfig = LANGUAGE_ALIASES[language.toLowerCase()] || {
language: language,
version: '*' // Latest available
};
apiLogger.debug({
language: langConfig.language,
version: langConfig.version,
codeLength: source_code.length
}, 'Executing code via Piston');
// Execute via Piston
const response = await axios.post(`${PISTON_URL}/api/v2/execute`, {
language: langConfig.language,
version: langConfig.version,
files: [{ content: source_code }],
stdin,
args,
run_timeout: safeTimeout,
compile_timeout: safeTimeout,
});
const result = response.data;
const run = result.run || {};
const isSuccess = run.code === 0;
const responseBody = {
success: isSuccess,
stdout: run.stdout || '',
stderr: run.stderr || '',
code: run.code,
signal: run.signal,
time: {
cpu_ms: run.cpu_time,
wall_ms: run.wall_time
},
memory_bytes: run.memory,
language: result.language,
version: result.version,
};
apiLogger.debug({
success: isSuccess,
exitCode: run.code,
cpu_time: run.cpu_time,
wall_time: run.wall_time
}, 'Code execution completed');
return res.json(responseBody);
} catch (err) {
apiLogger.error({ err: err.message }, 'Code execution failed');
return error(res, 'PISTON_ERROR', `Piston execution failed: ${err.message}`);
}
});
/**
* POST /api/v3/code/execute-loop
* Execute code and trigger Agent callback with result
* Used for Agent Loop pattern - AI Agent continues conversation based on result
*/
router.post('/execute-loop', async (req, res) => {
try {
const {
source_code,
language = 'python',
stdin = '',
args = [],
timeout = 3000, // Piston max is 3000ms
// Agent callback configuration
conversation_id,
agent_id,
next_prompt,
} = req.body;
// Ensure timeout doesn't exceed Piston limit
const safeTimeout = Math.min(timeout, 3000);
if (!source_code) {
return badRequest(res, 'source_code is required');
}
// Resolve language alias
const langConfig = LANGUAGE_ALIASES[language.toLowerCase()] || {
language: language,
version: '*'
};
apiLogger.debug({
language: langConfig.language,
conversation_id,
agent_id
}, 'Executing code with Agent callback');
// Execute via Piston (synchronous - Piston is fast)
const response = await axios.post(`${PISTON_URL}/api/v2/execute`, {
language: langConfig.language,
version: langConfig.version,
files: [{ content: source_code }],
stdin,
args,
run_timeout: safeTimeout,
});
const result = response.data;
const run = result.run || {};
const isSuccess = run.code === 0;
// Build message for Agent continuation
let message;
if (isSuccess) {
message = `Код выполнен успешно.
**Output:**
\`\`\`
${run.stdout || '(no output)'}
\`\`\`
**Time:** ${run.cpu_time}ms CPU | ${run.wall_time}ms wall | **Memory:** ${Math.round((run.memory || 0) / 1024)} KB
${next_prompt || 'Проанализируй результат и продолжи.'}`;
} else {
message = `Код завершился с ошибкой (exit code: ${run.code})
**Error:**
\`\`\`
${run.stderr || run.output || 'Unknown error'}
\`\`\`
Исправь ошибку и попробуй снова.`;
}
// Call Agent continue endpoint if configured
if (conversation_id && agent_id) {
try {
await axios.post(`${BACKEND_URL}/api/v3/ai-agents/continue`, {
conversation_id,
agent_id,
message,
tool_result: {
stdout: run.stdout,
stderr: run.stderr,
success: isSuccess,
code: run.code,
time: { cpu_ms: run.cpu_time, wall_ms: run.wall_time },
memory_bytes: run.memory
},
source: 'code_execution'
});
apiLogger.debug({ conversation_id, agent_id }, 'Agent callback sent');
} catch (callbackErr) {
apiLogger.warn({ err: callbackErr.message }, 'Agent callback failed');
}
}
return res.json({
status: 'completed',
success: isSuccess,
stdout: run.stdout || '',
stderr: run.stderr || '',
code: run.code,
time: { cpu_ms: run.cpu_time, wall_ms: run.wall_time },
message: conversation_id ? 'Result sent to Agent' : 'No callback configured'
});
} catch (err) {
apiLogger.error({ err: err.message }, 'Code execution with loop failed');
return error(res, 'EXECUTION_ERROR', `Execution failed: ${err.message}`);
}
});
/**
* POST /api/v3/code/validate
* Validate code syntax without execution
* Currently implemented for Python via ast.parse
*/
router.post('/validate', async (req, res) => {
try {
const { source_code, language = 'python' } = req.body;
if (!source_code) {
return badRequest(res, 'source_code is required');
}
if (language.toLowerCase() === 'python' || language.toLowerCase() === 'py') {
// Validate Python via ast.parse in Piston
const validationCode = `import ast
try:
ast.parse(${JSON.stringify(source_code)})
print("valid")
except SyntaxError as e:
print(f"SyntaxError: {e.msg} at line {e.lineno}")`;
const response = await axios.post(`${PISTON_URL}/api/v2/execute`, {
language: 'python',
version: '3.10.0',
files: [{ content: validationCode }],
run_timeout: 3000, // Piston max limit
});
const run = response.data.run || {};
const stdout = run.stdout || '';
// Check exact match - "valid" only, not "invalid" which also contains "valid"
const isValid = run.code === 0 && stdout.trim() === 'valid';
return res.json({
valid: isValid,
errors: isValid ? [] : [{
message: stdout.trim() || run.stderr || 'Syntax error'
}]
});
}
// For other languages, return success (validation not implemented)
return res.json({
valid: true,
errors: [],
message: `Validation not implemented for ${language}`
});
} catch (err) {
apiLogger.error({ err: err.message }, 'Code validation failed');
return error(res, 'VALIDATION_ERROR', `Validation failed: ${err.message}`);
}
});
export default router;