feat: workflow param substitution + link troubleshooter support

Adds {{param_name}} template substitution to the workflow execution engine
so workflows can accept user-supplied inputs at run time.

server.js:
- applyParams() helper — substitutes {{name}} in command strings with
  validated values (alphanumeric + safe punctuation only); unsafe values
  throw and fail the execution cleanly
- executeCommandStep() / executeWorkflowSteps() — accept params={} and
  apply substitution before dispatching commands to workers
- POST /api/executions — accepts params:{} from client; validates required
  params against definition.params[]; logs params in initial execution log

index.html:
- loadWorkflows() caches definition in _workflowRegistry keyed by id;
  shows "[N params]" badge on parameterised workflows
- executeWorkflow() checks for definition.params; if present, shows
  param input modal instead of plain confirm()
- showParamModal() — builds labelled input form from param definitions,
  marks required fields, focuses first input, Enter submits
- submitParamForm() — validates required fields, calls startExecution()
- startExecution() — POSTs {workflow_id, params} and switches to executions tab
- Param input modal — terminal-aesthetic overlay, no external dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 16:20:05 -05:00
parent 6d945a1913
commit 033237482d
2 changed files with 142 additions and 21 deletions

View File

@@ -494,7 +494,21 @@ function authenticateGandalf(req, res, next) {
}
// Workflow Execution Engine
async function executeWorkflowSteps(executionId, workflowId, definition, username) {
// Substitute {{param_name}} placeholders in a command string.
// Only alphanumeric + safe punctuation allowed in substituted values.
function applyParams(command, params) {
return command.replace(/\{\{(\w+)\}\}/g, (match, key) => {
if (!(key in params)) return match;
const val = String(params[key]).trim();
if (!/^[a-zA-Z0-9._:@\-\/]+$/.test(val)) {
throw new Error(`Unsafe value for workflow parameter "${key}"`);
}
return val;
});
}
async function executeWorkflowSteps(executionId, workflowId, definition, username, params = {}) {
try {
console.log(`[Workflow] Starting execution ${executionId} for workflow ${workflowId}`);
@@ -522,7 +536,7 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
if (step.type === 'execute') {
// Execute command step
const success = await executeCommandStep(executionId, step, i + 1);
const success = await executeCommandStep(executionId, step, i + 1, params);
if (!success) {
allStepsSucceeded = false;
break; // Stop workflow on failure
@@ -580,9 +594,12 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
}
}
async function executeCommandStep(executionId, step, stepNumber) {
async function executeCommandStep(executionId, step, stepNumber, params = {}) {
try {
const command = step.command;
let command = step.command;
if (Object.keys(params).length > 0) {
command = applyParams(command, params);
}
const targets = step.targets || ['all'];
// Determine which workers to target
@@ -817,7 +834,7 @@ app.post('/api/workers/heartbeat', async (req, res) => {
app.post('/api/executions', authenticateSSO, async (req, res) => {
try {
const { workflow_id } = req.body;
const { workflow_id, params = {} } = req.body;
const id = generateUUID();
// Get workflow definition
@@ -829,16 +846,27 @@ app.post('/api/executions', authenticateSSO, async (req, res) => {
const workflow = workflows[0];
const definition = typeof workflow.definition === 'string' ? JSON.parse(workflow.definition) : workflow.definition;
// Validate required params
const paramDefs = definition.params || [];
for (const pd of paramDefs) {
if (pd.required && !params[pd.name]) {
return res.status(400).json({ error: `Missing required parameter: ${pd.label || pd.name}` });
}
}
// Create execution record
const initLogs = Object.keys(params).length > 0
? [{ action: 'params', params, timestamp: new Date().toISOString() }]
: [];
await pool.query(
'INSERT INTO executions (id, workflow_id, status, started_by, started_at, logs) VALUES (?, ?, ?, ?, NOW(), ?)',
[id, workflow_id, 'running', req.user.username, JSON.stringify([])]
[id, workflow_id, 'running', req.user.username, JSON.stringify(initLogs)]
);
broadcast({ type: 'execution_started', execution_id: id, workflow_id });
// Start workflow execution asynchronously
executeWorkflowSteps(id, workflow_id, definition, req.user.username).catch(err => {
executeWorkflowSteps(id, workflow_id, definition, req.user.username, params).catch(err => {
console.error(`[Workflow] Execution ${id} failed:`, err);
});