@app-vitals/agent-admin 0.4.0 → 0.6.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.
@@ -2749,6 +2749,7 @@ export const AgentGitHubInstallationsSnapshotScalarFieldEnum = {
2749
2749
  agentId: 'agentId',
2750
2750
  reportedAt: 'reportedAt',
2751
2751
  installations: 'installations',
2752
+ repoAccess: 'repoAccess',
2752
2753
  createdAt: 'createdAt'
2753
2754
  } as const
2754
2755
 
@@ -2808,6 +2809,14 @@ export const JsonNullValueInput = {
2808
2809
  export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
2809
2810
 
2810
2811
 
2812
+ export const NullableJsonNullValueInput = {
2813
+ DbNull: DbNull,
2814
+ JsonNull: JsonNull
2815
+ } as const
2816
+
2817
+ export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
2818
+
2819
+
2811
2820
  export const QueryMode = {
2812
2821
  default: 'default',
2813
2822
  insensitive: 'insensitive'
@@ -439,6 +439,7 @@ export const AgentGitHubInstallationsSnapshotScalarFieldEnum = {
439
439
  agentId: 'agentId',
440
440
  reportedAt: 'reportedAt',
441
441
  installations: 'installations',
442
+ repoAccess: 'repoAccess',
442
443
  createdAt: 'createdAt'
443
444
  } as const
444
445
 
@@ -498,6 +499,14 @@ export const JsonNullValueInput = {
498
499
  export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
499
500
 
500
501
 
502
+ export const NullableJsonNullValueInput = {
503
+ DbNull: DbNull,
504
+ JsonNull: JsonNull
505
+ } as const
506
+
507
+ export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
508
+
509
+
501
510
  export const QueryMode = {
502
511
  default: 'default',
503
512
  insensitive: 'insensitive'
@@ -43,6 +43,7 @@ export type AgentGitHubInstallationsSnapshotCountAggregateOutputType = {
43
43
  agentId: number
44
44
  reportedAt: number
45
45
  installations: number
46
+ repoAccess: number
46
47
  createdAt: number
47
48
  _all: number
48
49
  }
@@ -67,6 +68,7 @@ export type AgentGitHubInstallationsSnapshotCountAggregateInputType = {
67
68
  agentId?: true
68
69
  reportedAt?: true
69
70
  installations?: true
71
+ repoAccess?: true
70
72
  createdAt?: true
71
73
  _all?: true
72
74
  }
@@ -148,6 +150,7 @@ export type AgentGitHubInstallationsSnapshotGroupByOutputType = {
148
150
  agentId: string
149
151
  reportedAt: Date
150
152
  installations: runtime.JsonValue
153
+ repoAccess: runtime.JsonValue | null
151
154
  createdAt: Date
152
155
  _count: AgentGitHubInstallationsSnapshotCountAggregateOutputType | null
153
156
  _min: AgentGitHubInstallationsSnapshotMinAggregateOutputType | null
@@ -177,6 +180,7 @@ export type AgentGitHubInstallationsSnapshotWhereInput = {
177
180
  agentId?: Prisma.StringFilter<"AgentGitHubInstallationsSnapshot"> | string
178
181
  reportedAt?: Prisma.DateTimeFilter<"AgentGitHubInstallationsSnapshot"> | Date | string
179
182
  installations?: Prisma.JsonFilter<"AgentGitHubInstallationsSnapshot">
183
+ repoAccess?: Prisma.JsonNullableFilter<"AgentGitHubInstallationsSnapshot">
180
184
  createdAt?: Prisma.DateTimeFilter<"AgentGitHubInstallationsSnapshot"> | Date | string
181
185
  agent?: Prisma.XOR<Prisma.AgentScalarRelationFilter, Prisma.AgentWhereInput>
182
186
  }
@@ -186,6 +190,7 @@ export type AgentGitHubInstallationsSnapshotOrderByWithRelationInput = {
186
190
  agentId?: Prisma.SortOrder
187
191
  reportedAt?: Prisma.SortOrder
188
192
  installations?: Prisma.SortOrder
193
+ repoAccess?: Prisma.SortOrderInput | Prisma.SortOrder
189
194
  createdAt?: Prisma.SortOrder
190
195
  agent?: Prisma.AgentOrderByWithRelationInput
191
196
  }
@@ -198,6 +203,7 @@ export type AgentGitHubInstallationsSnapshotWhereUniqueInput = Prisma.AtLeast<{
198
203
  NOT?: Prisma.AgentGitHubInstallationsSnapshotWhereInput | Prisma.AgentGitHubInstallationsSnapshotWhereInput[]
199
204
  reportedAt?: Prisma.DateTimeFilter<"AgentGitHubInstallationsSnapshot"> | Date | string
200
205
  installations?: Prisma.JsonFilter<"AgentGitHubInstallationsSnapshot">
206
+ repoAccess?: Prisma.JsonNullableFilter<"AgentGitHubInstallationsSnapshot">
201
207
  createdAt?: Prisma.DateTimeFilter<"AgentGitHubInstallationsSnapshot"> | Date | string
202
208
  agent?: Prisma.XOR<Prisma.AgentScalarRelationFilter, Prisma.AgentWhereInput>
203
209
  }, "id" | "agentId">
@@ -207,6 +213,7 @@ export type AgentGitHubInstallationsSnapshotOrderByWithAggregationInput = {
207
213
  agentId?: Prisma.SortOrder
208
214
  reportedAt?: Prisma.SortOrder
209
215
  installations?: Prisma.SortOrder
216
+ repoAccess?: Prisma.SortOrderInput | Prisma.SortOrder
210
217
  createdAt?: Prisma.SortOrder
211
218
  _count?: Prisma.AgentGitHubInstallationsSnapshotCountOrderByAggregateInput
212
219
  _max?: Prisma.AgentGitHubInstallationsSnapshotMaxOrderByAggregateInput
@@ -221,6 +228,7 @@ export type AgentGitHubInstallationsSnapshotScalarWhereWithAggregatesInput = {
221
228
  agentId?: Prisma.StringWithAggregatesFilter<"AgentGitHubInstallationsSnapshot"> | string
222
229
  reportedAt?: Prisma.DateTimeWithAggregatesFilter<"AgentGitHubInstallationsSnapshot"> | Date | string
223
230
  installations?: Prisma.JsonWithAggregatesFilter<"AgentGitHubInstallationsSnapshot">
231
+ repoAccess?: Prisma.JsonNullableWithAggregatesFilter<"AgentGitHubInstallationsSnapshot">
224
232
  createdAt?: Prisma.DateTimeWithAggregatesFilter<"AgentGitHubInstallationsSnapshot"> | Date | string
225
233
  }
226
234
 
@@ -228,6 +236,7 @@ export type AgentGitHubInstallationsSnapshotCreateInput = {
228
236
  id?: string
229
237
  reportedAt: Date | string
230
238
  installations: Prisma.JsonNullValueInput | runtime.InputJsonValue
239
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
231
240
  createdAt?: Date | string
232
241
  agent: Prisma.AgentCreateNestedOneWithoutGithubInstallationsSnapshotInput
233
242
  }
@@ -237,6 +246,7 @@ export type AgentGitHubInstallationsSnapshotUncheckedCreateInput = {
237
246
  agentId: string
238
247
  reportedAt: Date | string
239
248
  installations: Prisma.JsonNullValueInput | runtime.InputJsonValue
249
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
240
250
  createdAt?: Date | string
241
251
  }
242
252
 
@@ -244,6 +254,7 @@ export type AgentGitHubInstallationsSnapshotUpdateInput = {
244
254
  id?: Prisma.StringFieldUpdateOperationsInput | string
245
255
  reportedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
246
256
  installations?: Prisma.JsonNullValueInput | runtime.InputJsonValue
257
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
247
258
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
248
259
  agent?: Prisma.AgentUpdateOneRequiredWithoutGithubInstallationsSnapshotNestedInput
249
260
  }
@@ -253,6 +264,7 @@ export type AgentGitHubInstallationsSnapshotUncheckedUpdateInput = {
253
264
  agentId?: Prisma.StringFieldUpdateOperationsInput | string
254
265
  reportedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
255
266
  installations?: Prisma.JsonNullValueInput | runtime.InputJsonValue
267
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
256
268
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
257
269
  }
258
270
 
@@ -261,6 +273,7 @@ export type AgentGitHubInstallationsSnapshotCreateManyInput = {
261
273
  agentId: string
262
274
  reportedAt: Date | string
263
275
  installations: Prisma.JsonNullValueInput | runtime.InputJsonValue
276
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
264
277
  createdAt?: Date | string
265
278
  }
266
279
 
@@ -268,6 +281,7 @@ export type AgentGitHubInstallationsSnapshotUpdateManyMutationInput = {
268
281
  id?: Prisma.StringFieldUpdateOperationsInput | string
269
282
  reportedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
270
283
  installations?: Prisma.JsonNullValueInput | runtime.InputJsonValue
284
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
271
285
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
272
286
  }
273
287
 
@@ -276,6 +290,7 @@ export type AgentGitHubInstallationsSnapshotUncheckedUpdateManyInput = {
276
290
  agentId?: Prisma.StringFieldUpdateOperationsInput | string
277
291
  reportedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
278
292
  installations?: Prisma.JsonNullValueInput | runtime.InputJsonValue
293
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
279
294
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
280
295
  }
281
296
 
@@ -289,6 +304,7 @@ export type AgentGitHubInstallationsSnapshotCountOrderByAggregateInput = {
289
304
  agentId?: Prisma.SortOrder
290
305
  reportedAt?: Prisma.SortOrder
291
306
  installations?: Prisma.SortOrder
307
+ repoAccess?: Prisma.SortOrder
292
308
  createdAt?: Prisma.SortOrder
293
309
  }
294
310
 
@@ -342,6 +358,7 @@ export type AgentGitHubInstallationsSnapshotCreateWithoutAgentInput = {
342
358
  id?: string
343
359
  reportedAt: Date | string
344
360
  installations: Prisma.JsonNullValueInput | runtime.InputJsonValue
361
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
345
362
  createdAt?: Date | string
346
363
  }
347
364
 
@@ -349,6 +366,7 @@ export type AgentGitHubInstallationsSnapshotUncheckedCreateWithoutAgentInput = {
349
366
  id?: string
350
367
  reportedAt: Date | string
351
368
  installations: Prisma.JsonNullValueInput | runtime.InputJsonValue
369
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
352
370
  createdAt?: Date | string
353
371
  }
354
372
 
@@ -372,6 +390,7 @@ export type AgentGitHubInstallationsSnapshotUpdateWithoutAgentInput = {
372
390
  id?: Prisma.StringFieldUpdateOperationsInput | string
373
391
  reportedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
374
392
  installations?: Prisma.JsonNullValueInput | runtime.InputJsonValue
393
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
375
394
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
376
395
  }
377
396
 
@@ -379,6 +398,7 @@ export type AgentGitHubInstallationsSnapshotUncheckedUpdateWithoutAgentInput = {
379
398
  id?: Prisma.StringFieldUpdateOperationsInput | string
380
399
  reportedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
381
400
  installations?: Prisma.JsonNullValueInput | runtime.InputJsonValue
401
+ repoAccess?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
382
402
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
383
403
  }
384
404
 
@@ -389,6 +409,7 @@ export type AgentGitHubInstallationsSnapshotSelect<ExtArgs extends runtime.Types
389
409
  agentId?: boolean
390
410
  reportedAt?: boolean
391
411
  installations?: boolean
412
+ repoAccess?: boolean
392
413
  createdAt?: boolean
393
414
  agent?: boolean | Prisma.AgentDefaultArgs<ExtArgs>
394
415
  }, ExtArgs["result"]["agentGitHubInstallationsSnapshot"]>
@@ -398,6 +419,7 @@ export type AgentGitHubInstallationsSnapshotSelectCreateManyAndReturn<ExtArgs ex
398
419
  agentId?: boolean
399
420
  reportedAt?: boolean
400
421
  installations?: boolean
422
+ repoAccess?: boolean
401
423
  createdAt?: boolean
402
424
  agent?: boolean | Prisma.AgentDefaultArgs<ExtArgs>
403
425
  }, ExtArgs["result"]["agentGitHubInstallationsSnapshot"]>
@@ -407,6 +429,7 @@ export type AgentGitHubInstallationsSnapshotSelectUpdateManyAndReturn<ExtArgs ex
407
429
  agentId?: boolean
408
430
  reportedAt?: boolean
409
431
  installations?: boolean
432
+ repoAccess?: boolean
410
433
  createdAt?: boolean
411
434
  agent?: boolean | Prisma.AgentDefaultArgs<ExtArgs>
412
435
  }, ExtArgs["result"]["agentGitHubInstallationsSnapshot"]>
@@ -416,10 +439,11 @@ export type AgentGitHubInstallationsSnapshotSelectScalar = {
416
439
  agentId?: boolean
417
440
  reportedAt?: boolean
418
441
  installations?: boolean
442
+ repoAccess?: boolean
419
443
  createdAt?: boolean
420
444
  }
421
445
 
422
- export type AgentGitHubInstallationsSnapshotOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "agentId" | "reportedAt" | "installations" | "createdAt", ExtArgs["result"]["agentGitHubInstallationsSnapshot"]>
446
+ export type AgentGitHubInstallationsSnapshotOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "agentId" | "reportedAt" | "installations" | "repoAccess" | "createdAt", ExtArgs["result"]["agentGitHubInstallationsSnapshot"]>
423
447
  export type AgentGitHubInstallationsSnapshotInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
424
448
  agent?: boolean | Prisma.AgentDefaultArgs<ExtArgs>
425
449
  }
@@ -440,6 +464,7 @@ export type $AgentGitHubInstallationsSnapshotPayload<ExtArgs extends runtime.Typ
440
464
  agentId: string
441
465
  reportedAt: Date
442
466
  installations: runtime.JsonValue
467
+ repoAccess: runtime.JsonValue | null
443
468
  createdAt: Date
444
469
  }, ExtArgs["result"]["agentGitHubInstallationsSnapshot"]>
445
470
  composites: {}
@@ -869,6 +894,7 @@ export interface AgentGitHubInstallationsSnapshotFieldRefs {
869
894
  readonly agentId: Prisma.FieldRef<"AgentGitHubInstallationsSnapshot", 'String'>
870
895
  readonly reportedAt: Prisma.FieldRef<"AgentGitHubInstallationsSnapshot", 'DateTime'>
871
896
  readonly installations: Prisma.FieldRef<"AgentGitHubInstallationsSnapshot", 'Json'>
897
+ readonly repoAccess: Prisma.FieldRef<"AgentGitHubInstallationsSnapshot", 'Json'>
872
898
  readonly createdAt: Prisma.FieldRef<"AgentGitHubInstallationsSnapshot", 'DateTime'>
873
899
  }
874
900
 
@@ -0,0 +1,3 @@
1
+ -- COS-44.1: per-repo GitHub access probe result reported by the runtime.
2
+ -- Nullable and additive; no token or credential is ever stored here.
3
+ ALTER TABLE "AgentGitHubInstallationsSnapshot" ADD COLUMN "repoAccess" JSONB;
@@ -496,6 +496,7 @@ model AgentGitHubInstallationsSnapshot {
496
496
  agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
497
497
  reportedAt DateTime
498
498
  installations Json // GitHubInstallation[]
499
+ repoAccess Json? // RepoAccessEntry[] — per-repo read/write probe (COS-44.1); null until a flagged runtime reports it
499
500
  createdAt DateTime @default(now())
500
501
  }
501
502
 
package/src/admin-boot.ts CHANGED
@@ -74,6 +74,7 @@ import { AgentAskService } from "./agent-ask.ts";
74
74
  import { parseAgentAskConfig } from "./agent-ask-config.ts";
75
75
  import { CronRunNowService } from "./cron-run-now.ts";
76
76
  import { parseCronRunNowConfig } from "./cron-run-now-config.ts";
77
+ import { parsePodServiceAccountConfig } from "./pod-service-account-config.ts";
77
78
  import { AgentChatTokenService } from "./agent-chat-tokens.ts";
78
79
  import { AgentCronJobService } from "./agent-cron-jobs.ts";
79
80
  import { AgentCronRunStatsService } from "./agent-cron-run-stats.ts";
@@ -104,6 +105,7 @@ import { mountPlatformDoc } from "./platform-doc-route.ts";
104
105
  import { createAdminPrismaClient } from "./prisma-client.ts";
105
106
  import { isPushEnabled } from "./push-sender.ts";
106
107
  import { PushService } from "./push-service.ts";
108
+ import { parseRepoAccessConfig } from "./repo-access-config.ts";
107
109
  import { parseSelfServeConfig } from "./self-serve-config.ts";
108
110
  import { HttpSlackProvisioningClient } from "./slack-provisioning-client.ts";
109
111
  import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
@@ -300,6 +302,9 @@ export async function startAdmin(
300
302
  // Cron-run transcript API (COS-43.1): flag off by default.
301
303
  const transcriptConfig = parseTranscriptConfig(process.env);
302
304
 
305
+ // Per-repo GitHub access check (COS-44.1): flag off by default.
306
+ const repoAccessConfig = parseRepoAccessConfig(process.env);
307
+
303
308
  // Web Push (CFB-4.2). Enabled ONLY when public + private + subject are all
304
309
  // set — a partial config must not half-enable the feature. When disabled,
305
310
  // pushService is undefined, the push routes 503, and the toggle renders "".
@@ -425,6 +430,7 @@ export async function startAdmin(
425
430
  agentTokenService,
426
431
  anthropicFederationEnabled,
427
432
  transcriptUploadEnabled: transcriptConfig.enabled,
433
+ repoAccessCheckEnabled: repoAccessConfig.enabled,
428
434
  });
429
435
 
430
436
  root.route("/agents", runtimeApp);
@@ -458,6 +464,7 @@ export async function startAdmin(
458
464
  pool: adminPool,
459
465
  selfServe,
460
466
  anthropicFederationEnabled,
467
+ podServiceAccountEnabled: parsePodServiceAccountConfig(process.env).enabled,
461
468
  chatCredentialsEnabled: parseChatCredentialConfig(process.env).enabled,
462
469
  ...(chatCredentialsClient
463
470
  ? { chatCredentials: chatCredentialsClient }
@@ -15,15 +15,44 @@ import type { AgentProvisioner } from "./agent-provisioner.ts";
15
15
  * already committed, so a reconcile failure is returned as a warning string
16
16
  * (the next full reconcile pass will converge it) instead of failing the call.
17
17
  */
18
- export async function reconcileAgentAfterAccountChange(
18
+ type ReconcilableAgent = Partial<AgentAnthropicIdentity> & {
19
+ id: string;
20
+ name: string;
21
+ selfHosted: boolean;
22
+ accountId?: string | null;
23
+ podServiceAccountName?: string | null;
24
+ };
25
+
26
+ export function reconcileAgentAfterAccountChange(
27
+ provisioner: Pick<AgentProvisioner, "reconcile">,
28
+ agent: ReconcilableAgent,
29
+ ): Promise<string | undefined> {
30
+ return reconcileAgentWorkload(provisioner, agent, "account updated");
31
+ }
32
+
33
+ /**
34
+ * COS-49.1: reconciles the workload after podServiceAccountName changed.
35
+ * `previous` is the column value before the update; the provisioner uses it to
36
+ * clear the live ServiceAccount only when it is still the one it applied.
37
+ */
38
+ export function reconcileAgentAfterPodServiceAccountChange(
39
+ provisioner: Pick<AgentProvisioner, "reconcile">,
40
+ agent: ReconcilableAgent,
41
+ previous: string | null,
42
+ ): Promise<string | undefined> {
43
+ return reconcileAgentWorkload(
44
+ provisioner,
45
+ agent,
46
+ "pod service account updated",
47
+ previous,
48
+ );
49
+ }
50
+
51
+ async function reconcileAgentWorkload(
19
52
  provisioner: Pick<AgentProvisioner, "reconcile">,
20
- agent: Partial<AgentAnthropicIdentity> & {
21
- id: string;
22
- name: string;
23
- selfHosted: boolean;
24
- accountId?: string | null;
25
- podServiceAccountName?: string | null;
26
- },
53
+ agent: ReconcilableAgent,
54
+ what: string,
55
+ previousPodServiceAccountName?: string | null,
27
56
  ): Promise<string | undefined> {
28
57
  if (agent.selfHosted) return undefined;
29
58
  try {
@@ -38,14 +67,17 @@ export async function reconcileAgentAfterAccountChange(
38
67
  anthropicFederationRuleId: agent.anthropicFederationRuleId,
39
68
  k8sServiceAccountName: agent.k8sServiceAccountName,
40
69
  podServiceAccountName: agent.podServiceAccountName,
70
+ ...(previousPodServiceAccountName !== undefined
71
+ ? { previousPodServiceAccountName }
72
+ : {}),
41
73
  },
42
74
  ]);
43
75
  const failure = result.failed.find((f) => f.agentId === agent.id);
44
76
  return failure
45
- ? `account updated, but reconciling the agent workload failed: ${failure.error}`
77
+ ? `${what}, but reconciling the agent workload failed: ${failure.error}`
46
78
  : undefined;
47
79
  } catch (err) {
48
80
  const message = err instanceof Error ? err.message : String(err);
49
- return `account updated, but reconciling the agent workload failed: ${message}`;
81
+ return `${what}, but reconciling the agent workload failed: ${message}`;
50
82
  }
51
83
  }
@@ -66,6 +66,38 @@ describeOrSkip("AgentGitHubInstallationsService (integration)", () => {
66
66
  expect(read?.reportedAt).toEqual(reportedAt);
67
67
  });
68
68
 
69
+ it("round-trips repoAccess and keeps it when a later push omits it (COS-44.1)", async () => {
70
+ const agentId = await createAgent(prisma);
71
+ const repoAccess = [
72
+ {
73
+ repo: "agent",
74
+ owner: "app-vitals",
75
+ read: true,
76
+ write: true,
77
+ checkedAt: "2026-01-15T12:00:00.000Z",
78
+ },
79
+ ];
80
+ const first = await service.push(agentId, {
81
+ reportedAt: new Date("2026-01-15T12:00:00.000Z"),
82
+ installations: [],
83
+ repoAccess,
84
+ });
85
+ expect(first.repoAccess).toEqual(repoAccess);
86
+
87
+ await service.push(agentId, {
88
+ reportedAt: new Date("2026-01-16T12:00:00.000Z"),
89
+ installations: [],
90
+ });
91
+ expect((await service.get(agentId))?.repoAccess).toEqual(repoAccess);
92
+
93
+ await service.push(agentId, {
94
+ reportedAt: new Date("2026-01-17T12:00:00.000Z"),
95
+ installations: [],
96
+ repoAccess: [],
97
+ });
98
+ expect((await service.get(agentId))?.repoAccess).toEqual([]);
99
+ });
100
+
69
101
  it("get() returns null when nothing has been pushed", async () => {
70
102
  const agentId = await createAgent(prisma);
71
103
  expect(await service.get(agentId)).toBeNull();
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * One row per agent (AgentGitHubInstallationsSnapshot.agentId is @unique).
7
7
  * push() upserts the single row, replacing the installations list wholesale —
8
- * an installation absent from the latest push is gone.
8
+ * an installation absent from the latest push is gone. repoAccess (COS-44.1)
9
+ * is only written when the push carries it, so a push from a runtime with the
10
+ * check off never wipes a result stored earlier.
9
11
  */
10
12
 
11
13
  import type {
@@ -19,6 +21,8 @@ export type { AgentGitHubInstallationsSnapshot };
19
21
  interface PushGitHubInstallationsInput {
20
22
  reportedAt: Date;
21
23
  installations: Prisma.InputJsonValue;
24
+ /** Omitted leaves the stored per-repo access result unchanged; [] clears it. */
25
+ repoAccess?: Prisma.InputJsonValue;
22
26
  }
23
27
 
24
28
  export class AgentGitHubInstallationsService {
@@ -35,10 +39,16 @@ export class AgentGitHubInstallationsService {
35
39
  agentId,
36
40
  reportedAt: input.reportedAt,
37
41
  installations: input.installations,
42
+ ...(input.repoAccess === undefined
43
+ ? {}
44
+ : { repoAccess: input.repoAccess }),
38
45
  },
39
46
  update: {
40
47
  reportedAt: input.reportedAt,
41
48
  installations: input.installations,
49
+ ...(input.repoAccess === undefined
50
+ ? {}
51
+ : { repoAccess: input.repoAccess }),
42
52
  },
43
53
  });
44
54
  }
@@ -151,6 +151,15 @@ export interface ReconcileTarget extends Partial<AgentAnthropicIdentity> {
151
151
  accountId?: string | null;
152
152
  /** The Agent row's pod ServiceAccount (COS-48.1); rendered only when the flag is on. */
153
153
  podServiceAccountName?: string | null;
154
+ /**
155
+ * The pod ServiceAccount this agent had before the change being reconciled
156
+ * (COS-49.1). Only used to clear: when `podServiceAccountName` is now unset,
157
+ * the live ServiceAccount is nulled only if it still equals this value, so a
158
+ * ServiceAccount set by another actor is left alone. The PATCH handler passes
159
+ * the pre-update column value; startup/full passes leave it absent (so they
160
+ * never clear anything).
161
+ */
162
+ previousPodServiceAccountName?: string | null;
154
163
  }
155
164
 
156
165
  export interface AgentProvisioner {
@@ -701,6 +710,44 @@ export function federationPatch(
701
710
  return Object.keys(patch).length > 0 ? patch : undefined;
702
711
  }
703
712
 
713
+ /** The pod-identity patch: the ServiceAccount fields only. */
714
+ interface PodIdentityPatch {
715
+ serviceAccountName: string | null;
716
+ serviceAccount?: null;
717
+ }
718
+
719
+ /**
720
+ * Strategic-merge fragment that brings the live pod ServiceAccount to the
721
+ * Agent row's podServiceAccountName (COS-49.1), for Deployments without
722
+ * federation wiring (federationPatch owns the ServiceAccount when the desired
723
+ * Deployment carries federation).
724
+ *
725
+ * - Desired ServiceAccount set: patched when the live one differs.
726
+ * - Desired unset: cleared (serviceAccountName and the deprecated
727
+ * `serviceAccount` alias nulled) ONLY when the live value equals
728
+ * `previous`, the value this admin previously applied. A ServiceAccount set
729
+ * by another actor therefore survives, as does everything when `previous`
730
+ * is unknown (startup passes).
731
+ */
732
+ export function podIdentityPatch(
733
+ live: KubernetesDeployment,
734
+ desired: KubernetesDeployment,
735
+ previous?: string | null,
736
+ ): PodIdentityPatch | undefined {
737
+ const cur = federationParts(live);
738
+ const want = federationParts(desired);
739
+ if (want.volume || want.env.size > 0) return undefined;
740
+ if (want.serviceAccountName) {
741
+ return cur.serviceAccountName !== want.serviceAccountName
742
+ ? { serviceAccountName: want.serviceAccountName }
743
+ : undefined;
744
+ }
745
+ if (previous && cur.serviceAccountName === previous) {
746
+ return { serviceAccountName: null, serviceAccount: null };
747
+ }
748
+ return undefined;
749
+ }
750
+
704
751
  function isConflict(err: unknown): boolean {
705
752
  return err instanceof ConflictError;
706
753
  }
@@ -1420,6 +1467,16 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
1420
1467
  const federation = manageFederation
1421
1468
  ? federationPatch(deployment, desiredDeployment)
1422
1469
  : undefined;
1470
+ // Pod ServiceAccount (COS-49.1). Runs under policyOnly too, so an
1471
+ // admin restart never regresses a Deployment to the wrong identity.
1472
+ const podIdentity =
1473
+ federation?.serviceAccountName === undefined
1474
+ ? podIdentityPatch(
1475
+ deployment,
1476
+ desiredDeployment,
1477
+ agent.previousPodServiceAccountName,
1478
+ )
1479
+ : undefined;
1423
1480
  const containerDrift =
1424
1481
  !policyOnly &&
1425
1482
  current !== undefined &&
@@ -1427,7 +1484,11 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
1427
1484
  containerDrifted(current, desired);
1428
1485
  if (
1429
1486
  desired &&
1430
- (labelsDrifted || containerDrift || policy || federation)
1487
+ (labelsDrifted ||
1488
+ containerDrift ||
1489
+ policy ||
1490
+ federation ||
1491
+ podIdentity)
1431
1492
  ) {
1432
1493
  // Under policyOnly the container entry carries only the policy and
1433
1494
  // federation mount/env changes (if any); image, the rest of env,
@@ -1466,11 +1527,15 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
1466
1527
  ...(policy?.volumes ?? []),
1467
1528
  ...(federation?.volumes ?? []),
1468
1529
  ];
1469
- const serviceAccount =
1530
+ const saPatch =
1470
1531
  federation?.serviceAccountName !== undefined
1532
+ ? federation
1533
+ : podIdentity;
1534
+ const serviceAccount =
1535
+ saPatch?.serviceAccountName !== undefined
1471
1536
  ? {
1472
- serviceAccountName: federation.serviceAccountName,
1473
- ...(federation.serviceAccount === null
1537
+ serviceAccountName: saPatch.serviceAccountName,
1538
+ ...(saPatch.serviceAccount === null
1474
1539
  ? { serviceAccount: null }
1475
1540
  : {}),
1476
1541
  }