Fix XSS, add per-step timeout/retry to workflow engine

index.html:
- Escape started_by in execution list, execution cards, and execution detail modal
- Escape schedule name in schedules list

server.js:
- Per-step timeout: step.timeout (seconds) overrides global COMMAND_TIMEOUT_MS (5s-600s range)
- Per-step retry: step.retries (max 5) with step.retryDelayMs (max 30s) re-sends command on failure
  with command_retry log entries showing attempt/max_retries progress

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 17:36:35 -04:00
parent ba5ba6f899
commit 8ff3700601
2 changed files with 44 additions and 9 deletions

View File

@@ -987,7 +987,7 @@ async function executeCommandStep(executionId, step, stepNumber, params = {}) {
}
// Send command to worker
const commandId = crypto.randomUUID();
let commandId = crypto.randomUUID();
await addExecutionLog(executionId, {
step: stepNumber,
@@ -998,21 +998,56 @@ async function executeCommandStep(executionId, step, stepNumber, params = {}) {
timestamp: new Date().toISOString()
});
// Per-step timeout override: step.timeout (seconds) overrides global default
const stepTimeoutMs = step.timeout
? Math.min(Math.max(parseInt(step.timeout, 10) * 1000, 5000), 600000)
: COMMAND_TIMEOUT_MS;
workerWs.send(JSON.stringify({
type: 'execute_command',
execution_id: executionId,
command_id: commandId,
command: command,
worker_id: workerId,
timeout: COMMAND_TIMEOUT_MS
timeout: stepTimeoutMs
}));
// Wait for command result (with timeout)
const result = await waitForCommandResult(executionId, commandId, COMMAND_TIMEOUT_MS);
// Per-step retry: step.retries (default 0) with step.retryDelayMs (default 2000)
const maxRetries = Math.min(parseInt(step.retries || 0, 10), 5);
const retryDelayMs = Math.min(parseInt(step.retryDelayMs || 2000, 10), 30000);
let result;
let attempt = 0;
while (true) {
result = await waitForCommandResult(executionId, commandId, stepTimeoutMs);
if (result.success || attempt >= maxRetries) break;
attempt++;
await addExecutionLog(executionId, {
step: stepNumber,
action: 'command_retry',
worker_id: workerId,
attempt,
max_retries: maxRetries,
message: `Command failed, retrying (attempt ${attempt}/${maxRetries})...`,
timestamp: new Date().toISOString()
});
await new Promise(r => setTimeout(r, retryDelayMs));
// Re-send command for retry
const retryCommandId = crypto.randomUUID();
await addExecutionLog(executionId, {
step: stepNumber, action: 'command_sent', worker_id: workerId,
command: command, command_id: retryCommandId, timestamp: new Date().toISOString()
});
workerWs.send(JSON.stringify({
type: 'execute_command', execution_id: executionId, command_id: retryCommandId,
command: command, worker_id: workerId, timeout: stepTimeoutMs
}));
commandId = retryCommandId; // update for next waitForCommandResult call
}
results.push(result);
if (!result.success) {
// Command failed, workflow should stop
// Command failed after all retries, workflow should stop
return false;
}
}