@oneuptime/common 11.5.7 → 11.5.8

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 (30) hide show
  1. package/Models/DatabaseModels/AIRun.ts +31 -0
  2. package/Server/API/CodeFixRunAPI.ts +25 -74
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.ts +41 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Middleware/MasterAdminAuthorization.ts +1 -1
  6. package/Server/Middleware/ProjectAuthorization.ts +1 -1
  7. package/Server/Services/AIRunService.ts +10 -11
  8. package/Server/Utils/AI/AIRunPrivacyFilter.ts +106 -0
  9. package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +295 -0
  10. package/Types/Email/EmailTemplateType.ts +2 -0
  11. package/UI/Components/EditionLabel/EditionLabel.tsx +821 -331
  12. package/build/dist/Models/DatabaseModels/AIRun.js +31 -0
  13. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  14. package/build/dist/Server/API/CodeFixRunAPI.js +25 -53
  15. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  16. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js +36 -0
  17. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js.map +1 -0
  18. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  19. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  20. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +1 -1
  21. package/build/dist/Server/Middleware/ProjectAuthorization.js +1 -1
  22. package/build/dist/Server/Services/AIRunService.js +10 -3
  23. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  24. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +82 -0
  25. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -0
  26. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  27. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  28. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +424 -139
  29. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  30. package/package.json +1 -1
@@ -66,6 +66,7 @@ import CodeFixTaskContext from "../../Types/AI/CodeFixTaskContext";
66
66
  Permission.ProjectOwner,
67
67
  Permission.ProjectAdmin,
68
68
  Permission.ProjectMember,
69
+ Permission.Viewer,
69
70
  ],
70
71
  delete: [],
71
72
  update: [],
@@ -77,6 +78,7 @@ export default class AIRun extends BaseModel {
77
78
  Permission.ProjectOwner,
78
79
  Permission.ProjectAdmin,
79
80
  Permission.ProjectMember,
81
+ Permission.Viewer,
80
82
  ],
81
83
  update: [],
82
84
  })
@@ -109,6 +111,7 @@ export default class AIRun extends BaseModel {
109
111
  Permission.ProjectOwner,
110
112
  Permission.ProjectAdmin,
111
113
  Permission.ProjectMember,
114
+ Permission.Viewer,
112
115
  ],
113
116
  update: [],
114
117
  })
@@ -133,6 +136,7 @@ export default class AIRun extends BaseModel {
133
136
  Permission.ProjectOwner,
134
137
  Permission.ProjectAdmin,
135
138
  Permission.ProjectMember,
139
+ Permission.Viewer,
136
140
  ],
137
141
  update: [],
138
142
  })
@@ -157,6 +161,7 @@ export default class AIRun extends BaseModel {
157
161
  Permission.ProjectOwner,
158
162
  Permission.ProjectAdmin,
159
163
  Permission.ProjectMember,
164
+ Permission.Viewer,
160
165
  ],
161
166
  update: [],
162
167
  })
@@ -181,6 +186,7 @@ export default class AIRun extends BaseModel {
181
186
  Permission.ProjectOwner,
182
187
  Permission.ProjectAdmin,
183
188
  Permission.ProjectMember,
189
+ Permission.Viewer,
184
190
  ],
185
191
  update: [],
186
192
  })
@@ -206,6 +212,7 @@ export default class AIRun extends BaseModel {
206
212
  Permission.ProjectOwner,
207
213
  Permission.ProjectAdmin,
208
214
  Permission.ProjectMember,
215
+ Permission.Viewer,
209
216
  ],
210
217
  update: [],
211
218
  })
@@ -238,6 +245,7 @@ export default class AIRun extends BaseModel {
238
245
  Permission.ProjectOwner,
239
246
  Permission.ProjectAdmin,
240
247
  Permission.ProjectMember,
248
+ Permission.Viewer,
241
249
  ],
242
250
  update: [],
243
251
  })
@@ -262,6 +270,7 @@ export default class AIRun extends BaseModel {
262
270
  Permission.ProjectOwner,
263
271
  Permission.ProjectAdmin,
264
272
  Permission.ProjectMember,
273
+ Permission.Viewer,
265
274
  ],
266
275
  update: [],
267
276
  })
@@ -294,6 +303,7 @@ export default class AIRun extends BaseModel {
294
303
  Permission.ProjectOwner,
295
304
  Permission.ProjectAdmin,
296
305
  Permission.ProjectMember,
306
+ Permission.Viewer,
297
307
  ],
298
308
  update: [],
299
309
  })
@@ -318,6 +328,7 @@ export default class AIRun extends BaseModel {
318
328
  Permission.ProjectOwner,
319
329
  Permission.ProjectAdmin,
320
330
  Permission.ProjectMember,
331
+ Permission.Viewer,
321
332
  ],
322
333
  update: [],
323
334
  })
@@ -343,6 +354,7 @@ export default class AIRun extends BaseModel {
343
354
  Permission.ProjectOwner,
344
355
  Permission.ProjectAdmin,
345
356
  Permission.ProjectMember,
357
+ Permission.Viewer,
346
358
  ],
347
359
  update: [],
348
360
  })
@@ -368,6 +380,7 @@ export default class AIRun extends BaseModel {
368
380
  Permission.ProjectOwner,
369
381
  Permission.ProjectAdmin,
370
382
  Permission.ProjectMember,
383
+ Permission.Viewer,
371
384
  ],
372
385
  update: [],
373
386
  })
@@ -397,6 +410,7 @@ export default class AIRun extends BaseModel {
397
410
  Permission.ProjectOwner,
398
411
  Permission.ProjectAdmin,
399
412
  Permission.ProjectMember,
413
+ Permission.Viewer,
400
414
  ],
401
415
  update: [],
402
416
  })
@@ -422,6 +436,7 @@ export default class AIRun extends BaseModel {
422
436
  Permission.ProjectOwner,
423
437
  Permission.ProjectAdmin,
424
438
  Permission.ProjectMember,
439
+ Permission.Viewer,
425
440
  ],
426
441
  update: [],
427
442
  })
@@ -447,6 +462,7 @@ export default class AIRun extends BaseModel {
447
462
  Permission.ProjectOwner,
448
463
  Permission.ProjectAdmin,
449
464
  Permission.ProjectMember,
465
+ Permission.Viewer,
450
466
  ],
451
467
  update: [],
452
468
  })
@@ -472,6 +488,7 @@ export default class AIRun extends BaseModel {
472
488
  Permission.ProjectOwner,
473
489
  Permission.ProjectAdmin,
474
490
  Permission.ProjectMember,
491
+ Permission.Viewer,
475
492
  ],
476
493
  update: [],
477
494
  })
@@ -497,6 +514,7 @@ export default class AIRun extends BaseModel {
497
514
  Permission.ProjectOwner,
498
515
  Permission.ProjectAdmin,
499
516
  Permission.ProjectMember,
517
+ Permission.Viewer,
500
518
  ],
501
519
  update: [],
502
520
  })
@@ -518,6 +536,7 @@ export default class AIRun extends BaseModel {
518
536
  Permission.ProjectOwner,
519
537
  Permission.ProjectAdmin,
520
538
  Permission.ProjectMember,
539
+ Permission.Viewer,
521
540
  ],
522
541
  update: [],
523
542
  })
@@ -539,6 +558,7 @@ export default class AIRun extends BaseModel {
539
558
  Permission.ProjectOwner,
540
559
  Permission.ProjectAdmin,
541
560
  Permission.ProjectMember,
561
+ Permission.Viewer,
542
562
  ],
543
563
  update: [],
544
564
  })
@@ -561,6 +581,7 @@ export default class AIRun extends BaseModel {
561
581
  Permission.ProjectOwner,
562
582
  Permission.ProjectAdmin,
563
583
  Permission.ProjectMember,
584
+ Permission.Viewer,
564
585
  ],
565
586
  update: [],
566
587
  })
@@ -583,6 +604,7 @@ export default class AIRun extends BaseModel {
583
604
  Permission.ProjectOwner,
584
605
  Permission.ProjectAdmin,
585
606
  Permission.ProjectMember,
607
+ Permission.Viewer,
586
608
  ],
587
609
  update: [],
588
610
  })
@@ -605,6 +627,7 @@ export default class AIRun extends BaseModel {
605
627
  Permission.ProjectOwner,
606
628
  Permission.ProjectAdmin,
607
629
  Permission.ProjectMember,
630
+ Permission.Viewer,
608
631
  ],
609
632
  update: [],
610
633
  })
@@ -627,6 +650,7 @@ export default class AIRun extends BaseModel {
627
650
  Permission.ProjectOwner,
628
651
  Permission.ProjectAdmin,
629
652
  Permission.ProjectMember,
653
+ Permission.Viewer,
630
654
  ],
631
655
  update: [],
632
656
  })
@@ -649,6 +673,7 @@ export default class AIRun extends BaseModel {
649
673
  Permission.ProjectOwner,
650
674
  Permission.ProjectAdmin,
651
675
  Permission.ProjectMember,
676
+ Permission.Viewer,
652
677
  ],
653
678
  update: [],
654
679
  })
@@ -724,6 +749,7 @@ export default class AIRun extends BaseModel {
724
749
  Permission.ProjectOwner,
725
750
  Permission.ProjectAdmin,
726
751
  Permission.ProjectMember,
752
+ Permission.Viewer,
727
753
  ],
728
754
  update: [],
729
755
  })
@@ -753,6 +779,7 @@ export default class AIRun extends BaseModel {
753
779
  Permission.ProjectOwner,
754
780
  Permission.ProjectAdmin,
755
781
  Permission.ProjectMember,
782
+ Permission.Viewer,
756
783
  ],
757
784
  update: [],
758
785
  })
@@ -777,6 +804,7 @@ export default class AIRun extends BaseModel {
777
804
  Permission.ProjectOwner,
778
805
  Permission.ProjectAdmin,
779
806
  Permission.ProjectMember,
807
+ Permission.Viewer,
780
808
  ],
781
809
  update: [],
782
810
  })
@@ -798,6 +826,7 @@ export default class AIRun extends BaseModel {
798
826
  Permission.ProjectOwner,
799
827
  Permission.ProjectAdmin,
800
828
  Permission.ProjectMember,
829
+ Permission.Viewer,
801
830
  ],
802
831
  update: [],
803
832
  })
@@ -828,6 +857,7 @@ export default class AIRun extends BaseModel {
828
857
  Permission.ProjectOwner,
829
858
  Permission.ProjectAdmin,
830
859
  Permission.ProjectMember,
860
+ Permission.Viewer,
831
861
  ],
832
862
  update: [],
833
863
  })
@@ -852,6 +882,7 @@ export default class AIRun extends BaseModel {
852
882
  Permission.ProjectOwner,
853
883
  Permission.ProjectAdmin,
854
884
  Permission.ProjectMember,
885
+ Permission.Viewer,
855
886
  ],
856
887
  update: [],
857
888
  })
@@ -9,11 +9,9 @@ import Express, {
9
9
  import Response from "../Utils/Response";
10
10
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
11
11
  import ObjectID from "../../Types/ObjectID";
12
- import PositiveNumber from "../../Types/PositiveNumber";
13
12
  import BadDataException from "../../Types/Exception/BadDataException";
14
13
  import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
15
14
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
16
- import Query from "../../Types/BaseDatabase/Query";
17
15
  import Select from "../Types/Database/Select";
18
16
  import { JSONObject } from "../../Types/JSON";
19
17
  import AIRunType from "../../Types/AI/AIRunType";
@@ -26,19 +24,26 @@ import ProjectService from "../Services/ProjectService";
26
24
  import AIRunService from "../Services/AIRunService";
27
25
  import AIRunEventService from "../Services/AIRunEventService";
28
26
 
29
- const DEFAULT_LIMIT: number = 25;
30
- const MAX_LIMIT: number = 100;
31
27
  const MAX_EVENTS: number = 500;
32
28
 
33
29
  /*
34
- * Dedicated read endpoints for the dashboard's AI fix-task pages.
30
+ * The AI fix task detail page's event trail.
35
31
  *
36
- * Code-fix runs are system-authored AIRuns (runType CodeFix, userId = null)
37
- * and are therefore hidden by the per-user privacy pin on the generic
38
- * AIRun / AIRunEvent CRUD. So access is gated explicitly here — the same
39
- * pattern as AIInvestigationAPI: first confirm the caller can read the
40
- * parent object (the project) under THEIR permissions, then read the runs
41
- * and their events as root.
32
+ * The LIST page does NOT come through here it reads AIRuns through the
33
+ * standard /ai-run CRUD, which AIRunService opens up for code-fix runs via a
34
+ * forced `runType = CodeFix OR userId = <caller>` clause (see
35
+ * Utils/AI/AIRunPrivacyFilter).
36
+ *
37
+ * The events cannot follow. AIRunEvent has NO runType column to key that
38
+ * clause off, and "userId is null" does not mean "system-authored" — the
39
+ * investigation engine writes null-userId events too — so project-scoping
40
+ * AIRunEvent wholesale would expose other members' CHAT toolArguments, which
41
+ * carry user-typed prompt content. Events are therefore still reached only
42
+ * through a run this endpoint has already authorized as runType = CodeFix.
43
+ *
44
+ * Access is gated explicitly, the same pattern as AIInvestigationAPI: confirm
45
+ * the caller can read the parent object (the project) under THEIR permissions,
46
+ * then read the run and its events as root.
42
47
  */
43
48
 
44
49
  const RUN_SELECT: Select<AIRun> = {
@@ -51,6 +56,15 @@ const RUN_SELECT: Select<AIRun> = {
51
56
  triggeredByTelemetryExceptionId: true,
52
57
  totalTokens: true,
53
58
  codeFixTaskType: true,
59
+ attemptCount: true,
60
+ llmCallCount: true,
61
+ toolCallCount: true,
62
+ /*
63
+ * Deliberately NOT selected: totalCostInUSDCents. This endpoint's gate is
64
+ * Project read, which admits roles beyond AIRun's own table ACL, so it must
65
+ * not return more than the detail page renders — and nothing there renders
66
+ * per-run spend.
67
+ */
54
68
  };
55
69
 
56
70
  /*
@@ -81,69 +95,6 @@ export default class CodeFixRunAPI {
81
95
  }
82
96
 
83
97
  private initRoutes(): void {
84
- // List the project's code-fix runs, newest first.
85
- this.router.post(
86
- "/code-fix-run/list",
87
- UserMiddleware.getUserMiddleware,
88
- async (
89
- req: ExpressRequest,
90
- res: ExpressResponse,
91
- next: NextFunction,
92
- ): Promise<void> => {
93
- try {
94
- const props: DatabaseCommonInteractionProps =
95
- await this.getAccessCheckedProps(req);
96
-
97
- const body: JSONObject = (req.body as JSONObject) || {};
98
-
99
- let limit: number = DEFAULT_LIMIT;
100
-
101
- if (typeof body["limit"] === "number") {
102
- limit = body["limit"];
103
- }
104
-
105
- limit = Math.min(Math.max(Math.floor(limit), 1), MAX_LIMIT);
106
-
107
- let skip: number = 0;
108
-
109
- if (typeof body["skip"] === "number") {
110
- skip = Math.max(Math.floor(body["skip"]), 0);
111
- }
112
-
113
- const query: Query<AIRun> = {
114
- projectId: props.tenantId!,
115
- runType: AIRunType.CodeFix,
116
- };
117
-
118
- // Root read after the tenant access check above.
119
- const runs: Array<AIRun> = await AIRunService.findBy({
120
- query: query,
121
- select: RUN_SELECT,
122
- sort: { createdAt: SortOrder.Descending },
123
- limit: limit,
124
- skip: skip,
125
- props: { isRoot: true },
126
- });
127
-
128
- const count: PositiveNumber = await AIRunService.countBy({
129
- query: query,
130
- props: { isRoot: true },
131
- });
132
-
133
- Response.sendJsonObjectResponse(req, res, {
134
- runs: runs.map((run: AIRun): JSONObject => {
135
- return runToJson(run);
136
- }),
137
- count: count.toNumber(),
138
- });
139
- return;
140
- } catch (err) {
141
- next(err);
142
- return;
143
- }
144
- },
145
- );
146
-
147
98
  // One run + its ordered glass-box event trail.
148
99
  this.router.post(
149
100
  "/code-fix-run/get/:runId",
@@ -0,0 +1,41 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class BackfillCodeFixTaskType1784105912819
4
+ implements MigrationInterface
5
+ {
6
+ public name = "BackfillCodeFixTaskType1784105912819";
7
+
8
+ /*
9
+ * A null AIRun.codeFixTaskType on a CodeFix run has always MEANT
10
+ * FixException: every code-fix run created before task recipes existed was
11
+ * an exception fix, and both the server's read path
12
+ * (CodeFixTaskTypeHelper.fromDatabaseValue) and the dashboard normalize the
13
+ * null to FixException so nobody ever sees it.
14
+ *
15
+ * That normalization only covers reads. Now that the AI Tasks list filters
16
+ * through the standard CRUD, a "Fix Exception" filter compiles to
17
+ * `codeFixTaskType IN ('FixException')`, which NULL never matches — the
18
+ * oldest runs would silently vanish from the filtered list. The bespoke
19
+ * endpoint papered over this with an equalToOrNull query it built itself;
20
+ * that endpoint is gone, so the meaning gets written into the data instead.
21
+ *
22
+ * Scoped to runType = 'CodeFix': codeFixTaskType is meaningless on chat and
23
+ * investigation runs and must stay null there.
24
+ */
25
+ public async up(queryRunner: QueryRunner): Promise<void> {
26
+ await queryRunner.query(
27
+ `UPDATE "AIRun" SET "codeFixTaskType" = 'FixException' WHERE "runType" = 'CodeFix' AND "codeFixTaskType" IS NULL`,
28
+ );
29
+ }
30
+
31
+ /*
32
+ * Deliberately a no-op. The rows this set are indistinguishable from rows
33
+ * that already said FixException, so nulling them back would corrupt data
34
+ * this migration never touched. Reverting is also unnecessary: the read path
35
+ * still normalizes null to FixException, so an older build reads these rows
36
+ * exactly as it did before.
37
+ */
38
+ public async down(_queryRunner: QueryRunner): Promise<void> {
39
+ return;
40
+ }
41
+ }
@@ -450,6 +450,7 @@ import { AddSentinelInsightFlags1784010274994 } from "./1784010274994-AddSentine
450
450
  import { RenameSentinelToAI1784030612266 } from "./1784030612266-RenameSentinelToAI";
451
451
  import { MigrationName1784033837629 } from "./1784033837629-MigrationName";
452
452
  import { AddCreatedByUserToUser1784048917994 } from "./1784048917994-AddCreatedByUserToUser";
453
+ import { BackfillCodeFixTaskType1784105912819 } from "./1784105912819-BackfillCodeFixTaskType";
453
454
 
454
455
  export default [
455
456
  InitialMigration,
@@ -904,4 +905,5 @@ export default [
904
905
  RenameSentinelToAI1784030612266,
905
906
  MigrationName1784033837629,
906
907
  AddCreatedByUserToUser1784048917994,
908
+ BackfillCodeFixTaskType1784105912819,
907
909
  ];
@@ -59,7 +59,7 @@ export default class MasterAdminAuthorization {
59
59
  * Same as isAuthorizedMasterAdminMiddleware, but ALSO accepts the instance-wide
60
60
  * master API key (Admin Dashboard → Settings → API Key) supplied in the
61
61
  * `apikey` header. The master key has root/master-admin access, so this lets
62
- * automated callers reach the master-admin instance-health endpoints with the
62
+ * automated callers reach the master-admin OneUptime Health endpoints with the
63
63
  * key instead of a logged-in master-admin session.
64
64
  *
65
65
  * Deliberately scoped to the read-only health / diagnostics routes only. It is
@@ -65,7 +65,7 @@ export default class ProjectMiddleware {
65
65
  * (Admin Dashboard → Settings → API Key) and that key is currently enabled.
66
66
  * The master key has root/master-admin access, so this is shared with
67
67
  * MasterAdminAuthorization to let the key reach master-admin-only endpoints
68
- * (e.g. instance health) as well.
68
+ * (e.g. OneUptime Health) as well.
69
69
  */
70
70
  @CaptureSpan()
71
71
  public static async isMasterApiKey(apiKey: ObjectID): Promise<boolean> {
@@ -16,7 +16,7 @@ import FindBy from "../Types/Database/FindBy";
16
16
  import { OnFind } from "../Types/Database/Hooks";
17
17
  import DatabaseService from "./DatabaseService";
18
18
  import Model from "../../Models/DatabaseModels/AIRun";
19
- import { pinQueryToRequestingUser } from "../Utils/AI/AIChatPrivacyFilter";
19
+ import { applyAIRunPrivacyFilter } from "../Utils/AI/AIRunPrivacyFilter";
20
20
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
21
21
  import { UpdateQueryBuilder, UpdateResult } from "typeorm";
22
22
  import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
@@ -331,23 +331,22 @@ export class Service extends DatabaseService<Model> {
331
331
  protected override async onBeforeFind(
332
332
  findBy: FindBy<Model>,
333
333
  ): Promise<OnFind<Model>> {
334
- findBy.query = pinQueryToRequestingUser(
335
- findBy.query,
336
- findBy.props,
337
- "userId",
338
- );
334
+ findBy.query = applyAIRunPrivacyFilter(findBy.query, findBy.props);
339
335
  return { findBy, carryForward: null };
340
336
  }
341
337
 
338
+ /*
339
+ * countBy MUST repeat the filter. DatabaseService has no onBeforeCount hook
340
+ * (only onCountSuccess / onCountError), and BaseAPI.getList issues findBy
341
+ * and countBy with the SAME client query, so filtering in onBeforeFind alone
342
+ * would return a correctly-scoped list beside an unscoped project-wide
343
+ * count on every page. IncidentService repeats it for the same reason.
344
+ */
342
345
  @CaptureSpan()
343
346
  public override async countBy(
344
347
  countBy: CountBy<Model>,
345
348
  ): Promise<PositiveNumber> {
346
- countBy.query = pinQueryToRequestingUser(
347
- countBy.query,
348
- countBy.props,
349
- "userId",
350
- );
349
+ countBy.query = applyAIRunPrivacyFilter(countBy.query, countBy.props);
351
350
  return super.countBy(countBy);
352
351
  }
353
352
  }
@@ -0,0 +1,106 @@
1
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
2
+ import { FindWhereProperty } from "../../../Types/BaseDatabase/Query";
3
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
4
+ import AIRunType from "../../../Types/AI/AIRunType";
5
+ import Text from "../../../Types/Text";
6
+ import { combineWithPrivacyClause } from "../PrivacyFilterUtil";
7
+ import { Raw } from "typeorm";
8
+
9
+ /*
10
+ * AIRun mixes two kinds of row under one table:
11
+ *
12
+ * - Chat and investigation runs are PERSONAL. They hang off a conversation
13
+ * and carry the user's own prompt trail, so only their author may read
14
+ * them. This is what AIChatPrivacyFilter's flat pin protects, and that pin
15
+ * stays exactly as it is for AIConversation, AIConversationMessage and
16
+ * AIRunEvent.
17
+ * - CodeFix runs are a SHARED TEAM ARTIFACT: they are triggered off an
18
+ * exception or an incident, they produce a pull request, and their child
19
+ * AIAgentTaskPullRequest rows are already project-readable through the
20
+ * standard CRUD. `userId` on a CodeFix run is attribution only (who
21
+ * clicked "Fix with AI Agent") and is null for automatic triggers — it
22
+ * carries no conversational content.
23
+ *
24
+ * So AIRun cannot use that flat pin. It needs `runType = CodeFix OR userId =
25
+ * <caller>`, a CROSS-COLUMN disjunction, which a query object cannot express
26
+ * (query objects are conjunctions).
27
+ *
28
+ * The clause below is built ENTIRELY from server state and AND-ed onto the
29
+ * caller's query. It never READS query.runType to decide anything — that
30
+ * would be unsound, because onBeforeFind runs BEFORE QueryUtil.serializeQuery,
31
+ * so query.runType here is still a raw, client-controlled operator object
32
+ * whose .value/.toString() are identical across the safe and hostile cases:
33
+ * LessThan("CodeFix") stringifies to exactly "CodeFix" but compiles to
34
+ * `runType < 'CodeFix'`, which matches "Chat". Because the clause is forced, a
35
+ * caller-supplied runType filter can only ever NARROW it, and
36
+ * combineWithPrivacyClause fails closed on operator shapes it does not
37
+ * recognize.
38
+ *
39
+ * Modelled on Utils/Incident/IncidentPrivacyFilter, which solves the same
40
+ * "unrestricted rows for everyone, restricted rows only for their owner"
41
+ * shape.
42
+ */
43
+ export function getAIRunPrivacyRaw(
44
+ props: DatabaseCommonInteractionProps,
45
+ ): FindWhereProperty<any> | undefined {
46
+ if (props.isRoot || props.isMasterAdmin) {
47
+ return undefined;
48
+ }
49
+
50
+ /*
51
+ * The same hard gate the flat pin has, and it must stay. A caller with no
52
+ * user — a project API key, which gets userTenantAccessPermission but never
53
+ * userAuthorization (see ProjectAuthorization), so props.userId is
54
+ * undefined — has no personal scope. Today such a caller cannot read AIRun
55
+ * at all. Letting CodeFix rows through here would hand every API key holding
56
+ * ProjectMember the project's whole fix history: access that neither
57
+ * /ai-run nor /code-fix-run grants today. That is a separate product
58
+ * decision and must not ride in as a side effect of this filter.
59
+ */
60
+ if (!props.userId) {
61
+ throw new NotAuthorizedException(
62
+ "AI runs are personal and can only be accessed by the user who created them.",
63
+ );
64
+ }
65
+
66
+ const userIdRid: string = "aiRunUid_" + Text.generateRandomText(10);
67
+ const runTypeRid: string = "aiRunType_" + Text.generateRandomText(10);
68
+
69
+ return Raw(
70
+ (alias: string): string => {
71
+ return `(${alias} = :${runTypeRid} OR "AIRun"."userId" = :${userIdRid})`;
72
+ },
73
+ {
74
+ [runTypeRid]: AIRunType.CodeFix,
75
+ [userIdRid]: props.userId.toString(),
76
+ },
77
+ );
78
+ }
79
+
80
+ /*
81
+ * Applies the clause above on AIRun.runType. Project isolation is unaffected:
82
+ * @TenantColumn("projectId") independently forces projectId = <tenant>, so
83
+ * "CodeFix runs are readable" always means "in YOUR project".
84
+ */
85
+ export function applyAIRunPrivacyFilter<TQuery>(
86
+ query: TQuery,
87
+ props: DatabaseCommonInteractionProps,
88
+ ): TQuery {
89
+ const rawClause: FindWhereProperty<any> | undefined =
90
+ getAIRunPrivacyRaw(props);
91
+
92
+ if (!rawClause) {
93
+ return query;
94
+ }
95
+
96
+ if (!query) {
97
+ return { runType: rawClause } as unknown as TQuery;
98
+ }
99
+
100
+ (query as any).runType = combineWithPrivacyClause(
101
+ (query as any).runType,
102
+ rawClause,
103
+ );
104
+
105
+ return query;
106
+ }