@oneuptime/common 11.3.28 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/Models/DatabaseModels/AIRun.ts +50 -0
  2. package/Models/DatabaseModels/Project.ts +60 -0
  3. package/Server/API/AIInvestigationAPI.ts +148 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.ts +19 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.ts +33 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.ts +19 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  8. package/Server/Services/AlertService.ts +25 -0
  9. package/Server/Services/IncidentService.ts +25 -0
  10. package/Server/Services/IncidentStateTimelineService.ts +19 -0
  11. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +119 -11
  12. package/Server/Utils/AI/Sentinel/AlertInvestigationRunner.ts +141 -0
  13. package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +178 -0
  14. package/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.ts +105 -0
  15. package/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.ts +405 -0
  16. package/Server/Utils/AI/Sentinel/SentinelMemory.ts +167 -0
  17. package/Server/Utils/AI/Toolbox/Index.ts +11 -0
  18. package/Server/Utils/AI/Toolbox/SentinelActionTools.ts +577 -0
  19. package/Tests/Server/API/BaseAPI.test.ts +1 -0
  20. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtil.test.ts +260 -0
  21. package/Types/Date.ts +46 -0
  22. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +257 -0
  23. package/UI/Components/Calendar/Calendar.tsx +8 -2
  24. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
  25. package/build/dist/Models/DatabaseModels/AIRun.js +52 -0
  26. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  27. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  28. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  29. package/build/dist/Server/API/AIInvestigationAPI.js +107 -0
  30. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -0
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js +12 -0
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js.map +1 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js +18 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js.map +1 -0
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js +12 -0
  36. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js.map +1 -0
  37. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  38. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  39. package/build/dist/Server/Services/AlertService.js +24 -0
  40. package/build/dist/Server/Services/AlertService.js.map +1 -1
  41. package/build/dist/Server/Services/IncidentService.js +24 -0
  42. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  43. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  44. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  45. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +59 -11
  46. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  47. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js +119 -0
  48. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js.map +1 -0
  49. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +147 -0
  50. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js.map +1 -0
  51. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js +93 -0
  52. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js.map +1 -0
  53. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js +311 -0
  54. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js.map +1 -0
  55. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js +143 -0
  56. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js.map +1 -0
  57. package/build/dist/Server/Utils/AI/Toolbox/Index.js +6 -0
  58. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  59. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js +451 -0
  60. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js.map +1 -0
  61. package/build/dist/Types/Date.js +32 -0
  62. package/build/dist/Types/Date.js.map +1 -1
  63. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +159 -0
  64. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -0
  65. package/build/dist/UI/Components/Calendar/Calendar.js +2 -2
  66. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  67. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  68. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  69. package/package.json +1 -1
@@ -0,0 +1,577 @@
1
+ import Incident from "../../../../Models/DatabaseModels/Incident";
2
+ import IncidentSeverity from "../../../../Models/DatabaseModels/IncidentSeverity";
3
+ import IncidentPublicNote from "../../../../Models/DatabaseModels/IncidentPublicNote";
4
+ import Runbook from "../../../../Models/DatabaseModels/Runbook";
5
+ import RunbookExecution from "../../../../Models/DatabaseModels/RunbookExecution";
6
+ import OnCallDutyPolicy from "../../../../Models/DatabaseModels/OnCallDutyPolicy";
7
+ import { JSONObject } from "../../../../Types/JSON";
8
+ import ObjectID from "../../../../Types/ObjectID";
9
+ import Permission from "../../../../Types/Permission";
10
+ import BadDataException from "../../../../Types/Exception/BadDataException";
11
+ import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
12
+ import OneUptimeDate from "../../../../Types/Date";
13
+ import UserNotificationEventType from "../../../../Types/UserNotification/UserNotificationEventType";
14
+ import { AIChatCitationTargetType } from "../../../../Types/AI/AIChatTypes";
15
+ import IncidentService from "../../../Services/IncidentService";
16
+ import IncidentSeverityService from "../../../Services/IncidentSeverityService";
17
+ import IncidentPublicNoteService from "../../../Services/IncidentPublicNoteService";
18
+ import OnCallDutyPolicyService from "../../../Services/OnCallDutyPolicyService";
19
+ import RunbookService from "../../../Services/RunbookService";
20
+ import RunbookRuleEngineService from "../../../Services/RunbookRuleEngineService";
21
+ import ToolResultSerializer, { SerializedResult } from "./Serializer";
22
+ import WidgetBuilder from "./WidgetBuilder";
23
+ import {
24
+ ObservabilityTool,
25
+ ToolArgs,
26
+ ToolContext,
27
+ ToolExecutionResult,
28
+ } from "./ToolTypes";
29
+
30
+ /*
31
+ * Sentinel "action belt" — Phase 0.
32
+ *
33
+ * Mutating tools that let the copilot OPERATE the platform (not just answer
34
+ * questions), each a thin wrapper over an already-RBAC-tested service method.
35
+ * They are gated twice: by the tool's requiredPermissions (RBAC) and by the
36
+ * conversation's permission mode (approval / auto-run / hidden in read-only).
37
+ * The acting user is always ctx.props.userId — never a tool argument.
38
+ *
39
+ * Permissions are resolved lazily (not at module load) because this module is
40
+ * pulled in through the service import graph before the model classes are fully
41
+ * wired up — calling a model method at import time throws a circular-dependency
42
+ * TypeError. By the time a tool executes, every module is loaded.
43
+ */
44
+
45
+ let cachedIncidentUpdatePermissions: Array<Permission> | null = null;
46
+ const resolveIncidentUpdatePermissions: () => Array<Permission> =
47
+ (): Array<Permission> => {
48
+ if (!cachedIncidentUpdatePermissions) {
49
+ cachedIncidentUpdatePermissions = new Incident().getUpdatePermissions();
50
+ }
51
+ return cachedIncidentUpdatePermissions;
52
+ };
53
+
54
+ let cachedPublicNoteCreatePermissions: Array<Permission> | null = null;
55
+ const resolvePublicNoteCreatePermissions: () => Array<Permission> =
56
+ (): Array<Permission> => {
57
+ if (!cachedPublicNoteCreatePermissions) {
58
+ cachedPublicNoteCreatePermissions =
59
+ new IncidentPublicNote().getCreatePermissions();
60
+ }
61
+ return cachedPublicNoteCreatePermissions;
62
+ };
63
+
64
+ let cachedRunbookUpdatePermissions: Array<Permission> | null = null;
65
+ const resolveRunbookUpdatePermissions: () => Array<Permission> =
66
+ (): Array<Permission> => {
67
+ if (!cachedRunbookUpdatePermissions) {
68
+ cachedRunbookUpdatePermissions = new Runbook().getUpdatePermissions();
69
+ }
70
+ return cachedRunbookUpdatePermissions;
71
+ };
72
+
73
+ /*
74
+ * ---------------------------------------------------------------------------
75
+ * page_on_call_policy — escalate an incident to its responders.
76
+ * ---------------------------------------------------------------------------
77
+ */
78
+ export const PageOnCallPolicyTool: ObservabilityTool = {
79
+ name: "page_on_call_policy",
80
+ description:
81
+ "Page (trigger) an on-call duty policy so its responders are notified about an incident. Provide the on-call policy id and the incident id it relates to. Use query tools to find the on-call policy id and incident id first. This notifies real people — only do it when clearly asked.",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ onCallDutyPolicyId: {
86
+ type: "string",
87
+ description: "The on-call duty policy's ID to trigger (required).",
88
+ },
89
+ incidentId: {
90
+ type: "string",
91
+ description:
92
+ "The incident's ID this page relates to (required). Find it with query_incidents first.",
93
+ },
94
+ },
95
+ required: ["onCallDutyPolicyId", "incidentId"],
96
+ },
97
+ get requiredPermissions(): Array<Permission> {
98
+ return resolveIncidentUpdatePermissions();
99
+ },
100
+ isMutation: true,
101
+ buildActionTitle: (args: JSONObject): string => {
102
+ return `Page on-call policy ${ToolArgs.getString(args, "onCallDutyPolicyId") || ""}`.trim();
103
+ },
104
+ execute: async (
105
+ args: JSONObject,
106
+ ctx: ToolContext,
107
+ ): Promise<ToolExecutionResult> => {
108
+ const policyId: ObjectID | undefined = ToolArgs.getObjectID(
109
+ args,
110
+ "onCallDutyPolicyId",
111
+ );
112
+ if (!policyId) {
113
+ throw new BadDataException(
114
+ "onCallDutyPolicyId is required. Use query tools to find it first.",
115
+ );
116
+ }
117
+
118
+ const incidentId: ObjectID | undefined = ToolArgs.getObjectID(
119
+ args,
120
+ "incidentId",
121
+ );
122
+ if (!incidentId) {
123
+ throw new BadDataException(
124
+ "incidentId is required. Use query_incidents to find it first.",
125
+ );
126
+ }
127
+
128
+ // Visibility checks under the user's RBAC (execution runs as root inside the service).
129
+ const policy: OnCallDutyPolicy | null =
130
+ await OnCallDutyPolicyService.findOneById({
131
+ id: policyId,
132
+ select: { _id: true, name: true },
133
+ props: ctx.props,
134
+ });
135
+ if (!policy) {
136
+ throw new BadDataException(
137
+ "On-call duty policy not found (or you do not have access to it).",
138
+ );
139
+ }
140
+
141
+ const incident: Incident | null = await IncidentService.findOneById({
142
+ id: incidentId,
143
+ select: { _id: true, incidentNumber: true, title: true },
144
+ props: ctx.props,
145
+ });
146
+ if (!incident) {
147
+ throw new BadDataException(
148
+ "Incident not found (or you do not have access to it).",
149
+ );
150
+ }
151
+
152
+ await OnCallDutyPolicyService.executePolicy(policyId, {
153
+ triggeredByIncidentId: incidentId,
154
+ userNotificationEventType: UserNotificationEventType.IncidentCreated,
155
+ });
156
+
157
+ const incidentIdString: string = incidentId.toString();
158
+
159
+ const serialized: SerializedResult = ToolResultSerializer.serializeRows([
160
+ {
161
+ onCallPolicy: policy.name,
162
+ incidentNumber: incident.incidentNumber,
163
+ incidentTitle: incident.title,
164
+ },
165
+ ]);
166
+
167
+ return {
168
+ dataForLlm: `Paged on-call policy "${policy.name}" for incident #${incident.incidentNumber} ("${incident.title}"). Responders are being notified.\n${serialized.text}`,
169
+ rowCount: 1,
170
+ citationLabel: `Paged "${policy.name}" for incident #${incident.incidentNumber}`,
171
+ citationTarget: {
172
+ type: AIChatCitationTargetType.IncidentView,
173
+ params: { incidentId: incidentIdString },
174
+ },
175
+ redactionCount: serialized.redactionCount,
176
+ isTruncated: false,
177
+ widget: WidgetBuilder.resourceCard({
178
+ title: "On-call paged",
179
+ resourceType: "OnCallDutyPolicy",
180
+ heading: policy.name || "On-call policy",
181
+ subheading: `Incident #${incident.incidentNumber}`,
182
+ fields: [
183
+ { label: "Policy", value: policy.name || "—" },
184
+ { label: "Incident", value: `#${incident.incidentNumber ?? ""}` },
185
+ ],
186
+ link: {
187
+ type: AIChatCitationTargetType.IncidentView,
188
+ params: { incidentId: incidentIdString },
189
+ },
190
+ }),
191
+ };
192
+ },
193
+ };
194
+
195
+ /*
196
+ * ---------------------------------------------------------------------------
197
+ * run_runbook — start an automated runbook.
198
+ * ---------------------------------------------------------------------------
199
+ */
200
+ export const RunRunbookTool: ObservabilityTool = {
201
+ name: "run_runbook",
202
+ description:
203
+ "Start (execute) a runbook by its id, optionally linked to an incident. Use this to run a predefined remediation or diagnostic automation. Find the runbook id with query tools first.",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ runbookId: {
208
+ type: "string",
209
+ description: "The runbook's ID to execute (required).",
210
+ },
211
+ incidentId: {
212
+ type: "string",
213
+ description:
214
+ "Optional incident ID to link this runbook execution to. Find it with query_incidents first.",
215
+ },
216
+ },
217
+ required: ["runbookId"],
218
+ },
219
+ get requiredPermissions(): Array<Permission> {
220
+ return resolveRunbookUpdatePermissions();
221
+ },
222
+ isMutation: true,
223
+ buildActionTitle: (args: JSONObject): string => {
224
+ return `Run runbook ${ToolArgs.getString(args, "runbookId") || ""}`.trim();
225
+ },
226
+ execute: async (
227
+ args: JSONObject,
228
+ ctx: ToolContext,
229
+ ): Promise<ToolExecutionResult> => {
230
+ const runbookId: ObjectID | undefined = ToolArgs.getObjectID(
231
+ args,
232
+ "runbookId",
233
+ );
234
+ if (!runbookId) {
235
+ throw new BadDataException(
236
+ "runbookId is required. Use query tools to find it first.",
237
+ );
238
+ }
239
+
240
+ const userId: ObjectID | undefined = ctx.props.userId;
241
+ if (!userId) {
242
+ throw new BadDataException(
243
+ "No authenticated user in context; cannot run a runbook.",
244
+ );
245
+ }
246
+
247
+ const incidentId: ObjectID | undefined = ToolArgs.getObjectID(
248
+ args,
249
+ "incidentId",
250
+ );
251
+
252
+ // Visibility check under the user's RBAC.
253
+ const runbook: Runbook | null = await RunbookService.findOneById({
254
+ id: runbookId,
255
+ select: { _id: true, name: true },
256
+ props: ctx.props,
257
+ });
258
+ if (!runbook) {
259
+ throw new BadDataException(
260
+ "Runbook not found (or you do not have access to it).",
261
+ );
262
+ }
263
+
264
+ const execution: RunbookExecution | null =
265
+ await RunbookRuleEngineService.startRunbookFor({
266
+ projectId: ctx.projectId,
267
+ runbookId: runbookId,
268
+ linkage: incidentId ? { incidentId: incidentId } : {},
269
+ triggeredByUserId: userId,
270
+ });
271
+
272
+ if (!execution) {
273
+ throw new BadDataException(
274
+ `Runbook "${runbook.name}" could not be started. It may be disabled, empty, or not part of this project.`,
275
+ );
276
+ }
277
+
278
+ const serialized: SerializedResult = ToolResultSerializer.serializeRows([
279
+ {
280
+ runbook: runbook.name,
281
+ executionId: execution.id?.toString(),
282
+ },
283
+ ]);
284
+
285
+ const result: ToolExecutionResult = {
286
+ dataForLlm: `Started runbook "${runbook.name}". Execution is scheduled.\n${serialized.text}`,
287
+ rowCount: 1,
288
+ citationLabel: `Started runbook "${runbook.name}"`,
289
+ redactionCount: serialized.redactionCount,
290
+ isTruncated: false,
291
+ widget: WidgetBuilder.resourceCard({
292
+ title: "Runbook started",
293
+ resourceType: "Runbook",
294
+ heading: runbook.name || "Runbook",
295
+ subheading: "Execution scheduled",
296
+ fields: [{ label: "Runbook", value: runbook.name || "—" }],
297
+ }),
298
+ };
299
+
300
+ if (incidentId) {
301
+ result.citationTarget = {
302
+ type: AIChatCitationTargetType.IncidentView,
303
+ params: { incidentId: incidentId.toString() },
304
+ };
305
+ }
306
+
307
+ return result;
308
+ },
309
+ };
310
+
311
+ /*
312
+ * ---------------------------------------------------------------------------
313
+ * post_incident_status_update — customer-facing update on an incident.
314
+ * ---------------------------------------------------------------------------
315
+ */
316
+ export const PostIncidentStatusUpdateTool: ObservabilityTool = {
317
+ name: "post_incident_status_update",
318
+ description:
319
+ "Post a customer-facing status update (public note) on an incident. It appears on the status page and, by default, notifies subscribers. Provide the incident id and the update text.",
320
+ inputSchema: {
321
+ type: "object",
322
+ properties: {
323
+ incidentId: {
324
+ type: "string",
325
+ description: "The incident's ID (required).",
326
+ },
327
+ note: {
328
+ type: "string",
329
+ description: "The customer-facing update text in markdown (required).",
330
+ },
331
+ notifySubscribers: {
332
+ type: "boolean",
333
+ description:
334
+ "Whether to notify status page subscribers. Defaults to true.",
335
+ },
336
+ },
337
+ required: ["incidentId", "note"],
338
+ },
339
+ get requiredPermissions(): Array<Permission> {
340
+ return resolvePublicNoteCreatePermissions();
341
+ },
342
+ isMutation: true,
343
+ buildActionTitle: (args: JSONObject): string => {
344
+ return `Post public status update on incident ${ToolArgs.getString(args, "incidentId") || ""}`.trim();
345
+ },
346
+ execute: async (
347
+ args: JSONObject,
348
+ ctx: ToolContext,
349
+ ): Promise<ToolExecutionResult> => {
350
+ const incidentId: ObjectID | undefined = ToolArgs.getObjectID(
351
+ args,
352
+ "incidentId",
353
+ );
354
+ if (!incidentId) {
355
+ throw new BadDataException(
356
+ "incidentId is required. Use query_incidents to find it first.",
357
+ );
358
+ }
359
+
360
+ const note: string | undefined = ToolArgs.getString(args, "note");
361
+ if (!note) {
362
+ throw new BadDataException("note is required (the update text).");
363
+ }
364
+
365
+ const userId: ObjectID | undefined = ctx.props.userId;
366
+ if (!userId) {
367
+ throw new BadDataException(
368
+ "No authenticated user in context; cannot post a status update.",
369
+ );
370
+ }
371
+
372
+ const notifySubscribers: boolean =
373
+ ToolArgs.getBoolean(args, "notifySubscribers") ?? true;
374
+
375
+ const incident: Incident | null = await IncidentService.findOneById({
376
+ id: incidentId,
377
+ select: { _id: true, incidentNumber: true, title: true },
378
+ props: ctx.props,
379
+ });
380
+ if (!incident) {
381
+ throw new BadDataException(
382
+ "Incident not found (or you do not have access to it).",
383
+ );
384
+ }
385
+
386
+ const publicNote: IncidentPublicNote = new IncidentPublicNote();
387
+ publicNote.incidentId = incidentId;
388
+ publicNote.projectId = ctx.projectId;
389
+ publicNote.note = note;
390
+ publicNote.postedAt = OneUptimeDate.getCurrentDate();
391
+ publicNote.createdByUserId = userId;
392
+ publicNote.shouldStatusPageSubscribersBeNotifiedOnNoteCreated =
393
+ notifySubscribers;
394
+
395
+ await IncidentPublicNoteService.create({
396
+ data: publicNote,
397
+ props: ctx.props,
398
+ });
399
+
400
+ const incidentIdString: string = incidentId.toString();
401
+
402
+ const serialized: SerializedResult = ToolResultSerializer.serializeRows([
403
+ {
404
+ incidentNumber: incident.incidentNumber,
405
+ notifySubscribers: notifySubscribers,
406
+ },
407
+ ]);
408
+
409
+ return {
410
+ dataForLlm: `Posted a public status update on incident #${incident.incidentNumber}${
411
+ notifySubscribers ? " and notified subscribers" : ""
412
+ }.\n${serialized.text}`,
413
+ rowCount: 1,
414
+ citationLabel: `Status update on incident #${incident.incidentNumber}`,
415
+ citationTarget: {
416
+ type: AIChatCitationTargetType.IncidentView,
417
+ params: { incidentId: incidentIdString },
418
+ },
419
+ redactionCount: serialized.redactionCount,
420
+ isTruncated: false,
421
+ widget: WidgetBuilder.resourceCard({
422
+ title: "Status update posted",
423
+ resourceType: "Incident",
424
+ heading: `#${incident.incidentNumber} · ${incident.title}`,
425
+ subheading: notifySubscribers
426
+ ? "Subscribers notified"
427
+ : "Subscribers not notified",
428
+ fields: [
429
+ {
430
+ label: "Update",
431
+ value: note.length > 200 ? `${note.substring(0, 200)}…` : note,
432
+ },
433
+ ],
434
+ link: {
435
+ type: AIChatCitationTargetType.IncidentView,
436
+ params: { incidentId: incidentIdString },
437
+ },
438
+ }),
439
+ };
440
+ },
441
+ };
442
+
443
+ /*
444
+ * ---------------------------------------------------------------------------
445
+ * change_incident_severity — re-classify an incident.
446
+ * ---------------------------------------------------------------------------
447
+ */
448
+ export const ChangeIncidentSeverityTool: ObservabilityTool = {
449
+ name: "change_incident_severity",
450
+ description:
451
+ "Change the severity of an existing incident. Provide the incident id and the new severity name (must match an existing incident severity in this project, e.g. 'SEV1', 'Critical').",
452
+ inputSchema: {
453
+ type: "object",
454
+ properties: {
455
+ incidentId: {
456
+ type: "string",
457
+ description: "The incident's ID (required).",
458
+ },
459
+ severityName: {
460
+ type: "string",
461
+ description: "Name of an existing incident severity (required).",
462
+ },
463
+ },
464
+ required: ["incidentId", "severityName"],
465
+ },
466
+ get requiredPermissions(): Array<Permission> {
467
+ return resolveIncidentUpdatePermissions();
468
+ },
469
+ isMutation: true,
470
+ buildActionTitle: (args: JSONObject): string => {
471
+ return `Change incident ${ToolArgs.getString(args, "incidentId") || ""} severity to ${ToolArgs.getString(args, "severityName") || ""}`.trim();
472
+ },
473
+ execute: async (
474
+ args: JSONObject,
475
+ ctx: ToolContext,
476
+ ): Promise<ToolExecutionResult> => {
477
+ const incidentId: ObjectID | undefined = ToolArgs.getObjectID(
478
+ args,
479
+ "incidentId",
480
+ );
481
+ if (!incidentId) {
482
+ throw new BadDataException(
483
+ "incidentId is required. Use query_incidents to find it first.",
484
+ );
485
+ }
486
+
487
+ const severityName: string | undefined = ToolArgs.getString(
488
+ args,
489
+ "severityName",
490
+ );
491
+ if (!severityName) {
492
+ throw new BadDataException("severityName is required.");
493
+ }
494
+
495
+ const incident: Incident | null = await IncidentService.findOneById({
496
+ id: incidentId,
497
+ select: { _id: true, incidentNumber: true, title: true },
498
+ props: ctx.props,
499
+ });
500
+ if (!incident) {
501
+ throw new BadDataException(
502
+ "Incident not found (or you do not have access to it).",
503
+ );
504
+ }
505
+
506
+ const severities: Array<IncidentSeverity> =
507
+ await IncidentSeverityService.findBy({
508
+ query: {},
509
+ select: { _id: true, name: true, order: true },
510
+ sort: { order: SortOrder.Ascending },
511
+ limit: 50,
512
+ skip: 0,
513
+ props: ctx.props,
514
+ });
515
+
516
+ const severity: IncidentSeverity | undefined = severities.find(
517
+ (item: IncidentSeverity) => {
518
+ return item.name?.toLowerCase() === severityName.toLowerCase();
519
+ },
520
+ );
521
+
522
+ if (!severity) {
523
+ const available: string = severities
524
+ .map((s: IncidentSeverity) => {
525
+ return s.name;
526
+ })
527
+ .filter(Boolean)
528
+ .join(", ");
529
+ throw new BadDataException(
530
+ `No incident severity named "${severityName}". Available severities: ${available || "none configured"}.`,
531
+ );
532
+ }
533
+
534
+ await IncidentService.updateOneById({
535
+ id: incidentId,
536
+ data: {
537
+ incidentSeverityId: severity.id!,
538
+ },
539
+ props: ctx.props,
540
+ });
541
+
542
+ const incidentIdString: string = incidentId.toString();
543
+
544
+ const serialized: SerializedResult = ToolResultSerializer.serializeRows([
545
+ {
546
+ incidentNumber: incident.incidentNumber,
547
+ newSeverity: severity.name,
548
+ },
549
+ ]);
550
+
551
+ return {
552
+ dataForLlm: `Changed incident #${incident.incidentNumber} severity to ${severity.name}.\n${serialized.text}`,
553
+ rowCount: 1,
554
+ citationLabel: `Incident #${incident.incidentNumber} → ${severity.name}`,
555
+ citationTarget: {
556
+ type: AIChatCitationTargetType.IncidentView,
557
+ params: { incidentId: incidentIdString },
558
+ },
559
+ redactionCount: serialized.redactionCount,
560
+ isTruncated: false,
561
+ widget: WidgetBuilder.resourceCard({
562
+ title: "Severity changed",
563
+ resourceType: "Incident",
564
+ heading: `#${incident.incidentNumber} · ${incident.title}`,
565
+ subheading: `Severity: ${severity.name}`,
566
+ fields: [
567
+ { label: "Number", value: `#${incident.incidentNumber ?? ""}` },
568
+ { label: "New severity", value: severity.name || "—" },
569
+ ],
570
+ link: {
571
+ type: AIChatCitationTargetType.IncidentView,
572
+ params: { incidentId: incidentIdString },
573
+ },
574
+ }),
575
+ };
576
+ },
577
+ };
@@ -103,6 +103,7 @@ jest.mock("../../../Server/Services/ProjectService", () => {
103
103
 
104
104
  jest.mock("../../../Server/EnvironmentConfig", () => {
105
105
  return {
106
+ ...jest.requireActual("../../../Server/EnvironmentConfig"),
106
107
  IsBillingEnabled: true,
107
108
  LogLevel: "INFO", // Use string literal instead of ConfigLogLevel.INFO
108
109
  DisableTelemetry: true,