From 086f4d504e31c8e55a34ed64bb05c8d6b7eceaa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Fri, 28 Aug 2026 12:41:42 -0600 Subject: [PATCH 1/9] feat(soar): add conditional if/else node --- backend/modules/soar/executor/conditional.go | 123 ++++++++++++++++ .../modules/soar/executor/conditional_test.go | 44 ++++++ backend/modules/soar/module.go | 7 +- .../components/ConditionalParamsEditor.tsx | 131 ++++++++++++++++++ .../soar/components/NodeInspector.tsx | 15 +- .../features/soar/components/NodePalette.tsx | 3 +- .../soar/components/nodes/DAGNode.tsx | 3 +- .../src/features/soar/types/soar.types.ts | 1 + frontend/src/shared/i18n/locales/en.json | 3 +- 9 files changed, 323 insertions(+), 7 deletions(-) create mode 100644 backend/modules/soar/executor/conditional.go create mode 100644 backend/modules/soar/executor/conditional_test.go create mode 100644 frontend/src/features/soar/components/ConditionalParamsEditor.tsx diff --git a/backend/modules/soar/executor/conditional.go b/backend/modules/soar/executor/conditional.go new file mode 100644 index 000000000..f5fb36752 --- /dev/null +++ b/backend/modules/soar/executor/conditional.go @@ -0,0 +1,123 @@ +package executor + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/tidwall/gjson" + + "github.com/utmstack/utmstack/backend/modules/soar/domain" +) + +// Conditional evaluates a list of predicates against the execution's merged +// context bag and returns success only when all of them are true (AND). A +// failing predicate returns an error so the dispatcher routes the flow down +// the node's onError branch — no bespoke edge kind needed. +// ponytail: reuses domain.FilterType and gjson (already vendored via variable +// + execution interpolation); OnSuccess/OnError already model the true/false +// exits of a conditional. +type Conditional struct{} + +func NewConditional() *Conditional { return &Conditional{} } + +func (Conditional) Type() string { return "conditional" } + +type conditionalParams struct { + Conditions []domain.FilterType `json:"conditions"` +} + +func (c *Conditional) Execute(_ context.Context, exec *domain.SoarExecution) (json.RawMessage, error) { + var p conditionalParams + if len(exec.Params) > 0 { + if err := json.Unmarshal(exec.Params, &p); err != nil { + return nil, fmt.Errorf("soar conditional: params: %w", err) + } + } + if len(p.Conditions) == 0 { + return nil, errors.New("soar conditional: at least one condition is required") + } + src := string(exec.Context) + if src == "" { + src = "{}" + } + for _, cond := range p.Conditions { + if !evaluateFilter(src, cond) { + exec.Result = fmt.Sprintf("condition failed: %s %s %v", cond.Field, cond.Operator, cond.Value) + return nil, errors.New(exec.Result) + } + } + exec.Result = "all conditions matched" + return nil, nil +} + +func evaluateFilter(src string, cond domain.FilterType) bool { + val := gjson.Get(src, cond.Field) + switch cond.Operator { + case domain.OperatorExists: + return val.Exists() + case domain.OperatorNotExists: + return !val.Exists() + } + got := val.String() + switch cond.Operator { + case domain.OperatorIS: + return got == asString(cond.Value) + case domain.OperatorISNot: + return got != asString(cond.Value) + case domain.OperatorContains: + return strings.Contains(got, asString(cond.Value)) + case domain.OperatorNotContains: + return !strings.Contains(got, asString(cond.Value)) + case domain.OperatorStartWith: + return strings.HasPrefix(got, asString(cond.Value)) + case domain.OperatorNotStartWith: + return !strings.HasPrefix(got, asString(cond.Value)) + case domain.OperatorEndsWith: + return strings.HasSuffix(got, asString(cond.Value)) + case domain.OperatorNotEndsWith: + return !strings.HasSuffix(got, asString(cond.Value)) + case domain.OperatorIsOneOf: + return oneOf(asStringSlice(cond.Value), got) + case domain.OperatorIsNotOneOf: + return !oneOf(asStringSlice(cond.Value), got) + } + return false +} + +func asString(v any) string { + switch t := v.(type) { + case string: + return t + case nil: + return "" + default: + b, _ := json.Marshal(t) + return string(b) + } +} + +func asStringSlice(v any) []string { + switch t := v.(type) { + case []string: + return t + case []any: + out := make([]string, 0, len(t)) + for _, x := range t { + out = append(out, asString(x)) + } + return out + } + return nil +} + +func oneOf(hay []string, needle string) bool { + for _, h := range hay { + if h == needle { + return true + } + } + return false +} diff --git a/backend/modules/soar/executor/conditional_test.go b/backend/modules/soar/executor/conditional_test.go new file mode 100644 index 000000000..98ad17f26 --- /dev/null +++ b/backend/modules/soar/executor/conditional_test.go @@ -0,0 +1,44 @@ +package executor + +import ( + "context" + "encoding/json" + "testing" + + "github.com/utmstack/utmstack/backend/modules/soar/domain" +) + +func TestConditional_AllMatchTakesSuccessBranch(t *testing.T) { + c := NewConditional() + exec := &domain.SoarExecution{ + Kind: domain.NodeKindExecutor, + Context: json.RawMessage(`{"alert":{"severity":"high","tags":["prod","edr"]}}`), + Params: json.RawMessage(`{"conditions":[ + {"field":"alert.severity","operator":"IS","value":"high"}, + {"field":"alert.tags","operator":"CONTAINS","value":"edr"} + ]}`), + } + if _, err := c.Execute(context.Background(), exec); err != nil { + t.Fatalf("expected success, got %v", err) + } +} + +func TestConditional_MismatchRoutesToOnError(t *testing.T) { + c := NewConditional() + exec := &domain.SoarExecution{ + Kind: domain.NodeKindExecutor, + Context: json.RawMessage(`{"alert":{"severity":"low"}}`), + Params: json.RawMessage(`{"conditions":[{"field":"alert.severity","operator":"IS","value":"high"}]}`), + } + if _, err := c.Execute(context.Background(), exec); err == nil { + t.Fatal("expected error so the dispatcher takes the onError branch") + } +} + +func TestConditional_MissingParamsFails(t *testing.T) { + c := NewConditional() + exec := &domain.SoarExecution{Context: json.RawMessage(`{}`)} + if _, err := c.Execute(context.Background(), exec); err == nil { + t.Fatal("expected error when no conditions are configured") + } +} diff --git a/backend/modules/soar/module.go b/backend/modules/soar/module.go index ea5f62140..e82506581 100644 --- a/backend/modules/soar/module.go +++ b/backend/modules/soar/module.go @@ -59,9 +59,10 @@ func NewModule( variableUC := usecase.NewVariableUsecase(variableRepo, cipher) registry := executor.Registry{ - "shell": executor.NewShell(agentClient), - "http": executor.NewHTTP(), - "select": executor.NewSelect(), + "shell": executor.NewShell(agentClient), + "http": executor.NewHTTP(), + "select": executor.NewSelect(), + "conditional": executor.NewConditional(), } if llm != nil { registry["llm_enrich"] = executor.NewLLMEnrich(llm) diff --git a/frontend/src/features/soar/components/ConditionalParamsEditor.tsx b/frontend/src/features/soar/components/ConditionalParamsEditor.tsx new file mode 100644 index 000000000..fe04be1b6 --- /dev/null +++ b/frontend/src/features/soar/components/ConditionalParamsEditor.tsx @@ -0,0 +1,131 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { Plus, X } from 'lucide-react' +import { Button } from '@/shared/components/ui/button' +import { Input } from '@/shared/components/ui/input' +import { ALERT_FIELDS } from '../lib/alert-fields' +import { enrichmentAncestors } from '../lib/ancestors' +import { + SOAR_MULTI_VALUE_OPERATORS, + SOAR_NO_VALUE_OPERATORS, + SOAR_OPERATORS, + type FlowCondition, + type FlowNode, + type SoarOperator, +} from '../types/soar.types' + +const SELECT = + 'h-8 rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring' + +interface Props { + nodeId: string + nodes: Record + params: unknown + readOnly?: boolean + onChange: (params: { conditions: FlowCondition[] }) => void +} + +// ponytail: reuses SoarOperator + native ; success/fail routing rides +// on the DAG's existing green/red handles — no bespoke branch state. +export function ConditionalParamsEditor({ nodeId, nodes, params, readOnly, onChange }: Props) { + const { t } = useTranslation() + const conditions = normalize(params) + const listId = `soar-cond-fields-${nodeId}` + const suggestions = useMemo(() => buildSuggestions(nodes, nodeId), [nodes, nodeId]) + + const setAt = (i: number, patch: Partial) => + onChange({ conditions: conditions.map((c, k) => (k === i ? { ...c, ...patch } : c)) }) + + const valueStr = (c: FlowCondition) => + Array.isArray(c.value) ? c.value.join(', ') : c.value == null ? '' : String(c.value) + + return ( +
+

{t('soar.editor.canvas.conditionalHint')}

+ + {suggestions.map((s) => ( + +
+ {conditions.map((c, i) => ( +
+ setAt(i, { field: e.target.value })} + placeholder="alert.severity" + className="h-8 min-w-[160px] flex-1 font-mono text-[11px]" + /> + + {!SOAR_NO_VALUE_OPERATORS.includes(c.operator) && ( + setAt(i, { value: e.target.value })} + placeholder={ + SOAR_MULTI_VALUE_OPERATORS.includes(c.operator) + ? t('soar.editor.valueList') + : t('soar.editor.value') + } + className="h-8 min-w-[140px] flex-1 font-mono text-[11px]" + /> + )} + {!readOnly && ( + + )} +
+ ))} +
+ {!readOnly && ( + + )} +
+ ) +} + +function normalize(params: unknown): FlowCondition[] { + if (!params || typeof params !== 'object') return [] + const list = (params as { conditions?: unknown }).conditions + return Array.isArray(list) ? (list as FlowCondition[]) : [] +} + +// Suggestion list for the field : alert.* plus every reachable +// enrichment ancestor's declared fields. Paths match the runtime context bag. +function buildSuggestions(nodes: Record, currentNodeId: string): string[] { + const out: string[] = ALERT_FIELDS.map((af) => `alert.${af.field}`) + for (const a of enrichmentAncestors(nodes, currentNodeId)) { + if (a.fields.length) out.push(...a.fields.map((f) => `${a.nodeId}.${f}`)) + else out.push(`${a.nodeId}.`) + } + return out +} diff --git a/frontend/src/features/soar/components/NodeInspector.tsx b/frontend/src/features/soar/components/NodeInspector.tsx index 632e80d2e..04f1671b3 100644 --- a/frontend/src/features/soar/components/NodeInspector.tsx +++ b/frontend/src/features/soar/components/NodeInspector.tsx @@ -6,6 +6,7 @@ import { Input } from '@/shared/components/ui/input' import { NODE_KINDS, EXECUTOR_CATALOG, type FlowNode, type NodeKind } from '../types/soar.types' import { COMMAND_TEMPLATES, shellKindFor } from '../lib/command-templates' import { AgentPicker } from './AgentPicker' +import { ConditionalParamsEditor } from './ConditionalParamsEditor' import { InsertFieldMenu } from './InsertFieldMenu' const SELECT = 'h-8 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring' @@ -230,7 +231,19 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang )} - {node.executor !== 'shell' && ( + {node.executor === 'conditional' && ( + + onChange({ params: next })} + /> + + )} + + {node.executor !== 'shell' && node.executor !== 'conditional' && ( {!readOnly && (
diff --git a/frontend/src/features/soar/components/NodePalette.tsx b/frontend/src/features/soar/components/NodePalette.tsx index 666d0e7a4..ae98590d0 100644 --- a/frontend/src/features/soar/components/NodePalette.tsx +++ b/frontend/src/features/soar/components/NodePalette.tsx @@ -1,4 +1,4 @@ -import { Bell, Boxes, Brain, Globe, Sparkles, Terminal, Zap } from 'lucide-react' +import { Bell, Boxes, Brain, GitBranch, Globe, Sparkles, Terminal, Zap } from 'lucide-react' import { EXECUTOR_CATALOG, type ExecutorMeta, type NodeKind } from '../types/soar.types' const ICONS: Record = { @@ -8,6 +8,7 @@ const ICONS: Record = { llm_enrich: Brain, llm_action: Zap, notify: Bell, + conditional: GitBranch, } /** Palette of draggable node types. Each row is one (executor, kind) pair — diff --git a/frontend/src/features/soar/components/nodes/DAGNode.tsx b/frontend/src/features/soar/components/nodes/DAGNode.tsx index aa817744a..dc6997599 100644 --- a/frontend/src/features/soar/components/nodes/DAGNode.tsx +++ b/frontend/src/features/soar/components/nodes/DAGNode.tsx @@ -1,6 +1,6 @@ import { memo } from 'react' import { Handle, Position, type NodeProps } from '@xyflow/react' -import { Bell, Boxes, Sparkles, Terminal, Globe, Brain, Zap } from 'lucide-react' +import { Bell, Boxes, Sparkles, Terminal, Globe, Brain, Zap, GitBranch } from 'lucide-react' import { cn } from '@/shared/lib/utils' import type { FlowNode } from '../../types/soar.types' @@ -17,6 +17,7 @@ const EXECUTOR_ICONS: Record = { llm_enrich: Brain, llm_action: Zap, notify: Bell, + conditional: GitBranch, } const KIND_TONES = { diff --git a/frontend/src/features/soar/types/soar.types.ts b/frontend/src/features/soar/types/soar.types.ts index 1cfb00b28..d566d39ac 100644 --- a/frontend/src/features/soar/types/soar.types.ts +++ b/frontend/src/features/soar/types/soar.types.ts @@ -155,6 +155,7 @@ export const EXECUTOR_CATALOG: ExecutorMeta[] = [ { type: 'llm_enrich', label: 'LLM enrichment', kinds: ['enrichment'], paramsPlaceholder: { prompt: '' } }, { type: 'llm_action', label: 'LLM action', kinds: ['executor'], paramsPlaceholder: { prompt: '' } }, { type: 'notify', label: 'Send notification', kinds: ['executor'], paramsPlaceholder: { message: '', type: 'INFO' } }, + { type: 'conditional', label: 'Conditional (if/else branch)', kinds: ['executor'], paramsPlaceholder: { conditions: [] } }, ] export function executorMeta(type: string): ExecutorMeta | undefined { diff --git a/frontend/src/shared/i18n/locales/en.json b/frontend/src/shared/i18n/locales/en.json index 79a10f847..2a876543a 100644 --- a/frontend/src/shared/i18n/locales/en.json +++ b/frontend/src/shared/i18n/locales/en.json @@ -5160,7 +5160,8 @@ "alert": "Alert", "wholeOutput": "whole output (edit the path)", "noAncestorsHint": "No enrichment ancestors yet — add an enrichment node upstream to expose its output here.", - "inserts": "inserts" + "inserts": "inserts", + "conditionalHint": "All conditions must match. Match → success branch (green); miss → error branch (red)." } }, "executions": { From 64619f6837a4a5127c6d211ace7fb9bad9892468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Fri, 28 Aug 2026 12:47:28 -0600 Subject: [PATCH 2/9] i18n[frontend](soar): translate conditional node hint --- frontend/src/shared/i18n/locales/de.json | 3 ++- frontend/src/shared/i18n/locales/es.json | 3 ++- frontend/src/shared/i18n/locales/fr.json | 3 ++- frontend/src/shared/i18n/locales/it.json | 3 ++- frontend/src/shared/i18n/locales/pt.json | 3 ++- frontend/src/shared/i18n/locales/ru.json | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/shared/i18n/locales/de.json b/frontend/src/shared/i18n/locales/de.json index ecddf9629..8961dac10 100644 --- a/frontend/src/shared/i18n/locales/de.json +++ b/frontend/src/shared/i18n/locales/de.json @@ -4774,7 +4774,8 @@ "alert": "Warnung", "wholeOutput": "gesamte Ausgabe (Pfad bearbeiten)", "noAncestorsHint": "Noch keine Anreicherungs-Vorgänger — fügen Sie einen Anreicherungsknoten davor ein, um dessen Ausgabe hier verfügbar zu machen.", - "inserts": "fügt ein" + "inserts": "fügt ein", + "conditionalHint": "Alle Bedingungen müssen zutreffen. Treffer → Erfolgszweig (grün); Fehltreffer → Fehlerzweig (rot)." } }, "executions": { diff --git a/frontend/src/shared/i18n/locales/es.json b/frontend/src/shared/i18n/locales/es.json index 8464c0961..993f8a68b 100644 --- a/frontend/src/shared/i18n/locales/es.json +++ b/frontend/src/shared/i18n/locales/es.json @@ -4896,7 +4896,8 @@ "alert": "Alerta", "wholeOutput": "salida completa (edita la ruta)", "noAncestorsHint": "Aún no hay ancestros de enriquecimiento — añade un nodo de enriquecimiento previo para exponer su salida aquí.", - "inserts": "inserta" + "inserts": "inserta", + "conditionalHint": "Todas las condiciones deben coincidir. Coincidencia → rama de éxito (verde); fallo → rama de error (rojo)." } }, "executions": { diff --git a/frontend/src/shared/i18n/locales/fr.json b/frontend/src/shared/i18n/locales/fr.json index 4b00ae73a..f4c6cad52 100644 --- a/frontend/src/shared/i18n/locales/fr.json +++ b/frontend/src/shared/i18n/locales/fr.json @@ -4774,7 +4774,8 @@ "alert": "Alerte", "wholeOutput": "sortie complète (éditez le chemin)", "noAncestorsHint": "Aucun ancêtre d'enrichissement — ajoutez un nœud d'enrichissement en amont pour exposer sa sortie ici.", - "inserts": "insère" + "inserts": "insère", + "conditionalHint": "Toutes les conditions doivent correspondre. Correspondance → branche succès (verte) ; échec → branche erreur (rouge)." } }, "executions": { diff --git a/frontend/src/shared/i18n/locales/it.json b/frontend/src/shared/i18n/locales/it.json index f1e7b6b7f..3fe43c63a 100644 --- a/frontend/src/shared/i18n/locales/it.json +++ b/frontend/src/shared/i18n/locales/it.json @@ -4774,7 +4774,8 @@ "alert": "Allerta", "wholeOutput": "output completo (modifica il percorso)", "noAncestorsHint": "Nessun antenato di arricchimento — aggiungi un nodo di arricchimento a monte per esporre qui il suo output.", - "inserts": "inserisce" + "inserts": "inserisce", + "conditionalHint": "Tutte le condizioni devono corrispondere. Corrispondenza → ramo di successo (verde); mancata → ramo di errore (rosso)." } }, "executions": { diff --git a/frontend/src/shared/i18n/locales/pt.json b/frontend/src/shared/i18n/locales/pt.json index 26f9ba6f1..fc7214f2b 100644 --- a/frontend/src/shared/i18n/locales/pt.json +++ b/frontend/src/shared/i18n/locales/pt.json @@ -4896,7 +4896,8 @@ "alert": "Alerta", "wholeOutput": "saída completa (edite o caminho)", "noAncestorsHint": "Ainda não há ancestrais de enriquecimento — adicione um nó de enriquecimento acima para expor sua saída aqui.", - "inserts": "insere" + "inserts": "insere", + "conditionalHint": "Todas as condições devem corresponder. Correspondência → ramo de sucesso (verde); falha → ramo de erro (vermelho)." } }, "executions": { diff --git a/frontend/src/shared/i18n/locales/ru.json b/frontend/src/shared/i18n/locales/ru.json index 8613b7396..62ca32e08 100644 --- a/frontend/src/shared/i18n/locales/ru.json +++ b/frontend/src/shared/i18n/locales/ru.json @@ -4566,7 +4566,8 @@ "alert": "Алерт", "wholeOutput": "весь вывод (отредактируйте путь)", "noAncestorsHint": "Пока нет предков обогащения — добавьте узел обогащения выше по цепочке, чтобы его вывод стал доступен здесь.", - "inserts": "вставляет" + "inserts": "вставляет", + "conditionalHint": "Все условия должны совпадать. Совпадение → ветвь успеха (зелёная); несовпадение → ветвь ошибки (красная)." } }, "executions": { From 72211d97b0d790b925b05e0ae799dde93546c965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Fri, 28 Aug 2026 12:51:46 -0600 Subject: [PATCH 3/9] refactor(soar): drop select enrichment executor --- backend/modules/soar/executor/selectexec.go | 61 ------------------- backend/modules/soar/module.go | 1 - .../features/soar/components/NodePalette.tsx | 3 +- .../soar/components/nodes/DAGNode.tsx | 3 +- frontend/src/features/soar/lib/ancestors.ts | 15 +---- .../src/features/soar/types/soar.types.ts | 1 - 6 files changed, 3 insertions(+), 81 deletions(-) delete mode 100644 backend/modules/soar/executor/selectexec.go diff --git a/backend/modules/soar/executor/selectexec.go b/backend/modules/soar/executor/selectexec.go deleted file mode 100644 index 57e54accd..000000000 --- a/backend/modules/soar/executor/selectexec.go +++ /dev/null @@ -1,61 +0,0 @@ -package executor - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - "github.com/tidwall/gjson" - - "github.com/utmstack/utmstack/backend/modules/soar/domain" -) - -// Select is a lightweight enrichment: it composes an output object by pulling -// gjson paths out of the current context bag. Handy when downstream nodes want -// a subset (or a renamed subset) of ancestor data without dragging in a jq -// dependency. Meant for kind=enrichment. -// ponytail: gjson is already vendored (used in variable + execution -// interpolation); output built via encoding/json — no new dep. -type Select struct{} - -func NewSelect() *Select { return &Select{} } - -func (Select) Type() string { return "select" } - -type selectParams struct { - Fields map[string]string `json:"fields"` -} - -func (s *Select) Execute(_ context.Context, exec *domain.SoarExecution) (json.RawMessage, error) { - if exec.Kind != domain.NodeKindEnrichment { - return nil, errors.New("soar select: kind must be enrichment") - } - var p selectParams - if len(exec.Params) > 0 { - if err := json.Unmarshal(exec.Params, &p); err != nil { - return nil, fmt.Errorf("soar select: params: %w", err) - } - } - if len(p.Fields) == 0 { - return json.RawMessage(`{}`), nil - } - src := string(exec.Context) - if src == "" { - src = "{}" - } - out := make(map[string]json.RawMessage, len(p.Fields)) - for name, path := range p.Fields { - val := gjson.Get(src, path) - if !val.Exists() { - continue - } - out[name] = json.RawMessage(val.Raw) - } - raw, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("soar select: marshal: %w", err) - } - exec.Result = string(raw) - return raw, nil -} diff --git a/backend/modules/soar/module.go b/backend/modules/soar/module.go index e82506581..16b8ea036 100644 --- a/backend/modules/soar/module.go +++ b/backend/modules/soar/module.go @@ -61,7 +61,6 @@ func NewModule( registry := executor.Registry{ "shell": executor.NewShell(agentClient), "http": executor.NewHTTP(), - "select": executor.NewSelect(), "conditional": executor.NewConditional(), } if llm != nil { diff --git a/frontend/src/features/soar/components/NodePalette.tsx b/frontend/src/features/soar/components/NodePalette.tsx index ae98590d0..2eca5552f 100644 --- a/frontend/src/features/soar/components/NodePalette.tsx +++ b/frontend/src/features/soar/components/NodePalette.tsx @@ -1,10 +1,9 @@ -import { Bell, Boxes, Brain, GitBranch, Globe, Sparkles, Terminal, Zap } from 'lucide-react' +import { Bell, Brain, GitBranch, Globe, Sparkles, Terminal, Zap } from 'lucide-react' import { EXECUTOR_CATALOG, type ExecutorMeta, type NodeKind } from '../types/soar.types' const ICONS: Record = { shell: Terminal, http: Globe, - select: Boxes, llm_enrich: Brain, llm_action: Zap, notify: Bell, diff --git a/frontend/src/features/soar/components/nodes/DAGNode.tsx b/frontend/src/features/soar/components/nodes/DAGNode.tsx index dc6997599..0241e7e6b 100644 --- a/frontend/src/features/soar/components/nodes/DAGNode.tsx +++ b/frontend/src/features/soar/components/nodes/DAGNode.tsx @@ -1,6 +1,6 @@ import { memo } from 'react' import { Handle, Position, type NodeProps } from '@xyflow/react' -import { Bell, Boxes, Sparkles, Terminal, Globe, Brain, Zap, GitBranch } from 'lucide-react' +import { Bell, Sparkles, Terminal, Globe, Brain, Zap, GitBranch } from 'lucide-react' import { cn } from '@/shared/lib/utils' import type { FlowNode } from '../../types/soar.types' @@ -13,7 +13,6 @@ export interface DAGNodeData extends FlowNode { const EXECUTOR_ICONS: Record = { shell: Terminal, http: Globe, - select: Boxes, llm_enrich: Brain, llm_action: Zap, notify: Bell, diff --git a/frontend/src/features/soar/lib/ancestors.ts b/frontend/src/features/soar/lib/ancestors.ts index 643711171..9e03df7b2 100644 --- a/frontend/src/features/soar/lib/ancestors.ts +++ b/frontend/src/features/soar/lib/ancestors.ts @@ -26,7 +26,7 @@ export function enrichmentAncestors(nodes: Record, target: str const n = nodes[id] if (!n) continue if (n.kind === 'enrichment') { - out.push({ nodeId: id, executor: n.executor, fields: declaredFields(n) }) + out.push({ nodeId: id, executor: n.executor, fields: [] }) } for (const parent of reverse.get(id) ?? []) queue.push(parent) } @@ -48,16 +48,3 @@ function buildReverseIndex(nodes: Record): Map } | undefined - if (p && typeof p === 'object' && p.fields && typeof p.fields === 'object' && !Array.isArray(p.fields)) { - return Object.keys(p.fields) - } - } - return [] -} diff --git a/frontend/src/features/soar/types/soar.types.ts b/frontend/src/features/soar/types/soar.types.ts index d566d39ac..c59d46b6f 100644 --- a/frontend/src/features/soar/types/soar.types.ts +++ b/frontend/src/features/soar/types/soar.types.ts @@ -151,7 +151,6 @@ export interface ExecutorMeta { export const EXECUTOR_CATALOG: ExecutorMeta[] = [ { type: 'shell', label: 'Shell (endpoint agent)', kinds: ['executor'] }, { type: 'http', label: 'HTTP call', kinds: ['executor', 'enrichment'], paramsPlaceholder: { method: 'GET', url: '' } }, - { type: 'select', label: 'Select (context transform)', kinds: ['enrichment'], paramsPlaceholder: { fields: {} } }, { type: 'llm_enrich', label: 'LLM enrichment', kinds: ['enrichment'], paramsPlaceholder: { prompt: '' } }, { type: 'llm_action', label: 'LLM action', kinds: ['executor'], paramsPlaceholder: { prompt: '' } }, { type: 'notify', label: 'Send notification', kinds: ['executor'], paramsPlaceholder: { message: '', type: 'INFO' } }, From c5ae19867b17f56adb657d5a09b3e6de50756add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Fri, 28 Aug 2026 13:00:03 -0600 Subject: [PATCH 4/9] fix[frontend](soar-flows): improved node inspector --- .../features/soar/components/FlowCanvas.tsx | 2 +- .../soar/components/NodeInspector.tsx | 35 +------------------ 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/frontend/src/features/soar/components/FlowCanvas.tsx b/frontend/src/features/soar/components/FlowCanvas.tsx index 407c48654..6031a6f38 100644 --- a/frontend/src/features/soar/components/FlowCanvas.tsx +++ b/frontend/src/features/soar/components/FlowCanvas.tsx @@ -335,7 +335,7 @@ function FlowCanvasInner({ roots, nodes, readOnly, onChange }: Props) { return (
{paletteOpen ? ( -
+