@oneuptime/common 11.3.25 → 11.3.26

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 (189) hide show
  1. package/Models/DatabaseModels/AIConversation.ts +322 -0
  2. package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
  3. package/Models/DatabaseModels/AIRun.ts +584 -0
  4. package/Models/DatabaseModels/AIRunEvent.ts +443 -0
  5. package/Models/DatabaseModels/Index.ts +8 -0
  6. package/Models/DatabaseModels/LlmLog.ts +26 -0
  7. package/Models/DatabaseModels/LlmProvider.ts +2 -2
  8. package/Server/API/AIChatAPI.ts +693 -0
  9. package/Server/API/LlmProviderAPI.ts +169 -0
  10. package/Server/API/MicrosoftTeamsAPI.ts +5 -0
  11. package/Server/API/SlackAPI.ts +663 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  17. package/Server/Services/AIConversationMessageService.ts +39 -0
  18. package/Server/Services/AIConversationService.ts +63 -0
  19. package/Server/Services/AIRunEventService.ts +39 -0
  20. package/Server/Services/AIRunService.ts +39 -0
  21. package/Server/Services/AIService.ts +53 -31
  22. package/Server/Services/DatabaseService.ts +19 -0
  23. package/Server/Services/LlmProviderService.ts +110 -0
  24. package/Server/Services/ProjectService.ts +24 -0
  25. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
  26. package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
  27. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1054 -0
  28. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
  29. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
  30. package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
  31. package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
  32. package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
  33. package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
  34. package/Server/Utils/AI/Toolbox/IncidentTools.ts +204 -0
  35. package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
  36. package/Server/Utils/AI/Toolbox/Index.ts +228 -0
  37. package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
  38. package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
  39. package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
  40. package/Server/Utils/AI/Toolbox/Serializer.ts +257 -0
  41. package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
  42. package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
  43. package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
  44. package/Server/Utils/LLM/LLMService.ts +502 -92
  45. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
  46. package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
  47. package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  48. package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
  49. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
  50. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
  51. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +308 -0
  52. package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
  53. package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
  54. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +155 -0
  55. package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
  56. package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
  57. package/Types/AI/AIChatMessageRole.ts +6 -0
  58. package/Types/AI/AIChatMessageStatus.ts +32 -0
  59. package/Types/AI/AIChatPermissionMode.ts +69 -0
  60. package/Types/AI/AIChatTypes.ts +231 -0
  61. package/Types/AI/AIRunEventType.ts +17 -0
  62. package/Types/AI/AIRunStatus.ts +28 -0
  63. package/Types/AI/AIRunType.ts +6 -0
  64. package/Types/LLM/LlmType.ts +6 -0
  65. package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
  66. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
  67. package/UI/Components/Page/Page.tsx +14 -0
  68. package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
  69. package/UI/Utils/TestLLMProvider.ts +59 -0
  70. package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
  71. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
  72. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
  73. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
  74. package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
  75. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
  76. package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
  77. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
  78. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  79. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  80. package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
  81. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
  82. package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
  83. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  84. package/build/dist/Server/API/AIChatAPI.js +498 -0
  85. package/build/dist/Server/API/AIChatAPI.js.map +1 -0
  86. package/build/dist/Server/API/LlmProviderAPI.js +107 -1
  87. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  88. package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
  89. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  90. package/build/dist/Server/API/SlackAPI.js +442 -0
  91. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
  94. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  102. package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
  103. package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
  104. package/build/dist/Server/Services/AIConversationService.js +42 -0
  105. package/build/dist/Server/Services/AIConversationService.js.map +1 -0
  106. package/build/dist/Server/Services/AIRunEventService.js +34 -0
  107. package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
  108. package/build/dist/Server/Services/AIRunService.js +34 -0
  109. package/build/dist/Server/Services/AIRunService.js.map +1 -0
  110. package/build/dist/Server/Services/AIService.js +31 -23
  111. package/build/dist/Server/Services/AIService.js.map +1 -1
  112. package/build/dist/Server/Services/DatabaseService.js +10 -0
  113. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/LlmProviderService.js +108 -0
  115. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  116. package/build/dist/Server/Services/ProjectService.js +25 -0
  117. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  118. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
  119. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  120. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
  121. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
  122. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +745 -0
  123. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
  124. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
  125. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
  126. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
  127. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
  128. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
  129. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
  130. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
  131. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
  132. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
  133. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
  134. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
  135. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
  136. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +167 -0
  137. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
  138. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
  139. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
  140. package/build/dist/Server/Utils/AI/Toolbox/Index.js +150 -0
  141. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
  142. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
  143. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
  144. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
  145. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
  146. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
  147. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
  148. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +188 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
  150. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
  151. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
  152. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
  153. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
  154. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
  155. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
  156. package/build/dist/Server/Utils/LLM/LLMService.js +372 -88
  157. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  158. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  160. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
  161. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  162. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  163. package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
  164. package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
  165. package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
  166. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
  167. package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
  168. package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
  169. package/build/dist/Types/AI/AIChatTypes.js +74 -0
  170. package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
  171. package/build/dist/Types/AI/AIRunEventType.js +18 -0
  172. package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
  173. package/build/dist/Types/AI/AIRunStatus.js +26 -0
  174. package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
  175. package/build/dist/Types/AI/AIRunType.js +7 -0
  176. package/build/dist/Types/AI/AIRunType.js.map +1 -0
  177. package/build/dist/Types/LLM/LlmType.js +6 -0
  178. package/build/dist/Types/LLM/LlmType.js.map +1 -1
  179. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
  180. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  181. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
  182. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  183. package/build/dist/UI/Components/Page/Page.js +3 -1
  184. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  185. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
  186. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
  187. package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
  188. package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
  189. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ import { ToolArgs } from "../../../../Server/Utils/AI/Toolbox/ToolTypes";
2
+ import ObjectID from "../../../../Types/ObjectID";
3
+ import { describe, expect, test } from "@jest/globals";
4
+
5
+ /*
6
+ * scopeServiceIds decides the `serviceIds` filter the aggregation tools hand to
7
+ * the raw-SQL aggregation services. The aggregation services treat a
8
+ * missing/empty serviceIds as "no filter" (whole project), so for a
9
+ * label-restricted user this helper must NEVER return undefined or []. These
10
+ * tests pin that invariant.
11
+ */
12
+ describe("ToolArgs.scopeServiceIds", () => {
13
+ test("unrestricted user, no requested service → no filter", () => {
14
+ expect(ToolArgs.scopeServiceIds(null, undefined)).toBeUndefined();
15
+ });
16
+
17
+ test("unrestricted user with a requested service → just that service", () => {
18
+ const requested: ObjectID = ObjectID.generate();
19
+ const result: Array<ObjectID> | undefined = ToolArgs.scopeServiceIds(
20
+ null,
21
+ requested,
22
+ );
23
+ expect(
24
+ result?.map((id: ObjectID) => {
25
+ return id.toString();
26
+ }),
27
+ ).toEqual([requested.toString()]);
28
+ });
29
+
30
+ test("restricted user, no requested service → the accessible set", () => {
31
+ const a: ObjectID = ObjectID.generate();
32
+ const b: ObjectID = ObjectID.generate();
33
+ const result: Array<ObjectID> | undefined = ToolArgs.scopeServiceIds(
34
+ [a, b],
35
+ undefined,
36
+ );
37
+ expect(
38
+ result?.map((id: ObjectID) => {
39
+ return id.toString();
40
+ }),
41
+ ).toEqual([a.toString(), b.toString()]);
42
+ });
43
+
44
+ test("restricted user requesting an accessible service → intersection", () => {
45
+ const a: ObjectID = ObjectID.generate();
46
+ const b: ObjectID = ObjectID.generate();
47
+ const result: Array<ObjectID> | undefined = ToolArgs.scopeServiceIds(
48
+ [a, b],
49
+ b,
50
+ );
51
+ expect(
52
+ result?.map((id: ObjectID) => {
53
+ return id.toString();
54
+ }),
55
+ ).toEqual([b.toString()]);
56
+ });
57
+
58
+ test("restricted user requesting a forbidden service → no-match sentinel, never empty", () => {
59
+ const accessible: ObjectID = ObjectID.generate();
60
+ const forbidden: ObjectID = ObjectID.generate();
61
+ const result: Array<ObjectID> | undefined = ToolArgs.scopeServiceIds(
62
+ [accessible],
63
+ forbidden,
64
+ );
65
+ expect(result).toBeDefined();
66
+ expect(result).toHaveLength(1);
67
+ expect(result?.[0]?.toString()).toBe(ObjectID.getZeroObjectID().toString());
68
+ });
69
+
70
+ test("restricted user with no accessible services → no-match sentinel, never empty", () => {
71
+ const result: Array<ObjectID> | undefined = ToolArgs.scopeServiceIds(
72
+ [],
73
+ undefined,
74
+ );
75
+ expect(result).toBeDefined();
76
+ expect(result).toHaveLength(1);
77
+ expect(result?.[0]?.toString()).toBe(ObjectID.getZeroObjectID().toString());
78
+ });
79
+ });
@@ -0,0 +1,155 @@
1
+ import ToolResultSerializer, {
2
+ SerializedResult,
3
+ } from "../../../../Server/Utils/AI/Toolbox/Serializer";
4
+ import { JSONObject } from "../../../../Types/JSON";
5
+ import { describe, expect, test } from "@jest/globals";
6
+
7
+ describe("ToolResultSerializer.redact", () => {
8
+ test("redacts email addresses", () => {
9
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
10
+ "user john.doe@example.com logged in",
11
+ );
12
+ expect(result.text).toBe("user [redacted-email] logged in");
13
+ expect(result.count).toBe(1);
14
+ });
15
+
16
+ test("redacts JWTs", () => {
17
+ const jwt: string = `eyJhbGciOiJIUzI1NiJ9.${"a".repeat(20)}.${"b".repeat(10)}`;
18
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
19
+ `token=${jwt} used`,
20
+ );
21
+ expect(result.text).toContain("[redacted-jwt]");
22
+ expect(result.text).not.toContain(jwt);
23
+ });
24
+
25
+ test("redacts bearer tokens", () => {
26
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
27
+ "Authorization: Bearer abcdefghijklmnop1234567890",
28
+ );
29
+ expect(result.text).not.toContain("abcdefghijklmnop1234567890");
30
+ expect(result.text).toContain("[redacted");
31
+ });
32
+
33
+ test("redacts IPv4 addresses", () => {
34
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
35
+ "request from 203.0.113.42 failed",
36
+ );
37
+ expect(result.text).toBe("request from [redacted-ip] failed");
38
+ });
39
+
40
+ test("redacts long hex strings", () => {
41
+ const hex: string = "deadbeef".repeat(5);
42
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
43
+ `session ${hex} expired`,
44
+ );
45
+ expect(result.text).toContain("[redacted-hex]");
46
+ });
47
+
48
+ test("preserves 32-char OTel trace IDs and 16-char span IDs", () => {
49
+ /*
50
+ * Exactly 32 hex = W3C/OTel trace ID; exactly 16 hex = span ID. Both must
51
+ * survive so the model can pivot search_logs -> get_trace.
52
+ */
53
+ const traceId: string = "4bf92f3577b34da6a3ce929d0e0e4736";
54
+ const spanId: string = "00f067aa0ba902b7";
55
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
56
+ `traceId=${traceId} spanId=${spanId}`,
57
+ );
58
+ expect(result.text).toContain(traceId);
59
+ expect(result.text).toContain(spanId);
60
+ expect(result.text).not.toContain("[redacted-hex]");
61
+ expect(result.count).toBe(0);
62
+ });
63
+
64
+ test("still redacts hex secrets longer than a trace ID (33+ chars)", () => {
65
+ const sha256: string = "a".repeat(64);
66
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
67
+ `digest ${sha256} verified`,
68
+ );
69
+ expect(result.text).toContain("[redacted-hex]");
70
+ expect(result.text).not.toContain(sha256);
71
+ });
72
+
73
+ test("redacts key=value secrets and keeps the key via capture groups", () => {
74
+ const result: { text: string; count: number } = ToolResultSerializer.redact(
75
+ "config: password=supersecretvalue retries=3",
76
+ );
77
+ expect(result.text).toContain("password=[redacted]");
78
+ expect(result.text).not.toContain("supersecretvalue");
79
+ expect(result.text).toContain("retries=3");
80
+ });
81
+
82
+ test("leaves benign text untouched", () => {
83
+ const text: string = "p95 latency is 230ms for checkout";
84
+ const result: { text: string; count: number } =
85
+ ToolResultSerializer.redact(text);
86
+ expect(result.text).toBe(text);
87
+ expect(result.count).toBe(0);
88
+ });
89
+ });
90
+
91
+ describe("ToolResultSerializer.serializeRows", () => {
92
+ test("caps rows at 50 and marks truncation", () => {
93
+ const rows: Array<JSONObject> = [];
94
+ for (let i: number = 0; i < 80; i++) {
95
+ rows.push({ index: i, value: `row-${i}` });
96
+ }
97
+
98
+ const result: SerializedResult = ToolResultSerializer.serializeRows(rows);
99
+ expect(result.isTruncated).toBe(true);
100
+ expect(result.rowCount).toBe(80);
101
+ expect(result.text.split("\n").length).toBeLessThanOrEqual(51);
102
+ });
103
+
104
+ test("tells the model how many rows were hidden when capped", () => {
105
+ const rows: Array<JSONObject> = [];
106
+ for (let i: number = 0; i < 80; i++) {
107
+ rows.push({ index: i, value: `row-${i}` });
108
+ }
109
+
110
+ const result: SerializedResult = ToolResultSerializer.serializeRows(rows);
111
+ expect(result.text).toContain("first 50 of 80 rows");
112
+ });
113
+
114
+ test("does not add a truncation marker when rows fit", () => {
115
+ const rows: Array<JSONObject> = [{ index: 1 }, { index: 2 }];
116
+ const result: SerializedResult = ToolResultSerializer.serializeRows(rows);
117
+ expect(result.isTruncated).toBe(false);
118
+ expect(result.text).not.toContain("showing the first");
119
+ });
120
+
121
+ test("truncates long field values", () => {
122
+ const result: SerializedResult = ToolResultSerializer.serializeRows([
123
+ { body: "x".repeat(2000) },
124
+ ]);
125
+ expect(result.isTruncated).toBe(true);
126
+ expect(result.text).toContain("[truncated]");
127
+ expect(result.text.length).toBeLessThan(700);
128
+ });
129
+
130
+ test("caps total payload size", () => {
131
+ const rows: Array<JSONObject> = [];
132
+ for (let i: number = 0; i < 50; i++) {
133
+ rows.push({ body: "y".repeat(490) });
134
+ }
135
+ const result: SerializedResult = ToolResultSerializer.serializeRows(rows);
136
+ expect(Buffer.byteLength(result.text, "utf8")).toBeLessThanOrEqual(
137
+ 17 * 1024,
138
+ );
139
+ expect(result.isTruncated).toBe(true);
140
+ });
141
+
142
+ test("reports zero rows honestly", () => {
143
+ const result: SerializedResult = ToolResultSerializer.serializeRows([]);
144
+ expect(result.rowCount).toBe(0);
145
+ expect(result.text).toBe("(no rows found)");
146
+ });
147
+
148
+ test("redacts inside rows", () => {
149
+ const result: SerializedResult = ToolResultSerializer.serializeRows([
150
+ { body: "contact admin@corp.com now" },
151
+ ]);
152
+ expect(result.text).toContain("[redacted-email]");
153
+ expect(result.redactionCount).toBe(1);
154
+ });
155
+ });
@@ -0,0 +1,127 @@
1
+ import SubscriptionStatus, {
2
+ SubscriptionStatusUtil,
3
+ } from "../../../Types/Billing/SubscriptionStatus";
4
+
5
+ describe("SubscriptionStatusUtil", () => {
6
+ describe("isSubscriptionActive", () => {
7
+ test("treats a missing status as active", () => {
8
+ expect(SubscriptionStatusUtil.isSubscriptionActive(undefined)).toBe(true);
9
+ });
10
+
11
+ test.each([
12
+ SubscriptionStatus.Active,
13
+ SubscriptionStatus.Trialing,
14
+ SubscriptionStatus.PastDue,
15
+ ])("returns true for %s", (status: SubscriptionStatus) => {
16
+ expect(SubscriptionStatusUtil.isSubscriptionActive(status)).toBe(true);
17
+ });
18
+
19
+ test.each([
20
+ SubscriptionStatus.Incomplete,
21
+ SubscriptionStatus.IncompleteExpired,
22
+ SubscriptionStatus.Canceled,
23
+ SubscriptionStatus.Unpaid,
24
+ SubscriptionStatus.Expired,
25
+ SubscriptionStatus.Paused,
26
+ ])("returns false for %s", (status: SubscriptionStatus) => {
27
+ expect(SubscriptionStatusUtil.isSubscriptionActive(status)).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe("isSubscriptionInactive", () => {
32
+ test("is the inverse of isSubscriptionActive", () => {
33
+ const statuses: Array<SubscriptionStatus | undefined> = [
34
+ undefined,
35
+ ...Object.values(SubscriptionStatus),
36
+ ];
37
+
38
+ for (const status of statuses) {
39
+ expect(SubscriptionStatusUtil.isSubscriptionInactive(status)).toBe(
40
+ !SubscriptionStatusUtil.isSubscriptionActive(status),
41
+ );
42
+ }
43
+ });
44
+
45
+ test("treats a missing status as active (not inactive)", () => {
46
+ expect(SubscriptionStatusUtil.isSubscriptionInactive(undefined)).toBe(
47
+ false,
48
+ );
49
+ });
50
+ });
51
+
52
+ describe("isSubscriptionOverdue", () => {
53
+ test("returns false for a missing status", () => {
54
+ expect(SubscriptionStatusUtil.isSubscriptionOverdue(undefined)).toBe(
55
+ false,
56
+ );
57
+ });
58
+
59
+ test("returns true only for PastDue", () => {
60
+ expect(
61
+ SubscriptionStatusUtil.isSubscriptionOverdue(
62
+ SubscriptionStatus.PastDue,
63
+ ),
64
+ ).toBe(true);
65
+ expect(
66
+ SubscriptionStatusUtil.isSubscriptionOverdue(SubscriptionStatus.Active),
67
+ ).toBe(false);
68
+ expect(
69
+ SubscriptionStatusUtil.isSubscriptionOverdue(
70
+ SubscriptionStatus.Canceled,
71
+ ),
72
+ ).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe("isSubscriptionCancelled", () => {
77
+ test("returns false for a missing status", () => {
78
+ expect(SubscriptionStatusUtil.isSubscriptionCancelled(undefined)).toBe(
79
+ false,
80
+ );
81
+ });
82
+
83
+ test.each([
84
+ SubscriptionStatus.Canceled,
85
+ SubscriptionStatus.Unpaid,
86
+ SubscriptionStatus.Expired,
87
+ SubscriptionStatus.IncompleteExpired,
88
+ ])("returns true for %s", (status: SubscriptionStatus) => {
89
+ expect(SubscriptionStatusUtil.isSubscriptionCancelled(status)).toBe(true);
90
+ });
91
+
92
+ test.each([
93
+ SubscriptionStatus.Active,
94
+ SubscriptionStatus.Trialing,
95
+ SubscriptionStatus.PastDue,
96
+ SubscriptionStatus.Incomplete,
97
+ SubscriptionStatus.Paused,
98
+ ])("returns false for %s", (status: SubscriptionStatus) => {
99
+ expect(SubscriptionStatusUtil.isSubscriptionCancelled(status)).toBe(
100
+ false,
101
+ );
102
+ });
103
+ });
104
+
105
+ describe("status relationships", () => {
106
+ test("PastDue is both active and overdue", () => {
107
+ expect(
108
+ SubscriptionStatusUtil.isSubscriptionActive(SubscriptionStatus.PastDue),
109
+ ).toBe(true);
110
+ expect(
111
+ SubscriptionStatusUtil.isSubscriptionOverdue(
112
+ SubscriptionStatus.PastDue,
113
+ ),
114
+ ).toBe(true);
115
+ });
116
+
117
+ test("active and cancelled statuses never overlap", () => {
118
+ for (const status of Object.values(SubscriptionStatus)) {
119
+ const active: boolean =
120
+ SubscriptionStatusUtil.isSubscriptionActive(status);
121
+ const cancelled: boolean =
122
+ SubscriptionStatusUtil.isSubscriptionCancelled(status);
123
+ expect(active && cancelled).toBe(false);
124
+ }
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,213 @@
1
+ import AggregationType from "../../../Types/BaseDatabase/AggregationType";
2
+ import RecordingRuleDefinition, {
3
+ RecordingRuleDefinitionUtil,
4
+ RecordingRuleSource,
5
+ RECORDING_RULE_MAX_SOURCES,
6
+ RECORDING_RULE_MAX_EXPRESSION_LENGTH,
7
+ } from "../../../Types/Metrics/RecordingRuleDefinition";
8
+
9
+ describe("RecordingRuleDefinitionUtil", () => {
10
+ const makeSource: (
11
+ overrides?: Partial<RecordingRuleSource>,
12
+ ) => RecordingRuleSource = (
13
+ overrides: Partial<RecordingRuleSource> = {},
14
+ ): RecordingRuleSource => {
15
+ return {
16
+ alias: "A",
17
+ metricName: "http.requests",
18
+ aggregationType: AggregationType.Sum,
19
+ ...overrides,
20
+ };
21
+ };
22
+
23
+ describe("getAggregationOptions", () => {
24
+ test("returns a value/label option for each supported aggregation", () => {
25
+ const options: Array<{ value: AggregationType; label: string }> =
26
+ RecordingRuleDefinitionUtil.getAggregationOptions();
27
+
28
+ expect(options.length).toBeGreaterThan(0);
29
+ for (const option of options) {
30
+ expect(typeof option.label).toBe("string");
31
+ expect(option.label.length).toBeGreaterThan(0);
32
+ expect(Object.values(AggregationType)).toContain(option.value);
33
+ }
34
+ });
35
+ });
36
+
37
+ describe("getNextAlias", () => {
38
+ test("returns A when there are no sources", () => {
39
+ expect(RecordingRuleDefinitionUtil.getNextAlias(undefined)).toBe("A");
40
+ expect(RecordingRuleDefinitionUtil.getNextAlias([])).toBe("A");
41
+ });
42
+
43
+ test("returns the first unused letter", () => {
44
+ expect(
45
+ RecordingRuleDefinitionUtil.getNextAlias([makeSource({ alias: "A" })]),
46
+ ).toBe("B");
47
+ expect(
48
+ RecordingRuleDefinitionUtil.getNextAlias([
49
+ makeSource({ alias: "A" }),
50
+ makeSource({ alias: "B" }),
51
+ ]),
52
+ ).toBe("C");
53
+ });
54
+
55
+ test("skips gaps and returns the lowest free letter", () => {
56
+ expect(
57
+ RecordingRuleDefinitionUtil.getNextAlias([
58
+ makeSource({ alias: "A" }),
59
+ makeSource({ alias: "C" }),
60
+ ]),
61
+ ).toBe("B");
62
+ });
63
+ });
64
+
65
+ describe("getEmptyDefinition", () => {
66
+ test("returns a single source aliased A with expression A", () => {
67
+ const def: RecordingRuleDefinition =
68
+ RecordingRuleDefinitionUtil.getEmptyDefinition();
69
+
70
+ expect(def.sources.length).toBe(1);
71
+ expect(def.sources[0]!.alias).toBe("A");
72
+ expect(def.expression).toBe("A");
73
+ });
74
+
75
+ test("is itself a valid definition once a metric name is provided", () => {
76
+ const def: RecordingRuleDefinition =
77
+ RecordingRuleDefinitionUtil.getEmptyDefinition();
78
+ def.sources[0]!.metricName = "http.requests";
79
+
80
+ expect(RecordingRuleDefinitionUtil.getValidationError(def)).toBeNull();
81
+ });
82
+ });
83
+
84
+ describe("getValidationError", () => {
85
+ const validDefinition: () => RecordingRuleDefinition =
86
+ (): RecordingRuleDefinition => {
87
+ return {
88
+ sources: [makeSource({ alias: "A" }), makeSource({ alias: "B" })],
89
+ expression: "A / B * 100",
90
+ };
91
+ };
92
+
93
+ test("accepts a valid definition", () => {
94
+ expect(
95
+ RecordingRuleDefinitionUtil.getValidationError(validDefinition()),
96
+ ).toBeNull();
97
+ });
98
+
99
+ test("requires a definition", () => {
100
+ expect(RecordingRuleDefinitionUtil.getValidationError(undefined)).toBe(
101
+ "Definition is required.",
102
+ );
103
+ });
104
+
105
+ test("requires at least one source", () => {
106
+ expect(
107
+ RecordingRuleDefinitionUtil.getValidationError({
108
+ sources: [],
109
+ expression: "A",
110
+ }),
111
+ ).toBe("Add at least one source metric.");
112
+ });
113
+
114
+ test("rejects more than the maximum number of sources", () => {
115
+ const sources: Array<RecordingRuleSource> = Array.from(
116
+ { length: RECORDING_RULE_MAX_SOURCES + 1 },
117
+ (_unused: unknown, i: number) => {
118
+ return makeSource({ alias: "ABCDE"[i] as string });
119
+ },
120
+ );
121
+
122
+ expect(
123
+ RecordingRuleDefinitionUtil.getValidationError({
124
+ sources,
125
+ expression: "A",
126
+ }),
127
+ ).toContain(`at most ${RECORDING_RULE_MAX_SOURCES}`);
128
+ });
129
+
130
+ test("rejects an invalid alias", () => {
131
+ expect(
132
+ RecordingRuleDefinitionUtil.getValidationError({
133
+ sources: [makeSource({ alias: "a" })],
134
+ expression: "A",
135
+ }),
136
+ ).toContain("single uppercase letter");
137
+ });
138
+
139
+ test("rejects duplicate aliases", () => {
140
+ expect(
141
+ RecordingRuleDefinitionUtil.getValidationError({
142
+ sources: [makeSource({ alias: "A" }), makeSource({ alias: "A" })],
143
+ expression: "A",
144
+ }),
145
+ ).toContain("Duplicate alias");
146
+ });
147
+
148
+ test("requires a metric name", () => {
149
+ expect(
150
+ RecordingRuleDefinitionUtil.getValidationError({
151
+ sources: [makeSource({ alias: "A", metricName: " " })],
152
+ expression: "A",
153
+ }),
154
+ ).toContain("Metric name is required");
155
+ });
156
+
157
+ test("requires both a filter key and value, or neither", () => {
158
+ expect(
159
+ RecordingRuleDefinitionUtil.getValidationError({
160
+ sources: [makeSource({ alias: "A", filterAttributeKey: "env" })],
161
+ expression: "A",
162
+ }),
163
+ ).toContain("needs both a key and a value");
164
+ });
165
+
166
+ test("requires an expression", () => {
167
+ expect(
168
+ RecordingRuleDefinitionUtil.getValidationError({
169
+ sources: [makeSource({ alias: "A" })],
170
+ expression: " ",
171
+ }),
172
+ ).toBe("Expression is required.");
173
+ });
174
+
175
+ test("rejects an over-length expression", () => {
176
+ expect(
177
+ RecordingRuleDefinitionUtil.getValidationError({
178
+ sources: [makeSource({ alias: "A" })],
179
+ expression: "A".repeat(RECORDING_RULE_MAX_EXPRESSION_LENGTH + 1),
180
+ }),
181
+ ).toContain(
182
+ `${RECORDING_RULE_MAX_EXPRESSION_LENGTH} characters or fewer`,
183
+ );
184
+ });
185
+
186
+ test("rejects an expression referencing an undefined alias", () => {
187
+ expect(
188
+ RecordingRuleDefinitionUtil.getValidationError({
189
+ sources: [makeSource({ alias: "A" })],
190
+ expression: "A + B",
191
+ }),
192
+ ).toContain("references alias 'B'");
193
+ });
194
+
195
+ test("rejects an expression that references no alias", () => {
196
+ expect(
197
+ RecordingRuleDefinitionUtil.getValidationError({
198
+ sources: [makeSource({ alias: "A" })],
199
+ expression: "5 + 10",
200
+ }),
201
+ ).toContain("must reference at least one source alias");
202
+ });
203
+
204
+ test("rejects an expression with characters outside the DSL grammar", () => {
205
+ expect(
206
+ RecordingRuleDefinitionUtil.getValidationError({
207
+ sources: [makeSource({ alias: "A" }), makeSource({ alias: "B" })],
208
+ expression: "A & B",
209
+ }),
210
+ ).toContain("may only contain");
211
+ });
212
+ });
213
+ });
@@ -0,0 +1,6 @@
1
+ enum AIChatMessageRole {
2
+ User = "User",
3
+ Assistant = "Assistant",
4
+ }
5
+
6
+ export default AIChatMessageRole;
@@ -0,0 +1,32 @@
1
+ enum AIChatMessageStatus {
2
+ Pending = "Pending",
3
+ InProgress = "InProgress",
4
+ /*
5
+ * The turn paused because the agent wants to run one or more mutating tools
6
+ * and the conversation is in "Ask for approval" mode. The message carries the
7
+ * pending tool actions; the user approves or denies them to resume the turn.
8
+ */
9
+ WaitingForApproval = "WaitingForApproval",
10
+ Completed = "Completed",
11
+ Error = "Error",
12
+ }
13
+
14
+ export default AIChatMessageStatus;
15
+
16
+ export class AIChatMessageStatusHelper {
17
+ public static isTerminalStatus(status: AIChatMessageStatus): boolean {
18
+ return (
19
+ status === AIChatMessageStatus.Completed ||
20
+ status === AIChatMessageStatus.Error
21
+ );
22
+ }
23
+
24
+ // Statuses in which a run is still in flight (spinner or approval card shown).
25
+ public static isActiveStatus(status: AIChatMessageStatus): boolean {
26
+ return (
27
+ status === AIChatMessageStatus.Pending ||
28
+ status === AIChatMessageStatus.InProgress ||
29
+ status === AIChatMessageStatus.WaitingForApproval
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * How the AI chat agent is allowed to run tools that MUTATE project data
3
+ * (create/acknowledge/resolve incidents and alerts, etc). Read-only tools are
4
+ * never gated by this — they always run. This is stored per-conversation on
5
+ * AIConversation.permissionMode and enforced in ChatAgentRunner.
6
+ */
7
+ enum AIChatPermissionMode {
8
+ /*
9
+ * The agent pauses before every mutating tool and asks the user to approve
10
+ * it. This is the safe default.
11
+ */
12
+ AskForApproval = "AskForApproval",
13
+ /*
14
+ * The agent runs mutating tools immediately (still gated by the user's RBAC
15
+ * permissions). "Bypass permissions" / YOLO mode.
16
+ */
17
+ AutoRun = "AutoRun",
18
+ // Mutating tools are not even offered to the model. The agent can only read.
19
+ ReadOnly = "ReadOnly",
20
+ }
21
+
22
+ export default AIChatPermissionMode;
23
+
24
+ export interface AIChatPermissionModeOption {
25
+ value: AIChatPermissionMode;
26
+ title: string;
27
+ description: string;
28
+ }
29
+
30
+ export class AIChatPermissionModeHelper {
31
+ public static getDefault(): AIChatPermissionMode {
32
+ return AIChatPermissionMode.AskForApproval;
33
+ }
34
+
35
+ public static isValid(
36
+ value: string | undefined,
37
+ ): value is AIChatPermissionMode {
38
+ return Object.values(AIChatPermissionMode).includes(
39
+ value as AIChatPermissionMode,
40
+ );
41
+ }
42
+
43
+ public static parse(value: string | undefined): AIChatPermissionMode {
44
+ return this.isValid(value) ? value : this.getDefault();
45
+ }
46
+
47
+ public static getOptions(): Array<AIChatPermissionModeOption> {
48
+ return [
49
+ {
50
+ value: AIChatPermissionMode.AskForApproval,
51
+ title: "Ask for approval",
52
+ description:
53
+ "The copilot pauses and asks before it changes anything (creating incidents, acknowledging alerts, etc).",
54
+ },
55
+ {
56
+ value: AIChatPermissionMode.AutoRun,
57
+ title: "Auto-run (bypass approvals)",
58
+ description:
59
+ "The copilot performs actions immediately, still limited to what your role can do.",
60
+ },
61
+ {
62
+ value: AIChatPermissionMode.ReadOnly,
63
+ title: "Read-only",
64
+ description:
65
+ "The copilot can investigate and answer questions but can never change anything.",
66
+ },
67
+ ];
68
+ }
69
+ }