diff --git a/engine.go b/engine.go index 7eef2f63..51fd0647 100644 --- a/engine.go +++ b/engine.go @@ -401,6 +401,8 @@ func (e *StdEngine) BuildFromConfig(cfg *config.WorkflowConfig) error { // Compute config hash after transform hooks so the hash reflects the effective // runtime config (hooks may mutate cfg before modules are registered). + // Reset first so a marshal failure never leaves a stale hash from a previous build. + e.configHash = "" if configBytes, err := yaml.Marshal(cfg); err == nil { h := sha256.Sum256(configBytes) e.configHash = fmt.Sprintf("sha256:%x", h) diff --git a/module/api_v1_store.go b/module/api_v1_store.go index 66fc4344..d34b5b6d 100644 --- a/module/api_v1_store.go +++ b/module/api_v1_store.go @@ -962,8 +962,16 @@ func (s *V1Store) InsertLog(workflowID, executionID, level, message, moduleName, } // ListExecutionLogs returns log entries for an execution with optional level filter. -// Results are ordered by created_at ASC. limit=0 means no limit. +// Results are ordered by created_at ASC. limit=0 means no limit; negative values +// are treated as 0; values above maxExecutionLogLimit are clamped to that maximum. func (s *V1Store) ListExecutionLogs(executionID string, level string, limit int) ([]map[string]any, error) { + const maxExecutionLogLimit = 1000 + if limit < 0 { + limit = 0 + } else if limit > maxExecutionLogLimit { + limit = maxExecutionLogLimit + } + query := "SELECT id, workflow_id, execution_id, level, message, module_name, fields, created_at FROM execution_logs WHERE execution_id = ?" args := []any{executionID} if level != "" { diff --git a/module/execution_tracker.go b/module/execution_tracker.go index a74ac8a8..e34d64f9 100644 --- a/module/execution_tracker.go +++ b/module/execution_tracker.go @@ -124,11 +124,23 @@ func (t *ExecutionTracker) RecordEvent(ctx context.Context, executionID string, // This prevents PII leakage and unnecessary storage for normal runs. if state != nil && state.explicitTrace { t.handleStepInputRecorded(state, data) + // Write a minimal log entry (step name only, no payload) so + // GET /executions/{id}/logs can surface that an input was recorded. + minimal := map[string]any{} + if stepName, ok := data["step_name"]; ok { + minimal["step_name"] = stepName + } + t.writeLog(executionID, eventType, minimal, now) } return nil case "step.output_recorded": if state != nil && state.explicitTrace { t.handleStepOutputRecorded(state, data) + minimal := map[string]any{} + if stepName, ok := data["step_name"]; ok { + minimal["step_name"] = stepName + } + t.writeLog(executionID, eventType, minimal, now) } return nil } @@ -466,9 +478,17 @@ func (t *ExecutionTracker) TrackPipelineExecution( } // Set execution ID on pipeline for event correlation, and wire ourselves - // as the EventRecorder so step events flow to the tracker. + // as the EventRecorder so step events flow to the tracker. Save and + // restore previous values so we don't permanently mutate the pipeline's + // long-lived configuration. + prevExecID := pipeline.ExecutionID + prevRecorder := pipeline.EventRecorder pipeline.ExecutionID = execID pipeline.EventRecorder = t + defer func() { + pipeline.ExecutionID = prevExecID + pipeline.EventRecorder = prevRecorder + }() pc, pipeErr := pipeline.Execute(execCtx, triggerData) diff --git a/store/timeline_handler.go b/store/timeline_handler.go index 9f2eb7b5..9519278e 100644 --- a/store/timeline_handler.go +++ b/store/timeline_handler.go @@ -177,9 +177,19 @@ func (h *TimelineHandler) getExecutionLogs(w http.ResponseWriter, r *http.Reques level := q.Get("level") limit := 0 if limitStr := q.Get("limit"); limitStr != "" { - if n, err := json.Number(limitStr).Int64(); err == nil { - limit = int(n) + n, err := json.Number(limitStr).Int64() + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid limit parameter"}) + return + } + if n < 0 { + n = 0 + } + const maxLimit = 1000 + if n > maxLimit { + n = maxLimit } + limit = int(n) } logs, err := h.logQuerier.ListExecutionLogs(idStr, level, limit) diff --git a/ui/src/components/dashboard/WorkflowDashboard.tsx b/ui/src/components/dashboard/WorkflowDashboard.tsx index 6aa7ebbf..4e69f5bf 100644 --- a/ui/src/components/dashboard/WorkflowDashboard.tsx +++ b/ui/src/components/dashboard/WorkflowDashboard.tsx @@ -196,7 +196,18 @@ function TraceRequestModal({ const handleSend = () => { let headers: Record = {}; try { - headers = JSON.parse(headersText || '{}'); + const parsed = JSON.parse(headersText || '{}'); + if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) { + setHeadersError('Headers must be a JSON object'); + return; + } + for (const [k, v] of Object.entries(parsed)) { + if (typeof v !== 'string') { + setHeadersError(`Header value for "${k}" must be a string`); + return; + } + } + headers = parsed as Record; setHeadersError(''); } catch { setHeadersError('Headers must be valid JSON');