From 7fe0fda7a596454474d5024a7973761d57526c84 Mon Sep 17 00:00:00 2001 From: Rihards Date: Mon, 1 Jun 2026 18:25:37 +0000 Subject: [PATCH] Import UAPF package: incident-triage.uapf --- .gitignore | 4 + README.md | 69 ++++++ algorithms/classify_incident.card.yaml | 133 ++++++++++ algorithms/draft_response.card.yaml | 123 ++++++++++ algorithms/emit_event.card.yaml | 120 +++++++++ algorithms/evaluate_dmn.card.yaml | 111 +++++++++ algorithms/normalize_signal.card.yaml | 91 +++++++ algorithms/suggest_priority.card.yaml | 98 ++++++++ algorithms/update_incident.card.yaml | 122 +++++++++ bpmn/incident-triage.bpmn | 274 +++++++++++++++++++++ dmn/ownership.dmn | 142 +++++++++++ dmn/priority.dmn | 326 +++++++++++++++++++++++++ dmn/routing.dmn | 278 +++++++++++++++++++++ docs/host-lookup-tables.md | 49 ++++ docs/overview.md | 60 +++++ fixtures/signal-email-customer-lv.json | 16 ++ fixtures/signal-zabbix-ddos.json | 17 ++ fixtures/signal-zabbix-link-down.json | 18 ++ manifest.json | 63 +++++ metadata/lifecycle.yaml | 16 ++ metadata/ownership.yaml | 19 ++ processgit.mcp.yaml | 15 ++ resources/guardrails.yaml | 37 +++ resources/mappings.yaml | 194 +++++++++++++++ tests/bpmn/triage-link-down.test.yaml | 35 +++ uapf.yaml | 79 ++++++ 26 files changed, 2509 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 algorithms/classify_incident.card.yaml create mode 100644 algorithms/draft_response.card.yaml create mode 100644 algorithms/emit_event.card.yaml create mode 100644 algorithms/evaluate_dmn.card.yaml create mode 100644 algorithms/normalize_signal.card.yaml create mode 100644 algorithms/suggest_priority.card.yaml create mode 100644 algorithms/update_incident.card.yaml create mode 100644 bpmn/incident-triage.bpmn create mode 100644 dmn/ownership.dmn create mode 100644 dmn/priority.dmn create mode 100644 dmn/routing.dmn create mode 100644 docs/host-lookup-tables.md create mode 100644 docs/overview.md create mode 100644 fixtures/signal-email-customer-lv.json create mode 100644 fixtures/signal-zabbix-ddos.json create mode 100644 fixtures/signal-zabbix-link-down.json create mode 100644 manifest.json create mode 100644 metadata/lifecycle.yaml create mode 100644 metadata/ownership.yaml create mode 100644 processgit.mcp.yaml create mode 100644 resources/guardrails.yaml create mode 100644 resources/mappings.yaml create mode 100644 tests/bpmn/triage-link-down.test.yaml create mode 100644 uapf.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..075649b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.uapf.tmp +.DS_Store +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..baa8e90 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# lv.itsm.incident.triage + +UAPF v2.5.0 package — LVRTC Incident Triage. + +## Quick install (uapf-engine) + +The engine reads `.uapf` ZIP archives from its `/packages` mount. Build: + +```bash +cd uapf-packages/incident-triage +zip -rq ../incident-triage.uapf . +mv ../incident-triage.uapf /path/to/engine/packages/ +docker compose restart uapf-engine +``` + +Or use the admin install-from-url endpoint: + +```bash +curl -X POST http://uapf-engine:4000/uapf/admin/install-from-url \ + -H "Authorization: Bearer $UAPF_ENGINE_AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"sourceUrl": "https://processgit.org/AI_Sandbox/incident-triage/archive/main.zip"}' +``` + +## Layout + +``` +incident-triage/ + manifest.json UAPF v2.5.0 manifest + uapf.yaml YAML mirror of manifest (easier diff) + algorithms/ 7 Algorithm Cards with embedded v2.5.0 tests + bpmn/ incident-triage.bpmn — 9 service tasks, linear + dmn/ 3 DMN 1.3 tables: priority, ownership, routing + resources/ guardrails.yaml, mappings.yaml + metadata/ lifecycle.yaml, ownership.yaml + docs/ overview.md (start here) + fixtures/ 3 sample signals + expected post-triage state + tests/bpmn/ Sidecar BPMN scenario tests +``` + +## Algorithm Cards + +| Card | Capability | Determinism | +|---|---|---| +| `algo.incident_triage.normalize_signal` | `intake.normalize@1` | deterministic | +| `algo.incident_triage.classify_incident` | `ai.classify@1` | stochastic | +| `algo.incident_triage.suggest_priority` | `ai.suggest_priority@1` | stochastic | +| `algo.incident_triage.evaluate_dmn` | `dmn.evaluate@1` | deterministic | +| `algo.incident_triage.draft_response` | `ai.draft_response@1` | stochastic | +| `algo.incident_triage.update_incident` | `incident.update@1` | deterministic, stateful | +| `algo.incident_triage.emit_event` | `event.emit@1` | deterministic, stateful | + +All seven carry the v2.5.0 mandatory embedded `tests` array (≥ 2 cases each; +17 cases total across the package). + +## Spec version + +Tracks **`main`** of `github.com/UAPFormat/UAPF-specification`. Current +locked spec at package release: **v2.5.0** (2026-05-21). + +## Source + +- Workspace (OpenITSM repo): `uapf-packages/incident-triage/` +- ProcessGit (canonical): `https://processgit.org/AI_Sandbox/incident-triage` +- Engine: any uapf-engine ≥ commit aligned with UAPF v2.5.0 schemas + +## License + +MIT — same as the OpenITSM repository. diff --git a/algorithms/classify_incident.card.yaml b/algorithms/classify_incident.card.yaml new file mode 100644 index 0000000..796609c --- /dev/null +++ b/algorithms/classify_incident.card.yaml @@ -0,0 +1,133 @@ +kind: uapf.algorithm.card +id: algo.incident_triage.classify_incident +version: 1.0.0 +name: Incident classifier +intent: | + Reads the normalised payload and picks one taxonomy code from a fixed + closed list. The classifier is LLM-backed at runtime (Claude via the + LLM gateway) and falls back to a deterministic keyword matcher when + the gateway is unreachable. The taxonomy code is the primary driver + for the priority and routing DMN decisions; downstream rules treat + this output as authoritative. +algorithm_kind: classifier + +io: + inputs: + - id: payload + type: object + cardinality: single + documentation: | + The normalized_payload from the upstream intake.normalize step. + At minimum {title, description?, host?, severity?}. + - id: text + type: string + cardinality: single + documentation: | + Optional pre-flattened text. If absent, the host derives it from + payload.title + payload.description + payload.host. + outputs: + - id: taxonomy_code + type: string + constraints: + enum: + - network.outage.link_down + - network.degradation + - network.routing + - network.dns + - security.incident + - facility.power + - storage.capacity + - service.customer_request + - unknown.uncategorized + documentation: The chosen taxonomy code from the closed list above. + - id: confidence + type: probability + constraints: + minimum: 0 + maximum: 1 + documentation: Model-reported confidence; the stub fallback returns 0.75 for matched / 0.20 for unmatched. + - id: reasoning + type: string + documentation: One-sentence justification (English). Persisted with the AI decision; not shown to operator by default. + - id: label_hint + type: string + documentation: Human-friendly short label derived from the taxonomy code (e.g. "link_down"). + +implementation: + type: external + medium: mcp_tool + uri: uapf-ip://capability/ai.classify@1 + hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 + runtime: + capability: ai.classify@1 + note: | + Host-fulfilled UAPF-IP capability backed by the LLM gateway + (default Anthropic). When LLM_PROVIDER is unavailable, the host + falls back to a regex-driven keyword matcher that produces the + same output shape. + +determinism: stochastic +side_effects: pure +complexity: + typical_latency_ms: 800 + max_latency_ms: 30000 +failure_mode: | + Returns taxonomy_code='unknown.uncategorized' with confidence<=0.25. + Triage continues; the DMN priority table treats unknown as P4 default. + +reference: + legal: | + Latvijas Republikas Datu valsts inspekcijas vadlīnijas par + automatizētu lēmumu pieņemšanu — operators may override at any time. + standard: | + ITIL 4 — Incident Management practice; ISO/IEC 20000-1 — service + management taxonomy alignment. + +limitations: + - Closed taxonomy of 9 codes — broader incident types fall to unknown.uncategorized. + - Latvian and English input supported; mixed-locale text may degrade confidence. + +owners: + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: + status: draft + +tests: + - name: bgp-flap-network-routing + description: | + Edge router BGP session flapping — the classifier should pick + network.routing, not the broader network.outage.link_down. + inputs: + payload: + title: "BGP session flapping rtr-core-02 → AS6939" + host: "rtr-core-02.lvrtc.lv" + description: "BGP peer 198.51.100.1 toggled UP/DOWN 7 times in 12 minutes." + severity: "high" + expected_outputs: + taxonomy_code: "network.routing" + - name: customer-bandwidth-request + description: | + Latvian customer email asking for a bandwidth uplift — a + service.customer_request, not a network outage. + inputs: + payload: + title: "Klients SIA Latvija Tev: lūgums palielināt joslas platumu" + description: "Mūsu uzņēmumam nepieciešams palielināt internet pieslēguma joslas platumu no 100 Mbps uz 500 Mbps." + severity: "average" + expected_outputs: + taxonomy_code: "service.customer_request" + - name: ddos-volumetric + description: | + Volumetric UDP flood pattern — security.incident takes precedence + over generic network classifications even when the symptom is + network-shaped. + inputs: + payload: + title: "DDoS attack pattern detected on edge" + description: "Volumetric UDP flood, 4.2 Gbps inbound to 192.0.2.0/24." + severity: "critical" + expected_outputs: + taxonomy_code: "security.incident" diff --git a/algorithms/draft_response.card.yaml b/algorithms/draft_response.card.yaml new file mode 100644 index 0000000..9a60e28 --- /dev/null +++ b/algorithms/draft_response.card.yaml @@ -0,0 +1,123 @@ +kind: uapf.algorithm.card +id: algo.incident_triage.draft_response +version: 1.0.0 +name: Customer response drafter +intent: | + Drafts a customer-facing incident notification in parallel Latvian + and English on behalf of LVRTC. Output is a PROPOSED AIDecision — + never auto-sent. Operator approval in the GUI is required before any + message leaves the system (guardrail approval.human_required_for + enforces this at runtime). + + Tone: professional, calm, factual. Acknowledges the problem, states + that the team is investigating, gives an ETA only if known. Does NOT + promise specific resolutions. Uses proper Latvian diacritics. Bodies + capped at ~90 words each. +algorithm_kind: transformer + +io: + inputs: + - id: case_id + type: string + cardinality: single + constraints: + pattern: "^[0-9a-fA-F-]{36}$" + - id: locale + type: string + constraints: + enum: [lv, en, auto] + documentation: | + 'lv' or 'en' forces a single primary locale; 'auto' produces + both bodies and lets the operator choose. Default 'lv' for LVRTC. + - id: what_happened + type: string + documentation: One-line summary of the incident (used in subject + opening). + - id: eta_minutes + type: integer + cardinality: single + constraints: + minimum: 0 + documentation: 'Optional. When provided, surfaced in body as \"Aptuvenais risināšanas laiks: X min\".' + outputs: + - id: subject_lv + type: string + - id: subject_en + type: string + - id: body_lv + type: string + documentation: Latvian body, proper diacritics, <=~90 words. + - id: body_en + type: string + documentation: English body, <=~90 words. + - id: locale + type: string + documentation: The locale code echoed back; informational. + +implementation: + type: external + medium: mcp_tool + uri: uapf-ip://capability/ai.draft_response@1 + hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 + runtime: + capability: ai.draft_response@1 + note: | + Host-fulfilled UAPF-IP capability backed by the LLM gateway with + Anthropic as the default provider. The host enforces that the + resulting AIDecision row is PROPOSED (never AUTO_APPLIED) for + this capability. Operator approval moves it to APPROVED before + any outbound transport adapter is invoked. + +determinism: stochastic +side_effects: pure +complexity: + typical_latency_ms: 1500 + max_latency_ms: 60000 +failure_mode: | + Returns deterministic stub drafts ("Mūsu komanda ir saņēmusi + paziņojumu...") with locale='lv' and a flag indicating LLM unavailability. + Operator can edit before approving. + +reference: + legal: | + GDPR 2016/679 Article 13 — information to data subjects; LVRTC + customer-communication standards. + standard: | + ITIL 4 — Communication and Awareness practice during major incidents. + +limitations: + - Cap of ~90 words per body — long incident narratives are truncated. + - Does not yet support Russian or other locales beyond lv/en. + +owners: + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: + status: draft + +tests: + - name: link-down-with-eta + description: | + Edge link down with an ETA of 30 minutes. Both bodies should + acknowledge the outage and surface the ETA. + inputs: + case_id: "33333333-3333-3333-3333-333333333333" + locale: "auto" + what_happened: "Tīkla pārtraukums rtr-r1" + eta_minutes: 30 + expected_outputs: + locale: "lv" + subject_lv: "[LVRTC] Informējam par incidentu" + subject_en: "[LVRTC] Incident notification" + - name: customer-request-no-eta + description: | + Customer-initiated request acknowledgement without an ETA. + inputs: + case_id: "44444444-4444-4444-4444-444444444444" + locale: "auto" + what_happened: "Klienta pieprasījums par joslas platumu" + expected_outputs: + locale: "lv" + subject_lv: "[LVRTC] Informējam par incidentu" + subject_en: "[LVRTC] Incident notification" diff --git a/algorithms/emit_event.card.yaml b/algorithms/emit_event.card.yaml new file mode 100644 index 0000000..93afb71 --- /dev/null +++ b/algorithms/emit_event.card.yaml @@ -0,0 +1,120 @@ +kind: uapf.algorithm.card +id: algo.incident_triage.emit_event +version: 1.0.0 +name: Case event emitter +intent: | + Appends a CaseEvent row to the case timeline. This is the canonical + way the BPMN signals "I completed a step" to the operator UI and to + the audit pipeline. Each event ends up as one row in case_events and + is also a candidate for VC signing (the VeriDocs SDK wraps a subset + of event types as Verifiable Credentials in Phase 1). +algorithm_kind: emitter + +io: + inputs: + - id: case_id + type: string + cardinality: single + constraints: + pattern: "^[0-9a-fA-F-]{36}$" + - id: type + type: string + constraints: + enum: + - signal_attached + - status_changed + - triaged + - classified + - prioritized + - routed + - assigned + - ai_decision_recorded + - dmn_evaluated + - comment_added + - escalated + - resolved + - closed + documentation: Canonical event type. Used by the timeline filter pills in the operator UI. + - id: payload + type: object + documentation: | + Type-specific payload. For 'routed': {classification, priority, + ownership, group_slug}. For 'classified': {taxonomy_code, + confidence}. The schema per type is documented in the OpenITSM + case-events module. + - id: actor_label + type: string + documentation: | + Free-form actor identifier. For UAPF-driven events this is + typically 'uapf:lv.itsm.incident.triage'. For operator actions + it's 'operator:'. + outputs: + - id: event_id + type: string + documentation: UUID of the new CaseEvent row. + - id: recorded_at + type: string + documentation: ISO-8601 timestamp of the row insert (server clock). + +implementation: + type: external + medium: mcp_tool + uri: uapf-ip://capability/event.emit@1 + hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 + runtime: + capability: event.emit@1 + note: | + Host-fulfilled UAPF-IP capability. Append-only — the host's + CaseEvent table has no UPDATE or DELETE paths exposed to UAPF + callers. The same row may later have a VC reference attached + by the VeriDocs SDK pipeline. + +determinism: deterministic +side_effects: writes_state +complexity: + typical_latency_ms: 8 + max_latency_ms: 2000 +failure_mode: | + Throws on unknown event type or missing case_id. Caller in the + triage BPMN treats failure as soft — the case still ends in its + decided state, just without the closing 'routed' marker. + +owners: + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: + status: draft + +tests: + - name: routed-event + description: | + Standard 'routed' event at the end of triage. Payload echoes the + classification, priority, ownership, group_slug decided upstream. + inputs: + case_id: "99999999-9999-9999-9999-999999999999" + type: "routed" + payload: + classification: "security.incident" + priority: "P1" + ownership: "lvrtc" + group_slug: "soc-l2" + actor_label: "uapf:lv.itsm.incident.triage" + expected_outputs: + recorded_at: "any-iso-timestamp" + - name: ai-decision-recorded + description: | + AI decision recording — payload carries the AIDecision row id so + operators can click through to the proposal. + inputs: + case_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + type: "ai_decision_recorded" + payload: + capability: "ai.draft_response" + decision_id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + confidence: 0.7 + requires_human_approval: true + actor_label: "uapf:lv.itsm.incident.triage" + expected_outputs: + recorded_at: "any-iso-timestamp" diff --git a/algorithms/evaluate_dmn.card.yaml b/algorithms/evaluate_dmn.card.yaml new file mode 100644 index 0000000..9d72858 --- /dev/null +++ b/algorithms/evaluate_dmn.card.yaml @@ -0,0 +1,111 @@ +kind: uapf.algorithm.card +id: algo.incident_triage.evaluate_dmn +version: 1.0.0 +name: DMN decision evaluator +intent: | + Wraps a DMN 1.3 decision-table evaluation as a callable UAPF-IP + capability. The triage BPMN invokes the three DMN tables in this + package (priority, ownership, routing) natively via businessRuleTask; + this card describes the dmn.evaluate@1 capability for cases where the + same evaluation is needed outside the BPMN runtime (operator manual + re-evaluation, batch backfill, regression testing). + + Engine-internal DMN evaluation and this capability call return the + same output for the same input — that property is the only thing + this card asserts. +algorithm_kind: rule_table + +io: + inputs: + - id: package_id + type: string + documentation: UAPF package id whose dmn/ cornerstone holds the table. + - id: decision_id + type: string + constraints: + enum: [priority, ownership, routing] + documentation: Which DMN decision to evaluate in this package. + - id: inputs + type: object + documentation: | + Decision-input columns as a flat object. For priority: + {severity, service_tier, ai_suggested_priority, classification}. + For ownership: {classification, host_domain, source}. + For routing: {classification, priority, ownership}. + outputs: + - id: output + type: object + documentation: | + Object with the decision-output columns. priority -> {priority}. + ownership -> {ownership}. routing -> {group_slug}. + - id: hit_rule_ids + type: array + documentation: Rule ids that matched (FIRST hit-policy produces 1; ANY may produce N). + - id: hit_policy + type: string + constraints: + enum: [FIRST, UNIQUE, ANY, PRIORITY, OUTPUT_ORDER, COLLECT, RULE_ORDER] + +implementation: + type: external + medium: mcp_tool + uri: uapf-ip://capability/dmn.evaluate@1 + hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 + runtime: + capability: dmn.evaluate@1 + note: | + Host-fulfilled UAPF-IP capability. The OpenITSM host reads the + DMN file from the package's dmn/ cornerstone and applies the + DMN 1.3 hit-policy semantics. Same decision artifact, same + output as the in-process engine evaluation. + +determinism: deterministic +side_effects: pure +complexity: + typical_latency_ms: 5 + max_latency_ms: 5000 +failure_mode: | + Throws if the requested package_id is not loaded by the runtime, or + if the named decision_id is not present in that package's dmn/ + directory. Triage callers fall back to safe defaults (P4, lvrtc, + helpdesk-l1) so the case still completes. + +owners: + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: + status: draft + +tests: + - name: priority-critical-tier1 + description: | + The priority table should rule P1 for a critical+tier_1 input + regardless of AI suggestion. + inputs: + package_id: "lv.itsm.incident.triage" + decision_id: "priority" + inputs: + severity: "critical" + service_tier: "tier_1" + ai_suggested_priority: "P3" + classification: "network.outage.link_down" + expected_outputs: + output: + priority: "P1" + hit_policy: "FIRST" + - name: routing-security-to-soc + description: | + Routing of a security.incident at P1 should land on soc-l2. + inputs: + package_id: "lv.itsm.incident.triage" + decision_id: "routing" + inputs: + classification: "security.incident" + priority: "P1" + ownership: "lvrtc" + expected_outputs: + output: + group_slug: "soc-l2" + hit_policy: "FIRST" diff --git a/algorithms/normalize_signal.card.yaml b/algorithms/normalize_signal.card.yaml new file mode 100644 index 0000000..8013432 --- /dev/null +++ b/algorithms/normalize_signal.card.yaml @@ -0,0 +1,91 @@ +kind: uapf.algorithm.card +id: algo.incident_triage.normalize_signal +version: 1.0.0 +name: Signal normalizer +intent: | + Reads a freshly-received Signal row by id and folds the + source-specific payload (Zabbix event, IMAP email, Jira DC issue, + manual entry) into a single normalised shape with the fields + downstream cards expect: title, description, host, severity, source + hint, optional contact metadata, and a content-hash for dedupe. + Idempotent; safe to re-run. +algorithm_kind: transformer + +io: + inputs: + - id: signal_id + type: string + cardinality: single + constraints: + pattern: "^[0-9a-fA-F-]{36}$" + documentation: UUID of the Signal row to normalise. + outputs: + - id: normalized_payload + type: object + documentation: | + Adapter-agnostic dictionary. Stable keys: title, description, + host, severity (one of disaster|high|critical|average|warning|low|info), + source, source_event_id, optional received_at, optional contact_email. + - id: dedupe_hash + type: string + documentation: SHA-256 hex over (source, source_event_id, host) used to suppress repeat signals. + - id: source_kind + type: string + documentation: One of zabbix|email|jira_dc|jira_cloud|manual|cti. + +implementation: + type: external + medium: mcp_tool + uri: uapf-ip://capability/intake.normalize@1 + hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 + runtime: + capability: intake.normalize@1 + note: | + Host-fulfilled UAPF-IP capability. The OpenITSM host's + intake.normalize handler implements one normaliser per source. + Hash is a placeholder until the runtime publishes the + implementation hash. + +determinism: deterministic +side_effects: pure +complexity: + typical_latency_ms: 30 + max_latency_ms: 2000 +failure_mode: | + Returns source_kind='unknown' with whatever raw_payload was available + on the signal row. Triage continues with best-effort classification. + +owners: + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: + status: draft + +tests: + - name: zabbix-link-down + description: | + Zabbix event for a transport-link outage on an edge router. + Title, host and severity are normalised from the raw event shape. + inputs: + signal_id: "11111111-1111-1111-1111-111111111111" + expected_outputs: + source_kind: "zabbix" + normalized_payload: + title: "Link down on edge router rtr-r1" + host: "rtr-r1.lvrtc.lv" + severity: "high" + source: "zabbix" + - name: email-customer-lv + description: | + Customer-facing Latvian-language email about a bandwidth uplift + request. Subject becomes title; body becomes description. + inputs: + signal_id: "22222222-2222-2222-2222-222222222222" + expected_outputs: + source_kind: "email" + normalized_payload: + title: "Klients SIA Latvija Tev: lūgums palielināt joslas platumu" + severity: "average" + source: "email" diff --git a/algorithms/suggest_priority.card.yaml b/algorithms/suggest_priority.card.yaml new file mode 100644 index 0000000..b3bee9e --- /dev/null +++ b/algorithms/suggest_priority.card.yaml @@ -0,0 +1,98 @@ +kind: uapf.algorithm.card +id: algo.incident_triage.suggest_priority +version: 1.0.0 +name: Priority suggester +intent: | + Reads the classified incident plus its service-tier and reported + severity and proposes a priority on the P1..P4 scale. This output is + a SOFT suggestion only — the priority DMN table makes the binding + decision, with this suggestion as one of its four input columns. + Separating "AI suggestion" from "binding decision" keeps the AI + contestable and the auditor's job tractable. +algorithm_kind: classifier + +io: + inputs: + - id: severity + type: string + constraints: + enum: [disaster, critical, high, average, warning, low, info] + documentation: Source-reported severity (Zabbix verbatim; normalised for other adapters). + - id: service_tier + type: string + constraints: + enum: [tier_1, tier_2, best_effort] + documentation: Service tier of the affected service (from the connection or host catalog). + - id: classification + type: string + documentation: | + Taxonomy code from ai.classify@1. The suggester applies a fixed + elevation for security.incident regardless of severity. + outputs: + - id: priority + type: string + constraints: + enum: [P1, P2, P3, P4] + - id: reason + type: string + documentation: One-sentence justification (English). Visible to operator. + +implementation: + type: external + medium: mcp_tool + uri: uapf-ip://capability/ai.suggest_priority@1 + hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 + runtime: + capability: ai.suggest_priority@1 + note: | + Host-fulfilled UAPF-IP capability. The OpenITSM host can answer + via either the LLM gateway or a deterministic rule. The DMN + priority table downstream applies a binding decision on top. + +determinism: stochastic +side_effects: pure +complexity: + typical_latency_ms: 600 + max_latency_ms: 30000 +failure_mode: | + Returns priority='P4' with reason='LLM unavailable, defaulted'. The + binding DMN can still raise the priority based on severity x tier. + +owners: + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: + status: draft + +tests: + - name: disaster-on-tier1-is-p1 + description: | + Disaster severity on a tier_1 service is always P1 regardless of + classification. + inputs: + severity: "disaster" + service_tier: "tier_1" + classification: "network.outage.link_down" + expected_outputs: + priority: "P1" + - name: security-incident-elevates + description: | + Security incidents elevate to P1 regardless of severity field. + inputs: + severity: "average" + service_tier: "tier_2" + classification: "security.incident" + expected_outputs: + priority: "P1" + - name: customer-request-low + description: | + A customer service request on best_effort tier is P4 unless + escalated by an operator. + inputs: + severity: "info" + service_tier: "best_effort" + classification: "service.customer_request" + expected_outputs: + priority: "P4" diff --git a/algorithms/update_incident.card.yaml b/algorithms/update_incident.card.yaml new file mode 100644 index 0000000..927e938 --- /dev/null +++ b/algorithms/update_incident.card.yaml @@ -0,0 +1,122 @@ +kind: uapf.algorithm.card +id: algo.incident_triage.update_incident +version: 1.0.0 +name: Incident updater +intent: | + Applies a patch to the Case row (priority, ownership, assigned_group_id, + taxonomy_code) and optionally transitions the FSM (open -> triaged, + triaged -> in_progress, etc). Every transition emits a CaseEvent so + the timeline reflects the change. Refuses illegal transitions per the + state machine in OpenITSM's services/incident.py module — the engine + treats the resulting error as a hard failure. +algorithm_kind: emitter + +io: + inputs: + - id: case_id + type: string + cardinality: single + constraints: + pattern: "^[0-9a-fA-F-]{36}$" + - id: patch + type: object + documentation: | + Subset of writable case fields. Allowed keys: priority, + ownership, assigned_group_id, taxonomy_code, sla_breached, + operator_notes. Unknown keys are rejected. + - id: status + type: string + constraints: + enum: [open, triaged, in_progress, waiting_customer, waiting_third_party, resolved, closed, cancelled] + documentation: | + Optional target status. If absent, only field updates are applied. + If present, an FSM transition is attempted; illegal transitions + raise. + - id: reason + type: string + documentation: Human-readable reason recorded on the CaseEvent. + outputs: + - id: case_id + type: string + - id: new_status + type: string + documentation: The case status after the call (unchanged if status input was absent). + - id: success + type: boolean + - id: event_ids + type: array + documentation: CaseEvent row ids emitted by this call. + +implementation: + type: external + medium: mcp_tool + uri: uapf-ip://capability/incident.update@1 + hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 + runtime: + capability: incident.update@1 + note: | + Host-fulfilled UAPF-IP capability. The OpenITSM host validates + the patch against the writable-field allowlist and runs FSM + transitions through services/incident.transition_case. All + mutations happen in a single DB transaction with the emitted + CaseEvent rows. + +determinism: deterministic +side_effects: writes_state +complexity: + typical_latency_ms: 25 + max_latency_ms: 5000 +failure_mode: | + Illegal FSM transition or unknown patch key throws and the entire + call is rolled back. Triage callers do NOT proceed past this step on + failure — the case stays at its prior status. + +reference: + legal: | + Latvian National Standard for ITSM (LVS EN ISO/IEC 20000-1). + standard: | + ITIL 4 — Incident Management state machine. + +owners: + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: + status: draft + +tests: + - name: open-to-triaged-with-priority + description: | + Open case gets a priority + assigned group and transitions to triaged. + inputs: + case_id: "55555555-5555-5555-5555-555555555555" + patch: + priority: "P1" + ownership: "lvrtc" + assigned_group_id: "66666666-6666-6666-6666-666666666666" + status: "triaged" + reason: "Auto-triaged by lv.itsm.incident.triage v1.0.0" + expected_outputs: + success: true + new_status: "triaged" + - name: patch-only-no-transition + description: | + Patch without a status input updates fields but leaves the FSM + state alone. + inputs: + case_id: "77777777-7777-7777-7777-777777777777" + patch: + operator_notes: "Customer called to follow up" + reason: "Operator note added" + expected_outputs: + success: true + - name: illegal-transition-throws + description: | + Closed cases cannot return to in_progress; the capability fails. + inputs: + case_id: "88888888-8888-8888-8888-888888888888" + status: "in_progress" + reason: "Attempting to reopen a closed case" + expected_outputs: + success: false diff --git a/bpmn/incident-triage.bpmn b/bpmn/incident-triage.bpmn new file mode 100644 index 0000000..916ff30 --- /dev/null +++ b/bpmn/incident-triage.bpmn @@ -0,0 +1,274 @@ + + + + + Linear UAPF process invoked by OpenITSM intake. Each step is a host-fulfilled + UAPF-IP capability call governed by an Algorithm Card under algorithms/. + The three dmn.evaluate@1 invocations read priority.dmn / ownership.dmn / + routing.dmn from the package's dmn/ cornerstone. + + + Flow_01 + + + Reads the freshly-received Signal row and folds the source-specific payload (Zabbix / IMAP / Jira / manual) into a uniform shape downstream cards expect. + Flow_01 + Flow_02 + + + + + + + Task_NormalizeSignal_in_signal_id + + + Task_NormalizeSignal_out_normalized_payload + Task_NormalizeSignal_out_dedupe_hash + Task_NormalizeSignal_out_source_kind + + + + + Picks one taxonomy code from a closed list. LLM-backed at runtime with deterministic regex fallback. + Flow_02 + Flow_03 + + + + + + + + + Task_ClassifyIncident_in_payload + Task_ClassifyIncident_in_text + + + Task_ClassifyIncident_out_taxonomy_code + Task_ClassifyIncident_out_confidence + Task_ClassifyIncident_out_reasoning + Task_ClassifyIncident_out_label_hint + + + + + Soft P1..P4 suggestion from severity x service_tier x classification. The downstream DMN makes the binding decision. + Flow_03 + Flow_04 + + + + + + + + Task_SuggestPriority_in_severity + Task_SuggestPriority_in_service_tier + Task_SuggestPriority_in_classification + + + Task_SuggestPriority_out_priority + Task_SuggestPriority_out_reason + + + + + Binding priority decision: FIRST-hit DMN over severity, service_tier, ai_suggested_priority, classification. + Flow_04 + Flow_05 + + + + + + + + + Task_EvaluatePriorityDmn_in_package_id + Task_EvaluatePriorityDmn_in_decision_id + Task_EvaluatePriorityDmn_in_inputs + + + Task_EvaluatePriorityDmn_out_output + Task_EvaluatePriorityDmn_out_hit_rule_ids + Task_EvaluatePriorityDmn_out_hit_policy + + + + + Binding ownership decision: stay inside LVRTC or hand off externally based on classification + host_domain + source. + Flow_05 + Flow_06 + + + + + + + + + Task_EvaluateOwnershipDmn_in_package_id + Task_EvaluateOwnershipDmn_in_decision_id + Task_EvaluateOwnershipDmn_in_inputs + + + Task_EvaluateOwnershipDmn_out_output + Task_EvaluateOwnershipDmn_out_hit_rule_ids + Task_EvaluateOwnershipDmn_out_hit_policy + + + + + Picks the expert group (helpdesk-l1, noc-l1/l2, soc-l2, facility-l2, platform-l2, external-handoff) from classification + priority + ownership. + Flow_06 + Flow_07 + + + + + + + + + Task_EvaluateRoutingDmn_in_package_id + Task_EvaluateRoutingDmn_in_decision_id + Task_EvaluateRoutingDmn_in_inputs + + + Task_EvaluateRoutingDmn_out_output + Task_EvaluateRoutingDmn_out_hit_rule_ids + Task_EvaluateRoutingDmn_out_hit_policy + + + + + Applies the decided priority + ownership + assigned_group_id and transitions open -> triaged. All mutations in one DB transaction. + Flow_07 + Flow_08 + + + + + + + + + + + Task_UpdateIncident_in_case_id + Task_UpdateIncident_in_patch + Task_UpdateIncident_in_status + Task_UpdateIncident_in_reason + + + Task_UpdateIncident_out_case_id + Task_UpdateIncident_out_new_status + Task_UpdateIncident_out_success + Task_UpdateIncident_out_event_ids + + + + + Drafts a parallel Latvian + English customer notification. Produces an AIDecision row with requires_human_approval=true. Operator must approve before any outbound transport adapter runs. + Flow_08 + Flow_09 + + + + + + + + + + + + Task_DraftResponse_in_case_id + Task_DraftResponse_in_locale + Task_DraftResponse_in_what_happened + Task_DraftResponse_in_eta_minutes + + + Task_DraftResponse_out_subject_lv + Task_DraftResponse_out_subject_en + Task_DraftResponse_out_body_lv + Task_DraftResponse_out_body_en + Task_DraftResponse_out_locale + + + + + Emits the closing 'routed' CaseEvent. Payload echoes classification, priority, ownership, group_slug for the timeline. + Flow_09 + Flow_10 + + + + + + + + + Task_EmitEvent_in_case_id + Task_EmitEvent_in_type + Task_EmitEvent_in_payload + Task_EmitEvent_in_actor_label + + + Task_EmitEvent_out_event_id + Task_EmitEvent_out_recorded_at + + + + + Flow_10 + + + + + + + + + + + + + diff --git a/dmn/ownership.dmn b/dmn/ownership.dmn new file mode 100644 index 0000000..6e78af9 --- /dev/null +++ b/dmn/ownership.dmn @@ -0,0 +1,142 @@ + + + + Determine whether LVRTC owns this incident or it should be transferred. + + + + + + + classification + + + + + host_domain + + + + + source + + + + + + + "security.incident" + + + - + + + - + + + "lvrtc" + + + "Security incidents owned by LVRTC SOC" + + + + + "facility.power" + + + - + + + - + + + "lvrtc" + + + "Facility / power is LVRTC's" + + + + + "service.customer_request" + + + - + + + - + + + "lvrtc" + + + "Customer requests are LVRTC's intake" + + + + + "unknown.uncategorized" + + + - + + + "phone" + + + "lvrtc" + + + "Phone intake stays with LVRTC L1" + + + + + - + + + "lvrtc.lv" + + + - + + + "lvrtc" + + + "Anything on lvrtc.lv hosts is ours" + + + + + - + + + - + + + - + + + "lvrtc" + + + "Default: LVRTC owns until escalation transfers" + + + + + diff --git a/dmn/priority.dmn b/dmn/priority.dmn new file mode 100644 index 0000000..9acac22 --- /dev/null +++ b/dmn/priority.dmn @@ -0,0 +1,326 @@ + + + + Determine the final P1..P4 priority from severity, service tier, and AI + suggestion. FIRST matching rule wins. + + + + + + + severity + + + + + service_tier + + + + + ai_suggested_priority + + + + + classification + + + + + + + "disaster" + + + - + + + - + + + - + + + "P1" + + + "Disaster severity always overrides" + + + + + "critical" + + + - + + + - + + + - + + + "P1" + + + "Critical severity always overrides" + + + + + - + + + - + + + - + + + "security.incident" + + + "P1" + + + "Security incidents are always P1" + + + + + "high" + + + "tier_1" + + + - + + + - + + + "P1" + + + "High on tier_1 service" + + + + + "high" + + + "tier_2" + + + - + + + - + + + "P2" + + + "High on tier_2 service" + + + + + "high" + + + - + + + - + + + - + + + "P2" + + + "High severity, lower tier" + + + + + "average" + + + "tier_1" + + + - + + + - + + + "P2" + + + "Average on tier_1 service" + + + + + "average" + + + "tier_2" + + + - + + + - + + + "P3" + + + "Average on tier_2 service" + + + + + "warning" + + + - + + + - + + + - + + + "P3" + + + "Warning severity" + + + + + "information" + + + - + + + - + + + - + + + "P4" + + + "Informational" + + + + + - + + + - + + + "P1" + + + - + + + "P1" + + + "Fallback: trust AI suggestion P1" + + + + + - + + + - + + + "P2" + + + - + + + "P2" + + + "Fallback: trust AI suggestion P2" + + + + + - + + + - + + + "P3" + + + - + + + "P3" + + + "Fallback: trust AI suggestion P3" + + + + + - + + + - + + + - + + + - + + + "P4" + + + "Default when nothing else matches" + + + + + diff --git a/dmn/routing.dmn b/dmn/routing.dmn new file mode 100644 index 0000000..d593667 --- /dev/null +++ b/dmn/routing.dmn @@ -0,0 +1,278 @@ + + + + Route to an expert group slug based on classification + priority. FIRST hit. + + + + + + + classification + + + + + priority + + + + + ownership + + + + + + + "security.incident" + + + - + + + "lvrtc" + + + "soc-l2" + + + "Security goes to SOC L2" + + + + + "network.outage.link_down" + + + "P1" + + + "lvrtc" + + + "noc-l2" + + + "Network P1 to NOC L2" + + + + + "network.outage.link_down" + + + - + + + "lvrtc" + + + "noc-l1" + + + "Network non-P1 to NOC L1" + + + + + "network.degradation" + + + "P1" + + + "lvrtc" + + + "noc-l2" + + + "Network degradation P1 to NOC L2" + + + + + "network.degradation" + + + - + + + "lvrtc" + + + "noc-l1" + + + "Network degradation lower to NOC L1" + + + + + "network.routing" + + + - + + + "lvrtc" + + + "noc-l2" + + + "Routing issues need L2" + + + + + "network.dns" + + + - + + + "lvrtc" + + + "noc-l2" + + + "DNS issues to NOC L2" + + + + + "facility.power" + + + - + + + "lvrtc" + + + "facility-l2" + + + "Power to facility ops" + + + + + "storage.capacity" + + + - + + + "lvrtc" + + + "platform-l2" + + + "Storage to platform ops" + + + + + "service.customer_request" + + + - + + + "lvrtc" + + + "helpdesk-l1" + + + "Customer requests to L1 helpdesk" + + + + + - + + + "P1" + + + "lvrtc" + + + "noc-l2" + + + "Default P1: NOC L2" + + + + + - + + + - + + + "lvrtc" + + + "helpdesk-l1" + + + "Default: L1 helpdesk triages" + + + + + - + + + - + + + "not_lvrtc" + + + "external-handoff" + + + "Not ours - external handoff" + + + + + - + + + - + + + - + + + "helpdesk-l1" + + + "Final default" + + + + + diff --git a/docs/host-lookup-tables.md b/docs/host-lookup-tables.md new file mode 100644 index 0000000..ee86eef --- /dev/null +++ b/docs/host-lookup-tables.md @@ -0,0 +1,49 @@ +# Host lookup tables + +This package expects the OpenITSM host to maintain three deployment-specific +lookup tables. They are NOT part of the package (the package only defines the +abstract triage process); each host deployment populates them. + +## Expert groups + +The routing DMN resolves to one of these `group_slug` values. The host MUST +have a matching `expert_groups` row for each: + +| slug | name | +|-------------------|-------------------| +| `helpdesk-l1` | L1 helpdesk | +| `noc-l1` | Network Ops L1 | +| `noc-l2` | Network Ops L2 | +| `soc-l2` | Security Ops L2 | +| `facility-l2` | Facility & Power L2 | +| `platform-l2` | Platform Ops L2 | +| `external-handoff`| External handoff | + +## Service tiers + +The priority DMN consumes these tier codes. Hosts MUST have a matching +`service_tiers` row. + +| code | name | first_response | resolution | +|---------------|-------------------------------|----------------|------------| +| `tier_1` | Tier 1 (mission-critical) | 15 min | 240 min | +| `tier_2` | Tier 2 (standard) | 60 min | 480 min | +| `best_effort` | Best effort (non-SLA) | — (no SLA) | — | + +## Taxonomy + +The closed list of taxonomy codes `ai.classify@1` can emit, mirrored in the +classify_incident Algorithm Card's `io.outputs.taxonomy_code.constraints.enum` +and consumed as input to the priority + routing DMN. + +| code | LV | EN | +|-------------------------------|--------------------------|---------------------| +| `network.outage.link_down` | Saites pārtraukums | Link down | +| `network.degradation` | Tīkla degradācija | Network degradation | +| `network.routing` | Maršrutēšana | Routing | +| `network.dns` | DNS | DNS | +| `security.incident` | Drošības incidents | Security incident | +| `facility.power` | Elektroapgāde | Power / facility | +| `storage.capacity` | Diska vieta | Storage capacity | +| `service.customer_request` | Klienta pieprasījums | Customer request | +| `unknown.uncategorized` | Neklasificēts | Uncategorized | diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..52bb9b6 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,60 @@ +# `lv.itsm.incident.triage` — overview + +Level-4 UAPF process for triaging IT-infrastructure incidents at LVRTC. + +## What it does + +A new Signal lands in OpenITSM (Zabbix webhook, IMAP poll, Jira DC webhook, +manual entry). The host opens a Case and starts a session of this package +against the UAPF engine. The engine then drives the linear flow defined in +`bpmn/incident-triage.bpmn`, calling back to OpenITSM at every step: + +1. **Normalize** the source-specific payload to a uniform shape. +2. **Classify** into one of nine taxonomy codes (LLM + regex fallback). +3. **Suggest** a soft P1..P4 priority. +4. **Evaluate** the three DMN tables in order: priority (binding), + ownership (LVRTC vs external), routing (which expert group). +5. **Update** the case with the decided priority + group + status=triaged. +6. **Draft** a parallel Latvian + English customer notification — flagged + PROPOSED, requires operator approval before send. +7. **Emit** the closing `routed` CaseEvent. + +## Why the split + +Classification, priority suggestion, and customer-response drafting are +the AI steps. Everything *binding* — the actual priority, who handles it, +which group — lives in versioned DMN, not in Python. That keeps the AI +contestable and the auditor's job tractable: an evaluator can read +`dmn/priority.dmn` and know exactly what priority an incident *will* +receive, given its severity and tier, without running anything. + +## Cornerstones + +- **bpmn/** — `incident-triage.bpmn` — 1 process, 9 service tasks, linear. +- **dmn/** — `priority.dmn` (14 rules), `ownership.dmn` (6 rules), + `routing.dmn` (14 rules), all FIRST hit-policy. +- **algorithms/** — 7 algorithm cards, each with embedded v2.5.0 tests. +- **resources/** — guardrails (PII, approval gating, timeouts, retention) + and host mappings (expert groups, service tiers, taxonomy). +- **metadata/** — lifecycle + ownership. + +## Versioning + +This package targets **UAPF v2.5.0** (track main, refreshed on every +schema release). Breaking changes follow the spec's CHANGELOG. + +## Host requirements + +OpenITSM must implement and advertise (via `/uapf/host/manifest`) the +seven UAPF-IP capabilities listed in `requires_capabilities`: + +- `intake.normalize@1` +- `ai.classify@1` +- `ai.suggest_priority@1` +- `ai.draft_response@1` +- `dmn.evaluate@1` +- `incident.update@1` +- `event.emit@1` + +The first six are intent-bearing (each governed by its own Algorithm +Card); `event.emit` is an append-only timeline writer. diff --git a/fixtures/signal-email-customer-lv.json b/fixtures/signal-email-customer-lv.json new file mode 100644 index 0000000..228d169 --- /dev/null +++ b/fixtures/signal-email-customer-lv.json @@ -0,0 +1,16 @@ +{ + "source": "email", + "external_id": "MSG-2026-05-25-001", + "raw_payload": { + "subject": "Klients SIA Latvija Tev: lūgums palielināt joslas platumu", + "from": "ivars.berzins@klients-latvija.lv", + "to": "atbalsts@lvrtc.lv", + "body": "Labdien, mūsu uzņēmumam nepieciešams palielināt internet pieslēguma joslas platumu no 100 Mbps uz 500 Mbps. Lūdzu, informējiet par procedūru un izmaksām. Ar cieņu, Ivars Bērziņš" + }, + "expected_after_triage": { + "taxonomy_code": "service.customer_request", + "priority": "P3", + "ownership": "lvrtc", + "group_slug": "helpdesk-l1" + } +} diff --git a/fixtures/signal-zabbix-ddos.json b/fixtures/signal-zabbix-ddos.json new file mode 100644 index 0000000..bf7c479 --- /dev/null +++ b/fixtures/signal-zabbix-ddos.json @@ -0,0 +1,17 @@ +{ + "source": "zabbix", + "external_id": "ZBX-EVT-9374", + "raw_payload": { + "title": "DDoS attack pattern detected on edge", + "host": "rtr-r3.lvrtc.lv", + "severity": "critical", + "body": "Volumetric UDP flood, 4.2 Gbps inbound to 192.0.2.0/24. Source: 12 ASNs, predominantly AS197207. Auto-mitigation engaged.", + "tags": ["security", "ddos"] + }, + "expected_after_triage": { + "taxonomy_code": "security.incident", + "priority": "P1", + "ownership": "lvrtc", + "group_slug": "soc-l2" + } +} diff --git a/fixtures/signal-zabbix-link-down.json b/fixtures/signal-zabbix-link-down.json new file mode 100644 index 0000000..779b8eb --- /dev/null +++ b/fixtures/signal-zabbix-link-down.json @@ -0,0 +1,18 @@ +{ + "source": "zabbix", + "external_id": "ZBX-EVT-9342", + "raw_payload": { + "title": "Link down on edge router rtr-r1", + "host": "rtr-r1.lvrtc.lv", + "severity": "high", + "body": "Interface ge-0/0/3 went DOWN at 09:14:02 UTC. BGP session to upstream provider AS6939 also flapping.", + "event_id": "9342", + "tags": ["network", "edge"] + }, + "expected_after_triage": { + "taxonomy_code": "network.routing", + "priority": "P1", + "ownership": "lvrtc", + "group_slug": "noc-l2" + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..cc2de14 --- /dev/null +++ b/manifest.json @@ -0,0 +1,63 @@ +{ + "kind": "uapf.package", + "id": "lv.itsm.incident.triage", + "name": "LVRTC Incident Triage", + "description": "Level-4 UAPF process for triaging IT-infrastructure incidents at LVRTC.\n\nSix BPMN service tasks invoke the UAPF-IP capabilities intake.normalize@1,\nai.classify@1, ai.suggest_priority@1, ai.draft_response@1,\nincident.update@1 and event.emit@1. Three DMN decision tables encode the\ndeterministic rules previously hidden inside the host: priority maps\nseverity x service-tier x AI-suggestion x classification onto P1-P4;\nownership decides whether the case stays inside LVRTC or hands off to an\nexternal partner; routing picks the expert group (helpdesk-l1, noc-l1/l2,\nsoc-l2, facility-l2, platform-l2, external-handoff).\n\nClassification, priority suggestion and customer response drafting are AI\nsteps; the rules that decide *what* the AI proposes versus *who* handles\nit live in versioned DMN, not Python. Drafted customer responses are\nproduced in both Latvian and English and require human approval before\nsending (governed by Algorithm Card algo.incident_triage.draft_response).\n\nv1.0.0 targets UAPF v2.5.0: algorithm cards carry embedded `tests`\narrays (>=2 per card) per chapter 13.16; BPMN service tasks carry\nuapf24:algorithmCardRef attributes; resource targets are dispatch\nendpoints only.\n", + "level": 4, + "version": "1.0.0", + "requires_capabilities": [ + "intake.normalize@1+", + "ai.classify@1+", + "ai.suggest_priority@1+", + "ai.draft_response@1+", + "dmn.evaluate@1+", + "incident.update@1+", + "event.emit@1+" + ], + "profiles_supported": [ + "uapf-ip-orchestrated" + ], + "guardrails": "resources/guardrails.yaml", + "includes": [], + "dependencies": {}, + "cornerstones": { + "bpmn": true, + "dmn": true, + "cmmn": false, + "resources": true + }, + "paths": { + "bpmn": "bpmn", + "dmn": "dmn", + "resources": "resources", + "metadata": "metadata" + }, + "exposure": { + "mcp": { + "enabled": true, + "runnable": true, + "exposedEntrypoints": [ + "Process_IncidentTriage" + ], + "exposedArtifacts": [ + "manifest", + "bpmn", + "dmn", + "docs" + ] + } + }, + "owners": [ + { + "type": "team", + "id": "lvrtc", + "contact": "incident-mgmt@lvrtc.lv" + }, + { + "type": "team", + "id": "openitsm-stewards", + "contact": "stewards@openitsm.algomation.io" + } + ], + "lifecycle": "draft" +} diff --git a/metadata/lifecycle.yaml b/metadata/lifecycle.yaml new file mode 100644 index 0000000..aece5d1 --- /dev/null +++ b/metadata/lifecycle.yaml @@ -0,0 +1,16 @@ +kind: uapf.metadata.lifecycle +status: draft +created: "2026-06-01T00:00:00Z" +lastModified: "2026-06-01T00:00:00Z" +changeHistory: + - version: "1.0.0" + date: "2026-06-01" + summary: | + Initial release. Six BPMN service tasks invoke UAPF-IP capabilities + (intake.normalize, ai.classify, ai.suggest_priority, ai.draft_response, + incident.update, event.emit). Three DMN decision tables encode the + priority / ownership / routing rules previously hidden in the + OpenITSM host's inline triage runner. Seven algorithm cards (the + six service-task cards plus dmn.evaluate as a host-fulfilled + capability for governance) carry embedded tests per UAPF v2.5.0. + author: "openitsm-stewards" diff --git a/metadata/ownership.yaml b/metadata/ownership.yaml new file mode 100644 index 0000000..929111a --- /dev/null +++ b/metadata/ownership.yaml @@ -0,0 +1,19 @@ +kind: uapf.metadata.ownership +owners: + - type: team + id: lvrtc + name: VAS Latvijas Valsts Radio un Televizijas Centrs + contact: incident-mgmt@lvrtc.lv + role: owner + - type: team + id: openitsm-stewards + name: OpenITSM Package Stewards + contact: stewards@openitsm.algomation.io + role: maintainer +approvers: + - lvrtc-incident-board + - openitsm-stewards +escalation: + level_1: + team: lvrtc-incident-board + contact: incident-board@lvrtc.lv diff --git a/processgit.mcp.yaml b/processgit.mcp.yaml new file mode 100644 index 0000000..e68ee47 --- /dev/null +++ b/processgit.mcp.yaml @@ -0,0 +1,15 @@ +# Hints for ProcessGit's MCP indexer about this package. +kind: processgit.mcp.hints +package_id: lv.itsm.incident.triage +description: LVRTC incident triage — UAPF v2.5.0 package +tags: + - itsm + - incident-management + - lvrtc + - openitsm + - critical-infrastructure + - nis2 +locale_primary: lv +locales: + - lv + - en diff --git a/resources/guardrails.yaml b/resources/guardrails.yaml new file mode 100644 index 0000000..9b70ac6 --- /dev/null +++ b/resources/guardrails.yaml @@ -0,0 +1,37 @@ +kind: uapf.resources.guardrails + +# Applied by the UAPF runtime to every UAPF-IP capability invocation +# governed by this package. Cross-cutting safety rails — enforced regardless +# of which Algorithm Card the runtime is dispatching. + +pii: + redact_in_payloads: true + forbidden_in_drafts: + - personas_kods + - magnetic_stripe + - iban + allowed_in_drafts: + - case_number + - host_domain + - approximate_eta + +approval: + human_required_for: + - ai.draft_response # outbound customer text always reviewed + - incident.update # write actions never auto-applied + auto_applied: + - intake.normalize + - ai.classify + - ai.suggest_priority + - dmn.evaluate + - event.emit + +timeouts: + capability_default_ms: 30000 + llm_default_ms: 45000 + dmn_default_ms: 5000 + +retention: + algorithm_outputs_days: 365 + audit_events_days: 2557 # 7 years (Latvian state-archive default) + signed_artifacts: indefinite diff --git a/resources/mappings.yaml b/resources/mappings.yaml new file mode 100644 index 0000000..0572ecd --- /dev/null +++ b/resources/mappings.yaml @@ -0,0 +1,194 @@ +kind: uapf.resources.mapping + +# Host-readable contract for the seven capability-backed service tasks in +# this package. Algorithm Card references live on the BPMN service tasks +# themselves (uapf24:algorithmCardRef attribute) per UAPF v2.4.0+. The +# targets below are dispatch endpoints only. + +targets: + - id: agent.intake_normalizer + type: system_api + name: Signal normalizer + description: Host capability intake.normalize@1, governed by the normalize_signal Algorithm Card. + capabilities: + - capability.intake.normalize + + - id: agent.classifier + type: ai_agent + name: Incident taxonomy classifier + description: Host capability ai.classify@1 (LLM-backed), governed by the classify_incident Algorithm Card. + capabilities: + - capability.ai.classify + + - id: agent.priority_suggester + type: ai_agent + name: Priority suggester + description: Host capability ai.suggest_priority@1, governed by the suggest_priority Algorithm Card. + capabilities: + - capability.ai.suggest_priority + + - id: agent.dmn_evaluator + type: system_api + name: DMN decision evaluator + description: Host capability dmn.evaluate@1, governed by the evaluate_dmn Algorithm Card. Invoked three times in the BPMN with different decision_id input. + capabilities: + - capability.dmn.evaluate + + - id: agent.response_drafter + type: ai_agent + name: Customer response drafter + description: Host capability ai.draft_response@1, governed by the draft_response Algorithm Card. Requires human approval before send. + capabilities: + - capability.ai.draft_response + + - id: agent.incident_updater + type: system_api + name: Incident state writer + description: Host capability incident.update@1, governed by the update_incident Algorithm Card. Applies field patches and FSM transitions. + capabilities: + - capability.incident.update + + - id: agent.event_emitter + type: system_api + name: Case event emitter + description: Host capability event.emit@1, governed by the emit_event Algorithm Card. Append-only timeline writer. + capabilities: + - capability.event.emit + +bindings: + - source: { type: bpmn.serviceTask, ref: Task_NormalizeSignal } + targetId: agent.intake_normalizer + mode: autonomous + contract: + input: + - { name: signal_id, type: string, required: true } + output: + - { name: normalized_payload, type: object } + - { name: dedupe_hash, type: string } + - { name: source_kind, type: string } + timeout: "5s" + requiredCapabilities: [capability.intake.normalize] + + - source: { type: bpmn.serviceTask, ref: Task_ClassifyIncident } + targetId: agent.classifier + mode: autonomous + contract: + input: + - { name: payload, type: object, required: true } + - { name: text, type: string } + output: + - { name: taxonomy_code, type: string } + - { name: confidence, type: number, description: "0.0-1.0 model confidence" } + - { name: reasoning, type: string } + - { name: label_hint, type: string } + timeout: "30s" + requiredCapabilities: [capability.ai.classify] + + - source: { type: bpmn.serviceTask, ref: Task_SuggestPriority } + targetId: agent.priority_suggester + mode: autonomous + contract: + input: + - { name: severity, type: string, required: true } + - { name: service_tier, type: string, required: true } + - { name: classification, type: string, required: true } + output: + - { name: priority, type: string } + - { name: reason, type: string } + timeout: "30s" + requiredCapabilities: [capability.ai.suggest_priority] + + - source: { type: bpmn.serviceTask, ref: Task_EvaluatePriorityDmn } + targetId: agent.dmn_evaluator + mode: autonomous + contract: + input: + - { name: package_id, type: string, required: true } + - { name: decision_id, type: string, required: true } + - { name: inputs, type: object, required: true } + output: + - { name: output, type: object } + - { name: hit_rule_ids, type: array } + - { name: hit_policy, type: string } + timeout: "5s" + requiredCapabilities: [capability.dmn.evaluate] + + - source: { type: bpmn.serviceTask, ref: Task_EvaluateOwnershipDmn } + targetId: agent.dmn_evaluator + mode: autonomous + contract: + input: + - { name: package_id, type: string, required: true } + - { name: decision_id, type: string, required: true } + - { name: inputs, type: object, required: true } + output: + - { name: output, type: object } + - { name: hit_rule_ids, type: array } + - { name: hit_policy, type: string } + timeout: "5s" + requiredCapabilities: [capability.dmn.evaluate] + + - source: { type: bpmn.serviceTask, ref: Task_EvaluateRoutingDmn } + targetId: agent.dmn_evaluator + mode: autonomous + contract: + input: + - { name: package_id, type: string, required: true } + - { name: decision_id, type: string, required: true } + - { name: inputs, type: object, required: true } + output: + - { name: output, type: object } + - { name: hit_rule_ids, type: array } + - { name: hit_policy, type: string } + timeout: "5s" + requiredCapabilities: [capability.dmn.evaluate] + + - source: { type: bpmn.serviceTask, ref: Task_UpdateIncident } + targetId: agent.incident_updater + mode: autonomous + contract: + input: + - { name: case_id, type: string, required: true } + - { name: patch, type: object, required: true } + - { name: status, type: string } + - { name: reason, type: string } + output: + - { name: case_id, type: string } + - { name: new_status, type: string } + - { name: success, type: boolean } + - { name: event_ids, type: array } + timeout: "10s" + requiredCapabilities: [capability.incident.update] + + - source: { type: bpmn.serviceTask, ref: Task_DraftResponse } + targetId: agent.response_drafter + mode: supervised + contract: + input: + - { name: case_id, type: string, required: true } + - { name: locale, type: string } + - { name: what_happened, type: string, required: true } + - { name: eta_minutes, type: number } + output: + - { name: subject_lv, type: string } + - { name: subject_en, type: string } + - { name: body_lv, type: string } + - { name: body_en, type: string } + - { name: locale, type: string } + timeout: "60s" + requiredCapabilities: [capability.ai.draft_response] + + - source: { type: bpmn.serviceTask, ref: Task_EmitEvent } + targetId: agent.event_emitter + mode: autonomous + contract: + input: + - { name: case_id, type: string, required: true } + - { name: type, type: string, required: true } + - { name: payload, type: object } + - { name: actor_label, type: string } + output: + - { name: event_id, type: string } + - { name: recorded_at, type: string } + timeout: "5s" + requiredCapabilities: [capability.event.emit] diff --git a/tests/bpmn/triage-link-down.test.yaml b/tests/bpmn/triage-link-down.test.yaml new file mode 100644 index 0000000..3cd5cf5 --- /dev/null +++ b/tests/bpmn/triage-link-down.test.yaml @@ -0,0 +1,35 @@ +kind: uapf.test.bpmn +target: bpmn/incident-triage.bpmn +process_id: Process_IncidentTriage +name: triage-link-down +description: | + End-to-end happy path: a Zabbix link-down event arrives, classifies as + network.routing, gets prioritised P1 by the priority DMN, owned by LVRTC, + routed to noc-l2, the case transitions to triaged, a Latvian customer + draft is proposed, and a 'routed' event is emitted. + +input: + signal_id: "11111111-1111-1111-1111-111111111111" + payload: + title: "Link down on edge router rtr-r1" + host: "rtr-r1.lvrtc.lv" + severity: "high" + description: "BGP session also flapping" + +expected_steps_completed: + - Task_NormalizeSignal + - Task_ClassifyIncident + - Task_SuggestPriority + - Task_EvaluatePriorityDmn + - Task_EvaluateOwnershipDmn + - Task_EvaluateRoutingDmn + - Task_UpdateIncident + - Task_DraftResponse + - Task_EmitEvent + +expected_outputs: + taxonomy_code: "network.routing" + priority: "P1" + ownership: "lvrtc" + group_slug: "noc-l2" + new_status: "triaged" diff --git a/uapf.yaml b/uapf.yaml new file mode 100644 index 0000000..a817d95 --- /dev/null +++ b/uapf.yaml @@ -0,0 +1,79 @@ +kind: uapf.package +id: lv.itsm.incident.triage +name: LVRTC Incident Triage +description: | + Level-4 UAPF process for triaging IT-infrastructure incidents at LVRTC. + + Six BPMN service tasks invoke the UAPF-IP capabilities intake.normalize@1, + ai.classify@1, ai.suggest_priority@1, ai.draft_response@1, + incident.update@1 and event.emit@1. Three DMN decision tables encode the + deterministic rules previously hidden inside the host: priority maps + severity x service-tier x AI-suggestion x classification onto P1-P4; + ownership decides whether the case stays inside LVRTC or hands off to an + external partner; routing picks the expert group (helpdesk-l1, noc-l1/l2, + soc-l2, facility-l2, platform-l2, external-handoff). + + Classification, priority suggestion and customer response drafting are AI + steps; the rules that decide *what* the AI proposes versus *who* handles + it live in versioned DMN, not Python. Drafted customer responses are + produced in both Latvian and English and require human approval before + sending (governed by Algorithm Card algo.incident_triage.draft_response). + + v1.0.0 targets UAPF v2.5.0: algorithm cards carry embedded `tests` + arrays (>=2 per card) per chapter 13.16; BPMN service tasks carry + uapf24:algorithmCardRef attributes; resource targets are dispatch + endpoints only. + +level: 4 +version: "1.0.0" + +requires_capabilities: + - intake.normalize@1+ + - ai.classify@1+ + - ai.suggest_priority@1+ + - ai.draft_response@1+ + - dmn.evaluate@1+ + - incident.update@1+ + - event.emit@1+ + +profiles_supported: + - uapf-ip-orchestrated + +guardrails: resources/guardrails.yaml +includes: [] +dependencies: {} + +cornerstones: + bpmn: true + dmn: true + cmmn: false + resources: true + +paths: + bpmn: bpmn + dmn: dmn + resources: resources + metadata: metadata + + +exposure: + mcp: + enabled: true + runnable: true + exposedEntrypoints: + - "Process_IncidentTriage" + exposedArtifacts: + - manifest + - bpmn + - dmn + - docs + +owners: + - type: team + id: lvrtc + contact: incident-mgmt@lvrtc.lv + - type: team + id: openitsm-stewards + contact: stewards@openitsm.algomation.io + +lifecycle: draft