GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix[backend](soar/mcp): make soar.rule.update partial (merge + delete… · utmstack/UTMStack@1c69e9b

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • backend/modules/mcp
    • catalog.json
    • docs_soar.go
    • tools_soar.go
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎backend/modules/mcp/catalog.json‎

Copy file name to clipboardExpand all lines: backend/modules/mcp/catalog.json
+9-8Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,12 @@
319319
{"name": "incident_history.count", "status": "implemented", "module": "incidents", "title": "Count incident history rows", "permission": "incidents.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"incident_id": {"type": "integer"}}}, "output": "{ count: int64 }", "maps_to": "incidents.IncidentHistoryUsecase.Count", "rest_equivalent": "GET /api/v1/incident-histories/count"},
320320
{"name": "incident_history.get", "status": "implemented", "module": "incidents", "title": "Get incident history row", "permission": "incidents.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"id": {"type": "integer"}}, "required": ["id"]}, "output": "HistoryResponse", "maps_to": "incidents.IncidentHistoryUsecase.GetByID", "rest_equivalent": "GET /api/v1/incident-histories/:id"},
321321
{"name": "incidents.assignees", "status": "implemented", "module": "incidents", "title": "List the assignees incidents are currently handed to", "permission": "incidents.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {}}, "output": "string[]", "maps_to": "incidents.IncidentUsecase.GetAssignees", "rest_equivalent": "GET /api/v1/incidents/assignees"},
322-
{"name": "soar.flow.create", "status": "implemented", "module": "soar", "title": "Create SOAR rule", "permission": "soar.write", "input": {"type": "object", "properties": {"name": {"type": "string"}, "description": {"type": "string"}, "conditions": {"type": "array", "items": {"$ref": "#/definitions/FilterVM"}, "description": "Alert trigger: ALL must match (AND)"}, "roots": {"type": "array", "items": {"type": "string"}, "description": "Node ids starting at depth 0"}, "nodes": {"type": "object", "additionalProperties": {"$ref": "#/definitions/FlowNodeVM"}, "description": "DAG nodes keyed by id — see mcp://utmstack/docs/soar-flow-guide"}, "max_depth": {"type": "integer"}, "active": {"type": "boolean"}}, "required": ["name", "conditions", "roots", "nodes", "active"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.Create", "rest_equivalent": "POST /api/v1/soar/rules"},
323-
{"name": "soar.flow.update", "status": "implemented", "module": "soar", "title": "Update SOAR rule", "permission": "soar.write", "input": {"type": "object", "properties": {"rel_path": {"type": "string"}, "name": {"type": "string"}, "conditions": {"type": "array", "items": {"$ref": "#/definitions/FilterVM"}}, "roots": {"type": "array", "items": {"type": "string"}}, "nodes": {"type": "object", "additionalProperties": {"$ref": "#/definitions/FlowNodeVM"}}, "max_depth": {"type": "integer"}, "active": {"type": "boolean"}}, "required": ["rel_path", "name", "conditions", "roots", "nodes", "active"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.Update", "rest_equivalent": "PUT /api/v1/soar/rules/:relPath"},
324-
{"name": "soar.flow.get", "status": "implemented", "module": "soar", "title": "Get SOAR rule", "permission": "soar.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}}, "required": ["rel_path"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.Get", "rest_equivalent": "GET /api/v1/soar/rules/:relPath"},
325-
{"name": "soar.flow.delete", "status": "implemented", "module": "soar", "title": "Delete SOAR rule", "permission": "soar.write", "annotations": {"destructiveHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}}, "required": ["rel_path"]}, "output": "{ rel_path: string, deleted: true }", "maps_to": "soar.RuleUsecase.Delete", "rest_equivalent": "DELETE /api/v1/soar/rules/:relPath"},
326-
{"name": "soar.flow.set_enabled", "status": "implemented", "module": "soar", "title": "Enable or disable SOAR rule", "permission": "soar.write", "annotations": {"idempotentHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}, "enabled": {"type": "boolean"}}, "required": ["rel_path", "enabled"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.SetEnabled", "rest_equivalent": "PUT /api/v1/soar/rules/:relPath/enabled"},
327-
{"name": "soar.flow.list", "status": "implemented", "module": "soar", "title": "List SOAR rules", "permission": "soar.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"name": {"type": "string"}, "enabled": {"type": "boolean"}, "template": {"type": "string"}, "page": {"type": "integer"}, "size": {"type": "integer"}}}, "output": "{ items: RuleResponse[], total: int64 }", "maps_to": "soar.RuleUsecase.List", "rest_equivalent": "GET /api/v1/soar/rules"},
322+
{"name": "soar.rule.create", "status": "implemented", "module": "soar", "title": "Create SOAR rule", "permission": "soar.write", "description": "Creates a NEW flow; the full flow (conditions, roots, nodes) is required. To modify an existing flow use soar.rule.update (partial) instead.", "input": {"type": "object", "properties": {"name": {"type": "string"}, "description": {"type": "string"}, "conditions": {"type": "array", "items": {"$ref": "#/definitions/FilterVM"}, "description": "Alert trigger: ALL must match (AND)"}, "roots": {"type": "array", "items": {"type": "string"}, "description": "Node ids starting at depth 0"}, "nodes": {"type": "object", "additionalProperties": {"$ref": "#/definitions/FlowNodeVM"}, "description": "DAG nodes keyed by id — see mcp://utmstack/docs/soar-flow-guide"}, "max_depth": {"type": "integer"}, "active": {"type": "boolean"}}, "required": ["name", "conditions", "roots", "nodes", "active"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.Create", "rest_equivalent": "POST /api/v1/soar/rules"},
323+
{"name": "soar.rule.update", "status": "implemented", "module": "soar", "title": "Update SOAR rule", "permission": "soar.write", "description": "PARTIAL update: send only what changes. Omitted fields keep their current value; nodes are merged by id; deleted_nodes removes nodes and scrubs their edges. Read the flow with soar.rule.get first, then send just the changed pieces. Never resend the whole flow.", "input": {"type": "object", "properties": {"rel_path": {"type": "string"}, "name": {"type": "string", "description": "omit to keep current"}, "description": {"type": "string", "description": "omit to keep current"}, "conditions": {"type": "array", "items": {"$ref": "#/definitions/FilterVM"}, "description": "full replace of the list; omit to keep current"}, "roots": {"type": "array", "items": {"type": "string"}, "description": "omit to keep current (deleted ids always pruned)"}, "nodes": {"type": "object", "additionalProperties": {"$ref": "#/definitions/FlowNodeVM"}, "description": "nodes to add or overwrite, merged over current — not a replacement"}, "deleted_nodes": {"type": "array", "items": {"type": "string"}, "description": "ids to remove; edges scrubbed automatically; must not overlap nodes"}, "max_depth": {"type": "integer", "description": "omit to keep current"}, "active": {"type": "boolean", "description": "omit to keep current (or use soar.rule.set_enabled)"}}, "required": ["rel_path"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.Update (merged at MCP layer)", "rest_equivalent": "PUT /api/v1/soar/rules/:relPath"},
324+
{"name": "soar.rule.get", "status": "implemented", "module": "soar", "title": "Get SOAR rule", "permission": "soar.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}}, "required": ["rel_path"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.Get", "rest_equivalent": "GET /api/v1/soar/rules/:relPath"},
325+
{"name": "soar.rule.delete", "status": "implemented", "module": "soar", "title": "Delete SOAR rule", "permission": "soar.write", "annotations": {"destructiveHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}}, "required": ["rel_path"]}, "output": "{ rel_path: string, deleted: true }", "maps_to": "soar.RuleUsecase.Delete", "rest_equivalent": "DELETE /api/v1/soar/rules/:relPath"},
326+
{"name": "soar.rule.set_enabled", "status": "implemented", "module": "soar", "title": "Enable or disable SOAR rule", "permission": "soar.write", "annotations": {"idempotentHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}, "enabled": {"type": "boolean"}}, "required": ["rel_path", "enabled"]}, "output": "RuleResponse", "maps_to": "soar.RuleUsecase.SetEnabled", "rest_equivalent": "PUT /api/v1/soar/rules/:relPath/enabled"},
327+
{"name": "soar.rule.list", "status": "implemented", "module": "soar", "title": "List SOAR rules", "permission": "soar.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"name": {"type": "string"}, "enabled": {"type": "boolean"}, "template": {"type": "string"}, "page": {"type": "integer"}, "size": {"type": "integer"}}}, "output": "{ items: RuleResponse[], total: int64 }", "maps_to": "soar.RuleUsecase.List", "rest_equivalent": "GET /api/v1/soar/rules"},
328328
{"name": "soar.node_types", "status": "implemented", "module": "soar", "title": "List SOAR flow node types", "permission": "soar.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {}}, "output": "{ kinds: string[], executor_types: string[] } — kinds is fixed (executor, enrichment); executor_types is the live registry on this instance (shell/http/conditional always; llm_enrich/llm_action, notify, incident, mail when configured)", "maps_to": "soar.Module.GetExecutorTypes"},
329329
{"name": "soar.rule.resolve_filter_values", "status": "implemented", "module": "soar", "title": "Suggest filter values", "permission": "soar.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"field": {"type": "string"}, "prefix": {"type": "string"}}, "required": ["field"]}, "output": "ResolveFilterValuesResponse", "maps_to": "soar.RuleUsecase.ResolveFilterValues", "rest_equivalent": "GET /api/v1/soar/rules/resolve-filter-values"},
330330
{"name": "soar.execution.list", "status": "implemented", "module": "soar", "title": "List rule executions", "permission": "soar.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"rule_rel_path": {"type": "string"}, "status": {"type": "string"}, "from": {"type": "string"}, "to": {"type": "string"}, "page": {"type": "integer"}, "size": {"type": "integer"}}}, "output": "{ items: ExecutionResponse[], total: int64 }", "maps_to": "soar.ExecutionUsecase.List", "rest_equivalent": "GET /api/v1/soar/rule-executions"},
@@ -586,17 +586,18 @@
586586
{
587587
"name": "soc.draft-soar-rule",
588588
"title": "Draft SOAR rule",
589-
"description": "Guides the model through SOAR flow construction: list existing flows to start from, list variables, propose filter conditions, then create the flow on confirmation.",
589+
"description": "Guides the model through SOAR flow construction: list existing flows to start from, read the flow if modifying it, list variables, propose filter conditions, then apply on confirmation — create for new flows, PARTIAL soar.rule.update (delta only) for existing ones.",
590590
"status": "implemented",
591591
"arguments": [
592592
{"name": "goal", "description": "Plain-language goal for the rule (e.g. 'isolate hosts on Mimikatz detections')", "required": true}
593593
],
594594
"tool_sequence": [
595595
"soar.rule.list",
596+
"soar.rule.get",
596597
"soar.variable.list",
597598
"soar.rule.resolve_filter_values",
598599
"(model drafts)",
599-
"soar.rule.create"
600+
"soar.rule.create | soar.rule.update (partial)"
600601
]
601602
}
602603
],
Collapse file

‎backend/modules/mcp/docs_soar.go‎

Copy file name to clipboardExpand all lines: backend/modules/mcp/docs_soar.go
+25-17Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ package mcp
66
const soarFlowGuideDoc = `# SOAR flow guide — building alert-response DAGs
77
88
A SOAR rule ("flow") is a DAG of nodes that runs when an alert matches its
9-
trigger conditions. Build flows with soar.rule.create / soar.rule.update:
10-
pass conditions + roots + nodes in one call. Read an existing flow first
11-
with soar.rule.get (rel_path from soar.rule.list).
9+
trigger conditions. Create NEW flows with soar.rule.create (full flow in one
10+
call). MODIFY existing flows with soar.rule.update — it is PARTIAL: send only
11+
the fields that change, never resend the whole flow (see "Editing an existing
12+
flow" below).
1213
1314
## Node kinds — executor vs enrichment
1415
@@ -93,20 +94,27 @@ and "failure path" subtrees.
9394
9495
## Editing an existing flow
9596
96-
soar.rule.update is FULL REPLACE, not a patch — it overwrites conditions, roots
97-
and the entire nodes map. Always: soar.rule.get(rel_path) → modify the complete
98-
payload in memory → send everything back.
99-
100-
1. **Add a node** — new id in nodes; wire it by appending that id to an
101-
existing node's onSuccess or onError (or to roots if it starts at depth 0).
102-
2. **Change a node** — edit its entry in place; keep its id stable (other
103-
nodes' edges and $(<nodeId>...) interpolations reference it).
104-
3. **Remove/replace a node** — delete its entry AND scrub its id from every
105-
other node's onSuccess/onError and from roots. To replace, keep the id and
106-
swap kind/executor/params, or delete + add under a new id and rewire edges.
107-
The server does NOT validate edge consistency at write time — dangling refs
108-
only surface at run time as dead branches, so you own referential integrity.
109-
4. **Enable/disable** — soar.rule.set_enabled(rel_path, enabled); no payload
97+
soar.rule.update is a PARTIAL update, not a full replace:
98+
- omitted fields keep their current value (name, description, conditions,
99+
roots, nodes, max_depth, active);
100+
- nodes is a MERGE by id: the ids you send are added or overwritten, every
101+
other node stays as-is;
102+
- deleted_nodes removes ids, and the server scrubs each id from every
103+
remaining node's onSuccess/onError and from roots. Never list the same id
104+
in both nodes and deleted_nodes.
105+
106+
So: soar.rule.get(rel_path) once, then send only the delta. Typical edits:
107+
108+
1. **Add a node** — send nodes: {"new_id": {...}}; if it wires off an
109+
existing node, also send that existing node with new_id appended to its
110+
onSuccess or onError. A new root: send roots: [current roots + new_id].
111+
2. **Change a node** — send nodes: {"same_id": {updated node}}. Keep the id
112+
stable: other nodes' edges and $(<nodeId>...) interpolations reference it.
113+
3. **Remove a node** — send deleted_nodes: ["id"]. Edge scrubbing is done
114+
for you; nothing else to send.
115+
4. **Change triggers** — conditions is a full replace of the small list, so
116+
send the complete new conditions array.
117+
5. **Enable/disable** — soar.rule.set_enabled(rel_path, enabled); no payload
110118
needed. System-owned flows are read-only (update/delete rejected).
111119
112120
## Worked example — block brute-force source IP unless internal

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.