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:
@@ -1343,7 +1343,7 @@
|
||||
<div class="workflow-item" style="opacity: ${s.enabled ? 1 : 0.6};">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||
<div style="flex: 1;">
|
||||
<div class="workflow-name">${s.name}</div>
|
||||
<div class="workflow-name">${escapeHtml(s.name || '')}</div>
|
||||
<div style="color: var(--terminal-green); font-family: var(--font-mono); font-size: 0.9em; margin: 8px 0;">
|
||||
Command: <code>${escapeHtml(s.command)}</code>
|
||||
</div>
|
||||
@@ -1529,7 +1529,7 @@
|
||||
<div class="execution-item" onclick="viewExecution('${e.id}')">
|
||||
<span class="status ${e.status}">${e.status}</span>
|
||||
<strong>${e.workflow_name || '[Quick Command]'}</strong>
|
||||
<div class="timestamp">by ${e.started_by} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}</div>
|
||||
<div class="timestamp">by ${escapeHtml(e.started_by || '')} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('dashExecutions').innerHTML = dashHtml;
|
||||
@@ -1655,7 +1655,7 @@
|
||||
<span class="status ${e.status}">${e.status}</span>
|
||||
<strong>${e.workflow_name || '[Quick Command]'}</strong>
|
||||
<div class="timestamp">
|
||||
Started by ${e.started_by} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}
|
||||
Started by ${escapeHtml(e.started_by || '')} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}
|
||||
${e.completed_at ? ` • Completed at ${safeDate(e.completed_at)?.toLocaleString() ?? 'N/A'}` : elapsed}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1996,7 +1996,7 @@
|
||||
<div><strong>Status:</strong> <span class="status ${execution.status}">${execution.status}</span></div>
|
||||
<div><strong>Started:</strong> ${safeDate(execution.started_at)?.toLocaleString() ?? 'N/A'}</div>
|
||||
${execution.completed_at ? `<div><strong>Completed:</strong> ${safeDate(execution.completed_at)?.toLocaleString() ?? 'N/A'}</div>` : ''}
|
||||
<div><strong>Started by:</strong> ${execution.started_by}</div>
|
||||
<div><strong>Started by:</strong> ${escapeHtml(execution.started_by || '')}</div>
|
||||
`;
|
||||
|
||||
if (execution.waiting_for_input && execution.prompt) {
|
||||
|
||||
45
server.js
45
server.js
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user