palaryn 0.1.0 → 0.3.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 (328) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/audit/logger.d.ts +10 -0
  5. package/dist/src/audit/logger.d.ts.map +1 -1
  6. package/dist/src/audit/logger.js +52 -38
  7. package/dist/src/audit/logger.js.map +1 -1
  8. package/dist/src/auth/routes.js.map +1 -1
  9. package/dist/src/budget/manager.d.ts +5 -0
  10. package/dist/src/budget/manager.d.ts.map +1 -1
  11. package/dist/src/budget/manager.js +32 -0
  12. package/dist/src/budget/manager.js.map +1 -1
  13. package/dist/src/budget/model-pricing.d.ts +20 -0
  14. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  15. package/dist/src/budget/model-pricing.js +107 -0
  16. package/dist/src/budget/model-pricing.js.map +1 -0
  17. package/dist/src/budget/usage-extractor.d.ts +3 -1
  18. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  19. package/dist/src/budget/usage-extractor.js +47 -3
  20. package/dist/src/budget/usage-extractor.js.map +1 -1
  21. package/dist/src/config/defaults.d.ts.map +1 -1
  22. package/dist/src/config/defaults.js +65 -13
  23. package/dist/src/config/defaults.js.map +1 -1
  24. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  25. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  26. package/dist/src/dlp/tool-patterns.js +34 -0
  27. package/dist/src/dlp/tool-patterns.js.map +1 -0
  28. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  29. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  30. package/dist/src/executor/filesystem-executor.js +192 -0
  31. package/dist/src/executor/filesystem-executor.js.map +1 -0
  32. package/dist/src/executor/http-executor.d.ts.map +1 -1
  33. package/dist/src/executor/http-executor.js +4 -0
  34. package/dist/src/executor/http-executor.js.map +1 -1
  35. package/dist/src/executor/index.d.ts +4 -0
  36. package/dist/src/executor/index.d.ts.map +1 -1
  37. package/dist/src/executor/index.js +9 -1
  38. package/dist/src/executor/index.js.map +1 -1
  39. package/dist/src/executor/shell-executor.d.ts +22 -0
  40. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  41. package/dist/src/executor/shell-executor.js +119 -0
  42. package/dist/src/executor/shell-executor.js.map +1 -0
  43. package/dist/src/executor/sql-executor.d.ts +29 -0
  44. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  45. package/dist/src/executor/sql-executor.js +114 -0
  46. package/dist/src/executor/sql-executor.js.map +1 -0
  47. package/dist/src/executor/websocket-executor.d.ts +26 -0
  48. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  49. package/dist/src/executor/websocket-executor.js +205 -0
  50. package/dist/src/executor/websocket-executor.js.map +1 -0
  51. package/dist/src/interceptor/index.d.ts +2 -0
  52. package/dist/src/interceptor/index.d.ts.map +1 -0
  53. package/dist/src/interceptor/index.js +6 -0
  54. package/dist/src/interceptor/index.js.map +1 -0
  55. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  56. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  57. package/dist/src/interceptor/provider-interceptor.js +302 -0
  58. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  59. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  60. package/dist/src/mcp/auth-verifier.js +3 -2
  61. package/dist/src/mcp/auth-verifier.js.map +1 -1
  62. package/dist/src/mcp/bridge.d.ts +14 -10
  63. package/dist/src/mcp/bridge.d.ts.map +1 -1
  64. package/dist/src/mcp/bridge.js +51 -227
  65. package/dist/src/mcp/bridge.js.map +1 -1
  66. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  67. package/dist/src/mcp/http-transport.js +101 -65
  68. package/dist/src/mcp/http-transport.js.map +1 -1
  69. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  70. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  71. package/dist/src/mcp/tool-definitions.js +491 -0
  72. package/dist/src/mcp/tool-definitions.js.map +1 -0
  73. package/dist/src/middleware/auth.js.map +1 -1
  74. package/dist/src/middleware/session.js.map +1 -1
  75. package/dist/src/middleware/validate.d.ts +8 -0
  76. package/dist/src/middleware/validate.d.ts.map +1 -1
  77. package/dist/src/middleware/validate.js +45 -0
  78. package/dist/src/middleware/validate.js.map +1 -1
  79. package/dist/src/policy/engine.d.ts +4 -0
  80. package/dist/src/policy/engine.d.ts.map +1 -1
  81. package/dist/src/policy/engine.js +117 -0
  82. package/dist/src/policy/engine.js.map +1 -1
  83. package/dist/src/saas/routes.d.ts.map +1 -1
  84. package/dist/src/saas/routes.js +327 -10
  85. package/dist/src/saas/routes.js.map +1 -1
  86. package/dist/src/server/app.d.ts.map +1 -1
  87. package/dist/src/server/app.js +19 -2
  88. package/dist/src/server/app.js.map +1 -1
  89. package/dist/src/server/gateway.d.ts.map +1 -1
  90. package/dist/src/server/gateway.js +17 -0
  91. package/dist/src/server/gateway.js.map +1 -1
  92. package/dist/src/server/index.d.ts.map +1 -1
  93. package/dist/src/server/index.js +18 -0
  94. package/dist/src/server/index.js.map +1 -1
  95. package/dist/src/storage/interfaces.d.ts +14 -3
  96. package/dist/src/storage/interfaces.d.ts.map +1 -1
  97. package/dist/src/storage/memory.d.ts +2 -0
  98. package/dist/src/storage/memory.d.ts.map +1 -1
  99. package/dist/src/storage/memory.js +6 -0
  100. package/dist/src/storage/memory.js.map +1 -1
  101. package/dist/src/storage/postgres.d.ts +5 -0
  102. package/dist/src/storage/postgres.d.ts.map +1 -1
  103. package/dist/src/storage/postgres.js +16 -0
  104. package/dist/src/storage/postgres.js.map +1 -1
  105. package/dist/src/storage/redis.d.ts +10 -0
  106. package/dist/src/storage/redis.d.ts.map +1 -1
  107. package/dist/src/storage/redis.js +65 -0
  108. package/dist/src/storage/redis.js.map +1 -1
  109. package/dist/src/types/budget.d.ts +4 -0
  110. package/dist/src/types/budget.d.ts.map +1 -1
  111. package/dist/src/types/config.d.ts +58 -0
  112. package/dist/src/types/config.d.ts.map +1 -1
  113. package/dist/src/types/events.d.ts +1 -0
  114. package/dist/src/types/events.d.ts.map +1 -1
  115. package/dist/src/types/policy.d.ts +11 -1
  116. package/dist/src/types/policy.d.ts.map +1 -1
  117. package/dist/src/types/tool-result.d.ts +11 -0
  118. package/dist/src/types/tool-result.d.ts.map +1 -1
  119. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  120. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  121. package/dist/tests/unit/app-routes.test.js +715 -0
  122. package/dist/tests/unit/app-routes.test.js.map +1 -0
  123. package/dist/tests/unit/audit-logger.test.js +105 -0
  124. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  125. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  126. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  127. package/dist/tests/unit/auth-providers.test.js +279 -0
  128. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  129. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  130. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  131. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  132. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  133. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  134. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  135. package/dist/tests/unit/auth-verifier.test.js +505 -0
  136. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  137. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  138. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  139. package/dist/tests/unit/billing-routes.test.js +432 -0
  140. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  141. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  142. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  143. package/dist/tests/unit/config-defaults.test.js +119 -0
  144. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  145. package/dist/tests/unit/defaults.test.js +0 -10
  146. package/dist/tests/unit/defaults.test.js.map +1 -1
  147. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  148. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  149. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  150. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  151. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  152. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  153. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  154. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  155. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  156. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  157. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  158. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  159. package/dist/tests/unit/logger.test.d.ts +2 -0
  160. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  161. package/dist/tests/unit/logger.test.js +97 -0
  162. package/dist/tests/unit/logger.test.js.map +1 -0
  163. package/dist/tests/unit/metrics.test.js +102 -0
  164. package/dist/tests/unit/metrics.test.js.map +1 -1
  165. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  166. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  167. package/dist/tests/unit/model-pricing.test.js +87 -0
  168. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  169. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  170. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  171. package/dist/tests/unit/oauth-stores.test.js +260 -0
  172. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  173. package/dist/tests/unit/policy-engine.test.js +466 -0
  174. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  175. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  176. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  177. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  178. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  179. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  180. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  181. package/dist/tests/unit/saas-routes-branches.test.js +2040 -0
  182. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  183. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  184. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  185. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  186. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  187. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  188. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  189. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  190. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  191. package/dist/tests/unit/saas-routes.test.js +3 -3
  192. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  193. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  194. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  195. package/dist/tests/unit/shell-executor.test.js +145 -0
  196. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  197. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  198. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  199. package/dist/tests/unit/sql-executor.test.js +177 -0
  200. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  201. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  202. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  203. package/dist/tests/unit/stream-proxy.test.js +147 -0
  204. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  205. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  206. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  207. package/dist/tests/unit/tool-definitions.test.js +184 -0
  208. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  209. package/dist/tests/unit/usage-extractor.test.js +140 -0
  210. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  211. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  212. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  213. package/dist/tests/unit/webhook-handler.test.js +453 -0
  214. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  215. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  216. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  217. package/dist/tests/unit/webhook-routes.test.js +69 -0
  218. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  219. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  220. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  221. package/dist/tests/unit/websocket-executor.test.js +121 -0
  222. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  223. package/package.json +8 -2
  224. package/policy-packs/demo_fail.yaml +41 -0
  225. package/policy-packs/full_tools.yaml +136 -0
  226. package/src/admin/index.ts +1 -0
  227. package/src/admin/routes.ts +509 -0
  228. package/src/admin/templates.ts +572 -0
  229. package/src/anomaly/detector.ts +717 -0
  230. package/src/anomaly/index.ts +1 -0
  231. package/src/approval/manager.ts +569 -0
  232. package/src/approval/webhook.ts +133 -0
  233. package/src/audit/logger.ts +490 -0
  234. package/src/auth/index.ts +5 -0
  235. package/src/auth/password.ts +21 -0
  236. package/src/auth/pkce.ts +22 -0
  237. package/src/auth/providers.ts +208 -0
  238. package/src/auth/routes.ts +521 -0
  239. package/src/auth/session.ts +84 -0
  240. package/src/billing/index.ts +6 -0
  241. package/src/billing/plan-enforcer.ts +135 -0
  242. package/src/billing/routes.ts +229 -0
  243. package/src/billing/stripe-client.ts +58 -0
  244. package/src/billing/webhook-handler.ts +182 -0
  245. package/src/billing/webhook-routes.ts +28 -0
  246. package/src/budget/manager.ts +679 -0
  247. package/src/budget/model-pricing.ts +119 -0
  248. package/src/budget/usage-extractor.ts +214 -0
  249. package/src/cli.ts +91 -0
  250. package/src/config/defaults.ts +261 -0
  251. package/src/config/validate.ts +88 -0
  252. package/src/dlp/composite-scanner.ts +213 -0
  253. package/src/dlp/index.ts +9 -0
  254. package/src/dlp/interfaces.ts +34 -0
  255. package/src/dlp/patterns.ts +30 -0
  256. package/src/dlp/prompt-injection-backend.ts +181 -0
  257. package/src/dlp/prompt-injection-patterns.ts +302 -0
  258. package/src/dlp/regex-backend.ts +181 -0
  259. package/src/dlp/scanner.ts +502 -0
  260. package/src/dlp/text-normalizer.ts +225 -0
  261. package/src/dlp/tool-patterns.ts +35 -0
  262. package/src/dlp/trufflehog-backend.ts +190 -0
  263. package/src/executor/filesystem-executor.ts +196 -0
  264. package/src/executor/http-executor.ts +330 -0
  265. package/src/executor/index.ts +9 -0
  266. package/src/executor/interfaces.ts +11 -0
  267. package/src/executor/noop-executor.ts +23 -0
  268. package/src/executor/registry.ts +64 -0
  269. package/src/executor/shell-executor.ts +148 -0
  270. package/src/executor/slack-executor.ts +176 -0
  271. package/src/executor/sql-executor.ts +146 -0
  272. package/src/executor/websocket-executor.ts +211 -0
  273. package/src/index.ts +24 -0
  274. package/src/interceptor/index.ts +1 -0
  275. package/src/interceptor/provider-interceptor.ts +315 -0
  276. package/src/mcp/auth-verifier.ts +152 -0
  277. package/src/mcp/bridge.ts +703 -0
  278. package/src/mcp/http-transport.ts +672 -0
  279. package/src/mcp/index.ts +9 -0
  280. package/src/mcp/oauth-pages.ts +139 -0
  281. package/src/mcp/oauth-postgres-stores.ts +278 -0
  282. package/src/mcp/oauth-provider.ts +536 -0
  283. package/src/mcp/oauth-stores.ts +202 -0
  284. package/src/mcp/server.ts +55 -0
  285. package/src/mcp/tool-definitions.ts +562 -0
  286. package/src/metrics/collector.ts +357 -0
  287. package/src/metrics/index.ts +1 -0
  288. package/src/middleware/auth.ts +814 -0
  289. package/src/middleware/session.ts +85 -0
  290. package/src/middleware/validate.ts +130 -0
  291. package/src/policy/engine.ts +815 -0
  292. package/src/policy/index.ts +2 -0
  293. package/src/policy/opa-engine.ts +829 -0
  294. package/src/proxy/forward-proxy.ts +649 -0
  295. package/src/proxy/index.ts +1 -0
  296. package/src/ratelimit/limiter.ts +196 -0
  297. package/src/replay/engine.ts +142 -0
  298. package/src/replay/index.ts +1 -0
  299. package/src/saas/index.ts +1 -0
  300. package/src/saas/routes.ts +2161 -0
  301. package/src/server/app.ts +981 -0
  302. package/src/server/errors.ts +49 -0
  303. package/src/server/gateway.ts +1130 -0
  304. package/src/server/index.ts +307 -0
  305. package/src/server/logger.ts +255 -0
  306. package/src/server/stream-proxy.ts +202 -0
  307. package/src/storage/file-persistence.ts +315 -0
  308. package/src/storage/index.ts +4 -0
  309. package/src/storage/interfaces.ts +287 -0
  310. package/src/storage/memory.ts +686 -0
  311. package/src/storage/postgres.ts +1831 -0
  312. package/src/storage/redis.ts +835 -0
  313. package/src/tracing/index.ts +1 -0
  314. package/src/tracing/provider.ts +100 -0
  315. package/src/trust/calculator.ts +141 -0
  316. package/src/trust/index.ts +7 -0
  317. package/src/types/budget.ts +36 -0
  318. package/src/types/config.ts +278 -0
  319. package/src/types/events.ts +41 -0
  320. package/src/types/express.d.ts +14 -0
  321. package/src/types/index.ts +7 -0
  322. package/src/types/policy.ts +83 -0
  323. package/src/types/stripe-config.ts +11 -0
  324. package/src/types/subscription.ts +59 -0
  325. package/src/types/tool-call.ts +47 -0
  326. package/src/types/tool-result.ts +82 -0
  327. package/src/types/user.ts +125 -0
  328. package/tsconfig.json +24 -0
@@ -0,0 +1,717 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { ToolCall } from '../types/tool-call';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Types
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export interface AnomalyConfig {
9
+ enabled: boolean;
10
+ /** Window size for rolling baseline in milliseconds (default: 3600000 = 1 hour) */
11
+ window_ms?: number;
12
+ /** Number of standard deviations before flagging anomaly (default: 3) */
13
+ z_score_threshold?: number;
14
+ /** Minimum number of data points before anomaly detection activates (default: 10) */
15
+ min_samples?: number;
16
+ /** Action on anomaly: 'log' just records, 'flag' adds to result metadata, 'block' denies the call */
17
+ action?: 'log' | 'flag' | 'block';
18
+ /** Per-actor tracking (default: true) */
19
+ track_actors?: boolean;
20
+ /** Per-tool tracking (default: true) */
21
+ track_tools?: boolean;
22
+ /** Per-workspace tracking (default: true) */
23
+ track_workspaces?: boolean;
24
+ }
25
+
26
+ export interface AnomalyAlert {
27
+ alert_id: string;
28
+ timestamp: string;
29
+ anomaly_type: AnomalyType;
30
+ entity_type: 'actor' | 'tool' | 'workspace';
31
+ entity_id: string;
32
+ metric: string;
33
+ current_value: number;
34
+ baseline_mean: number;
35
+ baseline_stddev: number;
36
+ z_score: number;
37
+ severity: 'low' | 'medium' | 'high';
38
+ }
39
+
40
+ export type AnomalyType =
41
+ | 'request_rate_spike'
42
+ | 'error_rate_spike'
43
+ | 'latency_spike'
44
+ | 'new_tool_usage'
45
+ | 'off_hours_activity'
46
+ | 'capability_escalation'
47
+ | 'unusual_payload_size';
48
+
49
+ export interface AnomalyState {
50
+ windows: Record<string, { timestamps: number[]; values: number[] }>;
51
+ actorTools: Record<string, string[]>;
52
+ actorCaps: Record<string, string[]>;
53
+ alerts: AnomalyAlert[];
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // RollingWindow — rolling statistics tracker for a single metric
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export class RollingWindow {
61
+ private timestamps: number[] = [];
62
+ private values: number[] = [];
63
+ private windowMs: number;
64
+
65
+ constructor(windowMs: number) {
66
+ this.windowMs = windowMs;
67
+ }
68
+
69
+ /** Add a data point */
70
+ record(value: number, timestamp?: number): void {
71
+ const now = timestamp || Date.now();
72
+ this.timestamps.push(now);
73
+ this.values.push(value);
74
+ this.prune(now);
75
+ }
76
+
77
+ /** Remove data points outside the window */
78
+ private prune(now: number): void {
79
+ const cutoff = now - this.windowMs;
80
+ while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
81
+ this.timestamps.shift();
82
+ this.values.shift();
83
+ }
84
+ }
85
+
86
+ /** Get count of data points in window */
87
+ count(): number {
88
+ return this.values.length;
89
+ }
90
+
91
+ /** Get mean of values in window */
92
+ mean(): number {
93
+ if (this.values.length === 0) return 0;
94
+ return this.values.reduce((a, b) => a + b, 0) / this.values.length;
95
+ }
96
+
97
+ /** Get standard deviation (sample stddev) */
98
+ stddev(): number {
99
+ if (this.values.length < 2) return 0;
100
+ const m = this.mean();
101
+ const variance = this.values.reduce((sum, v) => sum + Math.pow(v - m, 2), 0) / (this.values.length - 1);
102
+ return Math.sqrt(variance);
103
+ }
104
+
105
+ /** Calculate z-score for a new value */
106
+ zScore(value: number): number {
107
+ const sd = this.stddev();
108
+ if (sd === 0) return 0;
109
+ return Math.abs((value - this.mean()) / sd);
110
+ }
111
+
112
+ /** Get rate (count / window duration in seconds) */
113
+ rate(): number {
114
+ this.prune(Date.now());
115
+ return this.values.length / (this.windowMs / 1000);
116
+ }
117
+
118
+ /** Reset window */
119
+ reset(): void {
120
+ this.timestamps = [];
121
+ this.values = [];
122
+ }
123
+
124
+ /** Serialize window state for persistence */
125
+ serialize(): { timestamps: number[]; values: number[] } {
126
+ return { timestamps: [...this.timestamps], values: [...this.values] };
127
+ }
128
+
129
+ /** Restore window state from serialized data */
130
+ static deserialize(data: { timestamps: number[]; values: number[] }, windowMs: number): RollingWindow {
131
+ const w = new RollingWindow(windowMs);
132
+ w.timestamps = [...data.timestamps];
133
+ w.values = [...data.values];
134
+ return w;
135
+ }
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // AnomalyDetector
140
+ // ---------------------------------------------------------------------------
141
+
142
+ export class AnomalyDetector {
143
+ private config: AnomalyConfig;
144
+
145
+ // Rolling windows keyed by "{entity_type}:{entity_id}:{metric}"
146
+ private windows: Map<string, RollingWindow> = new Map();
147
+
148
+ // Track which tools each actor has used (for new_tool_usage detection)
149
+ private actorToolHistory: Map<string, Set<string>> = new Map();
150
+
151
+ // Track capability levels per actor (for capability_escalation)
152
+ private actorCapabilityHistory: Map<string, Set<string>> = new Map();
153
+
154
+ // Track off-hours alerts to avoid flooding (key: "entity_id:hour")
155
+ private offHoursAlertedThisHour: Set<string> = new Set();
156
+
157
+ // Alert history
158
+ private alerts: AnomalyAlert[] = [];
159
+ private maxAlerts: number = 1000;
160
+
161
+ // Entity limits to prevent unbounded growth
162
+ private maxEntities: number;
163
+
164
+ // Track last update time per entity for eviction
165
+ private windowLastUpdated: Map<string, number> = new Map();
166
+ private actorToolLastUpdated: Map<string, number> = new Map();
167
+ private actorCapLastUpdated: Map<string, number> = new Map();
168
+
169
+ // Periodic cleanup interval
170
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
171
+
172
+ constructor(config: AnomalyConfig, options?: { maxEntities?: number }) {
173
+ this.config = config;
174
+ this.maxEntities = options?.maxEntities ?? 10000;
175
+
176
+ // Start periodic cleanup every 5 minutes
177
+ if (config.enabled) {
178
+ this.cleanupInterval = setInterval(() => this.cleanupStaleEntries(), 5 * 60 * 1000);
179
+ this.cleanupInterval.unref();
180
+ }
181
+ }
182
+
183
+ /** Stop the periodic cleanup interval. Call in tests or on shutdown. */
184
+ destroy(): void {
185
+ if (this.cleanupInterval) {
186
+ clearInterval(this.cleanupInterval);
187
+ this.cleanupInterval = null;
188
+ }
189
+ }
190
+
191
+ /** Remove entries that haven't been updated within the window duration. */
192
+ private cleanupStaleEntries(): void {
193
+ const windowMs = this.config.window_ms || 3600000;
194
+ const cutoff = Date.now() - windowMs;
195
+
196
+ for (const [key, lastUpdate] of this.windowLastUpdated) {
197
+ if (lastUpdate < cutoff) {
198
+ this.windows.delete(key);
199
+ this.windowLastUpdated.delete(key);
200
+ }
201
+ }
202
+
203
+ for (const [key, lastUpdate] of this.actorToolLastUpdated) {
204
+ if (lastUpdate < cutoff) {
205
+ this.actorToolHistory.delete(key);
206
+ this.actorToolLastUpdated.delete(key);
207
+ }
208
+ }
209
+
210
+ for (const [key, lastUpdate] of this.actorCapLastUpdated) {
211
+ if (lastUpdate < cutoff) {
212
+ this.actorCapabilityHistory.delete(key);
213
+ this.actorCapLastUpdated.delete(key);
214
+ }
215
+ }
216
+
217
+ // Clean up stale off-hours throttle keys
218
+ this.offHoursAlertedThisHour.clear();
219
+ }
220
+
221
+ /** Evict the oldest entry from a map+timestamps pair when over limit. */
222
+ private evictOldest(timestamps: Map<string, number>, dataMap: Map<string, any>): void {
223
+ let oldestKey: string | null = null;
224
+ let oldestTime = Infinity;
225
+ for (const [key, time] of timestamps) {
226
+ if (time < oldestTime) {
227
+ oldestTime = time;
228
+ oldestKey = key;
229
+ }
230
+ }
231
+ if (oldestKey) {
232
+ timestamps.delete(oldestKey);
233
+ dataMap.delete(oldestKey);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Analyze a tool call for anomalies. Called in the gateway pipeline.
239
+ * Returns an array of anomaly alerts (empty if no anomalies detected).
240
+ */
241
+ analyze(toolCall: ToolCall): AnomalyAlert[] {
242
+ if (!this.config.enabled) return [];
243
+
244
+ const alerts: AnomalyAlert[] = [];
245
+ const now = Date.now();
246
+
247
+ // 1. Request rate tracking (per actor, per tool, per workspace)
248
+ if (this.config.track_actors !== false) {
249
+ alerts.push(...this.checkRequestRate('actor', toolCall.actor.id, now));
250
+ }
251
+ if (this.config.track_tools !== false) {
252
+ alerts.push(...this.checkRequestRate('tool', toolCall.tool.name, now));
253
+ }
254
+ if (this.config.track_workspaces !== false) {
255
+ alerts.push(...this.checkRequestRate('workspace', toolCall.workspace_id, now));
256
+ }
257
+
258
+ // 2. New tool usage detection
259
+ if (this.config.track_actors !== false) {
260
+ const newToolAlert = this.checkNewToolUsage(toolCall.actor.id, toolCall.tool.name);
261
+ if (newToolAlert) alerts.push(newToolAlert);
262
+ }
263
+
264
+ // 3. Capability escalation detection
265
+ if (this.config.track_actors !== false) {
266
+ const capAlert = this.checkCapabilityEscalation(toolCall.actor.id, toolCall.tool.capability);
267
+ if (capAlert) alerts.push(capAlert);
268
+ }
269
+
270
+ // 4. Off-hours activity (weekends or 22:00-06:00 UTC)
271
+ const offHoursAlert = this.checkOffHours(toolCall.actor.id, now);
272
+ if (offHoursAlert) alerts.push(offHoursAlert);
273
+
274
+ // Store alerts
275
+ for (const alert of alerts) {
276
+ this.alerts.push(alert);
277
+ if (this.alerts.length > this.maxAlerts) {
278
+ this.alerts.shift();
279
+ }
280
+ }
281
+
282
+ return alerts;
283
+ }
284
+
285
+ /**
286
+ * Record the result of a tool execution for latency/error tracking.
287
+ * Called after execution completes.
288
+ */
289
+ recordResult(toolCall: ToolCall, durationMs: number, isError: boolean): void {
290
+ if (!this.config.enabled) return;
291
+
292
+ // Track latency per tool
293
+ const latencyKey = `tool:${toolCall.tool.name}:latency`;
294
+ this.getOrCreateWindow(latencyKey).record(durationMs);
295
+
296
+ // Track error rate per actor
297
+ const errorKey = `actor:${toolCall.actor.id}:error_rate`;
298
+ this.getOrCreateWindow(errorKey).record(isError ? 1 : 0);
299
+ }
300
+
301
+ /**
302
+ * Check if a new latency/error reading is anomalous.
303
+ * Returns alerts for latency spikes and error rate spikes.
304
+ */
305
+ analyzeResult(toolCall: ToolCall, durationMs: number, isError: boolean): AnomalyAlert[] {
306
+ if (!this.config.enabled) return [];
307
+
308
+ const alerts: AnomalyAlert[] = [];
309
+ const threshold = this.config.z_score_threshold || 3;
310
+ const minSamples = this.config.min_samples || 10;
311
+
312
+ // Check latency anomaly
313
+ const latencyKey = `tool:${toolCall.tool.name}:latency`;
314
+ const latencyWindow = this.getOrCreateWindow(latencyKey);
315
+ if (latencyWindow.count() >= minSamples) {
316
+ const z = latencyWindow.zScore(durationMs);
317
+ if (z > threshold) {
318
+ alerts.push(this.createAlert(
319
+ 'latency_spike', 'tool', toolCall.tool.name,
320
+ 'latency_ms', durationMs, latencyWindow.mean(), latencyWindow.stddev(), z
321
+ ));
322
+ }
323
+ }
324
+
325
+ // Check error rate anomaly
326
+ if (isError) {
327
+ const errorKey = `actor:${toolCall.actor.id}:error_rate`;
328
+ const errorWindow = this.getOrCreateWindow(errorKey);
329
+ if (errorWindow.count() >= minSamples) {
330
+ const errorRate = errorWindow.mean();
331
+ // If error rate was low (actor mostly succeeds) and now seeing errors
332
+ if (errorRate < 0.1 && errorWindow.count() > minSamples) {
333
+ const sd = errorWindow.stddev();
334
+ const z = sd > 0 ? (1 - errorRate) / sd : threshold + 1;
335
+ alerts.push(this.createAlert(
336
+ 'error_rate_spike', 'actor', toolCall.actor.id,
337
+ 'error_rate', 1, errorRate, sd, z
338
+ ));
339
+ }
340
+ }
341
+ }
342
+
343
+ // Store alerts
344
+ for (const alert of alerts) {
345
+ this.alerts.push(alert);
346
+ if (this.alerts.length > this.maxAlerts) {
347
+ this.alerts.shift();
348
+ }
349
+ }
350
+
351
+ return alerts;
352
+ }
353
+
354
+ /** Get recent alerts */
355
+ getAlerts(limit?: number): AnomalyAlert[] {
356
+ const n = limit || 100;
357
+ return this.alerts.slice(-n);
358
+ }
359
+
360
+ /** Get alerts for a specific entity */
361
+ getAlertsForEntity(entityType: string, entityId: string): AnomalyAlert[] {
362
+ return this.alerts.filter(a => a.entity_type === entityType && a.entity_id === entityId);
363
+ }
364
+
365
+ /** Get baseline stats for an entity+metric */
366
+ getBaseline(entityType: string, entityId: string, metric: string): { mean: number; stddev: number; count: number } | null {
367
+ const key = `${entityType}:${entityId}:${metric}`;
368
+ const window = this.windows.get(key);
369
+ if (!window || window.count() === 0) return null;
370
+ return { mean: window.mean(), stddev: window.stddev(), count: window.count() };
371
+ }
372
+
373
+ /** Get a baseline report suitable for the anomaly radar dashboard widget */
374
+ getBaselineReport(): {
375
+ current: Record<string, number>;
376
+ baseline: Record<string, number>;
377
+ alerts: AnomalyAlert[];
378
+ } {
379
+ const current: Record<string, number> = {
380
+ request_rate: 0,
381
+ error_rate: 0,
382
+ latency: 0,
383
+ new_tools: 0,
384
+ capability_escalations: 0,
385
+ off_hours: 0,
386
+ };
387
+ const baseline: Record<string, number> = {
388
+ request_rate: 0,
389
+ error_rate: 0,
390
+ latency: 0,
391
+ new_tools: 0,
392
+ capability_escalations: 0,
393
+ off_hours: 0,
394
+ };
395
+
396
+ // Aggregate request_rate from actor request rate windows
397
+ let rateCount = 0;
398
+ let rateSum = 0;
399
+ let rateMeanSum = 0;
400
+ for (const [key, window] of this.windows) {
401
+ if (key.endsWith(':request_rate')) {
402
+ rateSum += window.rate();
403
+ rateMeanSum += window.mean();
404
+ rateCount++;
405
+ }
406
+ }
407
+ if (rateCount > 0) {
408
+ current.request_rate = rateSum / rateCount;
409
+ baseline.request_rate = rateMeanSum / rateCount;
410
+ }
411
+
412
+ // Aggregate error_rate from actor error rate windows
413
+ let errorCount = 0;
414
+ let errorSum = 0;
415
+ let errorMeanSum = 0;
416
+ for (const [key, window] of this.windows) {
417
+ if (key.endsWith(':error_rate')) {
418
+ errorSum += window.mean(); // current error rate is the mean of recent 0/1 values
419
+ errorMeanSum += window.mean();
420
+ errorCount++;
421
+ }
422
+ }
423
+ if (errorCount > 0) {
424
+ current.error_rate = errorSum / errorCount;
425
+ baseline.error_rate = errorMeanSum / errorCount;
426
+ }
427
+
428
+ // Aggregate latency from tool latency windows
429
+ let latCount = 0;
430
+ let latSum = 0;
431
+ let latMeanSum = 0;
432
+ for (const [key, window] of this.windows) {
433
+ if (key.endsWith(':latency')) {
434
+ // Current value: use the mean of recent readings as the "current" value
435
+ latSum += window.mean();
436
+ latMeanSum += window.mean();
437
+ latCount++;
438
+ }
439
+ }
440
+ if (latCount > 0) {
441
+ current.latency = latSum / latCount;
442
+ baseline.latency = latMeanSum / latCount;
443
+ }
444
+
445
+ // Count alert types from alert history
446
+ current.new_tools = this.alerts.filter(a => a.anomaly_type === 'new_tool_usage').length;
447
+ current.capability_escalations = this.alerts.filter(a => a.anomaly_type === 'capability_escalation').length;
448
+ current.off_hours = this.alerts.filter(a => a.anomaly_type === 'off_hours_activity').length;
449
+
450
+ return {
451
+ current,
452
+ baseline,
453
+ alerts: [...this.alerts],
454
+ };
455
+ }
456
+
457
+ /** Reset all tracking data */
458
+ reset(): void {
459
+ this.windows.clear();
460
+ this.windowLastUpdated.clear();
461
+ this.actorToolHistory.clear();
462
+ this.actorToolLastUpdated.clear();
463
+ this.actorCapabilityHistory.clear();
464
+ this.actorCapLastUpdated.clear();
465
+ this.offHoursAlertedThisHour.clear();
466
+ this.alerts = [];
467
+ }
468
+
469
+ /** Export all state for persistence. Returns a JSON-serializable object. */
470
+ exportState(): AnomalyState {
471
+ const windows: Record<string, { timestamps: number[]; values: number[] }> = {};
472
+ for (const [key, window] of this.windows) {
473
+ windows[key] = window.serialize();
474
+ }
475
+ const actorTools: Record<string, string[]> = {};
476
+ for (const [actor, tools] of this.actorToolHistory) {
477
+ actorTools[actor] = [...tools];
478
+ }
479
+ const actorCaps: Record<string, string[]> = {};
480
+ for (const [actor, caps] of this.actorCapabilityHistory) {
481
+ actorCaps[actor] = [...caps];
482
+ }
483
+ return { windows, actorTools, actorCaps, alerts: [...this.alerts] };
484
+ }
485
+
486
+ /** Import state from a previously exported snapshot. */
487
+ importState(state: AnomalyState): void {
488
+ const windowMs = this.config.window_ms || 3600000;
489
+ this.windows.clear();
490
+ for (const [key, data] of Object.entries(state.windows)) {
491
+ this.windows.set(key, RollingWindow.deserialize(data, windowMs));
492
+ }
493
+ this.actorToolHistory.clear();
494
+ for (const [actor, tools] of Object.entries(state.actorTools)) {
495
+ this.actorToolHistory.set(actor, new Set(tools));
496
+ }
497
+ this.actorCapabilityHistory.clear();
498
+ for (const [actor, caps] of Object.entries(state.actorCaps)) {
499
+ this.actorCapabilityHistory.set(actor, new Set(caps));
500
+ }
501
+ this.alerts = [...state.alerts];
502
+ }
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // Private helpers
506
+ // ---------------------------------------------------------------------------
507
+
508
+ private getOrCreateWindow(key: string): RollingWindow {
509
+ let window = this.windows.get(key);
510
+ if (!window) {
511
+ // Evict oldest if at capacity
512
+ if (this.windows.size >= this.maxEntities) {
513
+ this.evictOldest(this.windowLastUpdated, this.windows);
514
+ }
515
+ window = new RollingWindow(this.config.window_ms || 3600000);
516
+ this.windows.set(key, window);
517
+ }
518
+ this.windowLastUpdated.set(key, Date.now());
519
+ return window;
520
+ }
521
+
522
+ private checkRequestRate(entityType: 'actor' | 'tool' | 'workspace', entityId: string, now: number): AnomalyAlert[] {
523
+ const alerts: AnomalyAlert[] = [];
524
+ const threshold = this.config.z_score_threshold || 3;
525
+ const minSamples = this.config.min_samples || 10;
526
+
527
+ const rateKey = `${entityType}:${entityId}:request_rate`;
528
+ const window = this.getOrCreateWindow(rateKey);
529
+
530
+ // Before recording this data point, check if the new rate would be anomalous
531
+ // We use inter-arrival intervals to detect rate spikes:
532
+ // Record a value of 1 for each request, then check the count-based rate
533
+ const countBefore = window.count();
534
+
535
+ // Record this request
536
+ window.record(1, now);
537
+
538
+ // Only check for anomalies if we have enough samples
539
+ if (countBefore >= minSamples) {
540
+ // Use a separate window for interval tracking
541
+ const intervalKey = `${entityType}:${entityId}:request_interval`;
542
+ const intervalWindow = this.getOrCreateWindow(intervalKey);
543
+
544
+ if (intervalWindow.count() >= minSamples) {
545
+ // Compute current rate as count in last sub-window
546
+ const currentRate = window.count();
547
+ const z = intervalWindow.zScore(currentRate);
548
+ if (z > threshold) {
549
+ alerts.push(this.createAlert(
550
+ 'request_rate_spike', entityType, entityId,
551
+ 'request_rate', currentRate, intervalWindow.mean(), intervalWindow.stddev(), z
552
+ ));
553
+ }
554
+ }
555
+
556
+ // Record the current count as a data point for interval tracking
557
+ intervalWindow.record(window.count(), now);
558
+ } else {
559
+ // Bootstrap: record the count
560
+ const intervalKey = `${entityType}:${entityId}:request_interval`;
561
+ const intervalWindow = this.getOrCreateWindow(intervalKey);
562
+ intervalWindow.record(window.count(), now);
563
+ }
564
+
565
+ return alerts;
566
+ }
567
+
568
+ private checkNewToolUsage(actorId: string, toolName: string): AnomalyAlert | null {
569
+ let tools = this.actorToolHistory.get(actorId);
570
+ if (!tools) {
571
+ // Evict oldest if at capacity
572
+ if (this.actorToolHistory.size >= this.maxEntities) {
573
+ this.evictOldest(this.actorToolLastUpdated, this.actorToolHistory);
574
+ }
575
+ tools = new Set<string>();
576
+ this.actorToolHistory.set(actorId, tools);
577
+ }
578
+ this.actorToolLastUpdated.set(actorId, Date.now());
579
+
580
+ if (tools.has(toolName)) {
581
+ return null; // Already used this tool before
582
+ }
583
+
584
+ // This is a new tool for this actor
585
+ const isFirstEver = tools.size === 0;
586
+ tools.add(toolName);
587
+
588
+ // Don't alert on the very first tool an actor uses (that's normal bootstrapping)
589
+ if (isFirstEver) {
590
+ return null;
591
+ }
592
+
593
+ return this.createAlert(
594
+ 'new_tool_usage', 'actor', actorId,
595
+ 'new_tool', 1, 0, 0, 0,
596
+ 'low'
597
+ );
598
+ }
599
+
600
+ private checkCapabilityEscalation(actorId: string, capability: string): AnomalyAlert | null {
601
+ let caps = this.actorCapabilityHistory.get(actorId);
602
+ if (!caps) {
603
+ // Evict oldest if at capacity
604
+ if (this.actorCapabilityHistory.size >= this.maxEntities) {
605
+ this.evictOldest(this.actorCapLastUpdated, this.actorCapabilityHistory);
606
+ }
607
+ caps = new Set<string>();
608
+ this.actorCapabilityHistory.set(actorId, caps);
609
+ }
610
+ this.actorCapLastUpdated.set(actorId, Date.now());
611
+
612
+ if (caps.has(capability)) {
613
+ return null; // Already used this capability
614
+ }
615
+
616
+ const isFirstEver = caps.size === 0;
617
+ caps.add(capability);
618
+
619
+ // Don't alert on the first capability
620
+ if (isFirstEver) {
621
+ return null;
622
+ }
623
+
624
+ // Determine severity based on escalation level
625
+ const capOrder: Record<string, number> = { read: 0, write: 1, delete: 2, admin: 3 };
626
+ const newLevel = capOrder[capability] ?? 0;
627
+
628
+ // Find the max level previously used
629
+ let maxPrevLevel = 0;
630
+ for (const prevCap of caps) {
631
+ if (prevCap !== capability) {
632
+ const level = capOrder[prevCap] ?? 0;
633
+ if (level > maxPrevLevel) maxPrevLevel = level;
634
+ }
635
+ }
636
+
637
+ // Only alert if this is an escalation (higher than any previous capability)
638
+ if (newLevel <= maxPrevLevel) {
639
+ return null;
640
+ }
641
+
642
+ let severity: 'low' | 'medium' | 'high' = 'low';
643
+ if (capability === 'admin') {
644
+ severity = 'high';
645
+ } else if (capability === 'delete') {
646
+ severity = 'medium';
647
+ } else if (capability === 'write' && maxPrevLevel === 0) {
648
+ severity = 'low';
649
+ }
650
+
651
+ return this.createAlert(
652
+ 'capability_escalation', 'actor', actorId,
653
+ 'capability_level', newLevel, maxPrevLevel, 0, 0,
654
+ severity
655
+ );
656
+ }
657
+
658
+ private checkOffHours(actorId: string, now: number): AnomalyAlert | null {
659
+ const date = new Date(now);
660
+ const hour = date.getUTCHours();
661
+ const day = date.getUTCDay(); // 0 = Sunday, 6 = Saturday
662
+
663
+ const isOffHours = hour >= 22 || hour < 6;
664
+ const isWeekend = day === 0 || day === 6;
665
+
666
+ if (!isOffHours && !isWeekend) {
667
+ return null;
668
+ }
669
+
670
+ // Throttle: only alert once per entity per hour
671
+ const hourKey = `${actorId}:${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}-${hour}`;
672
+ if (this.offHoursAlertedThisHour.has(hourKey)) {
673
+ return null;
674
+ }
675
+ this.offHoursAlertedThisHour.add(hourKey);
676
+
677
+ const severity: 'low' | 'medium' | 'high' = isWeekend && isOffHours ? 'medium' : 'low';
678
+
679
+ return this.createAlert(
680
+ 'off_hours_activity', 'actor', actorId,
681
+ 'hour_utc', hour, 12, 0, 0,
682
+ severity
683
+ );
684
+ }
685
+
686
+ private createAlert(
687
+ anomalyType: AnomalyType,
688
+ entityType: 'actor' | 'tool' | 'workspace',
689
+ entityId: string,
690
+ metric: string,
691
+ currentValue: number,
692
+ baselineMean: number,
693
+ baselineStddev: number,
694
+ zScore: number,
695
+ overrideSeverity?: 'low' | 'medium' | 'high',
696
+ ): AnomalyAlert {
697
+ return {
698
+ alert_id: randomUUID(),
699
+ timestamp: new Date().toISOString(),
700
+ anomaly_type: anomalyType,
701
+ entity_type: entityType,
702
+ entity_id: entityId,
703
+ metric,
704
+ current_value: currentValue,
705
+ baseline_mean: baselineMean,
706
+ baseline_stddev: baselineStddev,
707
+ z_score: zScore,
708
+ severity: overrideSeverity || this.classifySeverity(zScore),
709
+ };
710
+ }
711
+
712
+ private classifySeverity(zScore: number): 'low' | 'medium' | 'high' {
713
+ if (zScore >= 6) return 'high';
714
+ if (zScore >= 4) return 'medium';
715
+ return 'low';
716
+ }
717
+ }