@shipfox/api-runners 12.1.0 → 12.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 (129) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +35 -0
  3. package/dist/config.d.ts +3 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +24 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/errors.d.ts +4 -0
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +6 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/index.d.ts +1 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +1 -1
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/rate-limit.d.ts.map +1 -1
  16. package/dist/core/runner-control-sessions.d.ts.map +1 -1
  17. package/dist/core/runner-control-sessions.js +37 -11
  18. package/dist/core/runner-control-sessions.js.map +1 -1
  19. package/dist/core/runner-instances.d.ts +1 -0
  20. package/dist/core/runner-instances.d.ts.map +1 -1
  21. package/dist/core/runner-instances.js.map +1 -1
  22. package/dist/core/runner-labels.d.ts +11 -0
  23. package/dist/core/runner-labels.d.ts.map +1 -0
  24. package/dist/core/runner-labels.js +41 -0
  25. package/dist/core/runner-labels.js.map +1 -0
  26. package/dist/core/runner-sessions.d.ts.map +1 -1
  27. package/dist/core/runner-sessions.js +5 -1
  28. package/dist/core/runner-sessions.js.map +1 -1
  29. package/dist/db/db.d.ts +64 -64
  30. package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
  31. package/dist/db/ephemeral-registration-tokens.js +12 -2
  32. package/dist/db/ephemeral-registration-tokens.js.map +1 -1
  33. package/dist/db/index.d.ts +1 -1
  34. package/dist/db/index.d.ts.map +1 -1
  35. package/dist/db/index.js +1 -1
  36. package/dist/db/index.js.map +1 -1
  37. package/dist/db/reservations.d.ts.map +1 -1
  38. package/dist/db/reservations.js +36 -10
  39. package/dist/db/reservations.js.map +1 -1
  40. package/dist/db/runner-instances.d.ts +4 -0
  41. package/dist/db/runner-instances.d.ts.map +1 -1
  42. package/dist/db/runner-instances.js +16 -5
  43. package/dist/db/runner-instances.js.map +1 -1
  44. package/dist/db/runner-sessions.d.ts.map +1 -1
  45. package/dist/db/runner-sessions.js +33 -15
  46. package/dist/db/runner-sessions.js.map +1 -1
  47. package/dist/db/schema/admin-command-results.d.ts +1 -1
  48. package/dist/db/schema/ephemeral-registration-tokens.d.ts +1 -1
  49. package/dist/db/schema/manual-registration-tokens.d.ts +1 -1
  50. package/dist/db/schema/outbox.d.ts +1 -1
  51. package/dist/db/schema/pending-job-executions.d.ts +3 -3
  52. package/dist/db/schema/provisioner-capability-snapshots.d.ts +3 -3
  53. package/dist/db/schema/provisioner-tokens.d.ts +2 -2
  54. package/dist/db/schema/rate-limits.d.ts +1 -1
  55. package/dist/db/schema/reservations.d.ts +3 -3
  56. package/dist/db/schema/runner-activation-tokens.d.ts +1 -1
  57. package/dist/db/schema/runner-control-sessions.d.ts +2 -2
  58. package/dist/db/schema/runner-instances.d.ts +4 -4
  59. package/dist/db/schema/runner-instances.d.ts.map +1 -1
  60. package/dist/db/schema/runner-instances.js +1 -0
  61. package/dist/db/schema/runner-instances.js.map +1 -1
  62. package/dist/db/schema/runner-sessions.d.ts +4 -4
  63. package/dist/db/schema/running-job-executions.d.ts +5 -5
  64. package/dist/metrics/index.d.ts +2 -1
  65. package/dist/metrics/index.d.ts.map +1 -1
  66. package/dist/metrics/index.js +1 -1
  67. package/dist/metrics/index.js.map +1 -1
  68. package/dist/metrics/instance.d.ts +13 -8
  69. package/dist/metrics/instance.d.ts.map +1 -1
  70. package/dist/metrics/instance.js +8 -0
  71. package/dist/metrics/instance.js.map +1 -1
  72. package/dist/metrics/service.d.ts.map +1 -1
  73. package/dist/metrics/service.js +20 -4
  74. package/dist/metrics/service.js.map +1 -1
  75. package/dist/presentation/dto/admin-provisioner-token.d.ts +1 -1
  76. package/dist/presentation/dto/admin-provisioner-token.d.ts.map +1 -1
  77. package/dist/presentation/dto/admin-runner-instances.d.ts +2 -2
  78. package/dist/presentation/dto/admin-runner-instances.d.ts.map +1 -1
  79. package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
  80. package/dist/presentation/routes/poll-demand.js +12 -6
  81. package/dist/presentation/routes/poll-demand.js.map +1 -1
  82. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  83. package/dist/presentation/routes/register.d.ts.map +1 -1
  84. package/dist/presentation/routes/register.js +9 -1
  85. package/dist/presentation/routes/register.js.map +1 -1
  86. package/dist/presentation/routes/report-runner-instances.d.ts.map +1 -1
  87. package/dist/presentation/routes/report-runner-instances.js +1 -0
  88. package/dist/presentation/routes/report-runner-instances.js.map +1 -1
  89. package/dist/tsconfig.test.tsbuildinfo +1 -1
  90. package/drizzle/0004_huge_skreet.sql +1 -0
  91. package/drizzle/meta/0004_snapshot.json +2194 -0
  92. package/drizzle/meta/_journal.json +7 -0
  93. package/package.json +9 -9
  94. package/src/config.test.ts +81 -0
  95. package/src/config.ts +39 -2
  96. package/src/core/errors.ts +9 -0
  97. package/src/core/index.ts +1 -0
  98. package/src/core/runner-control-sessions.test.ts +404 -0
  99. package/src/core/runner-control-sessions.ts +51 -11
  100. package/src/core/runner-instances.ts +1 -0
  101. package/src/core/runner-labels.test.ts +53 -0
  102. package/src/core/runner-labels.ts +51 -0
  103. package/src/core/runner-sessions.test.ts +34 -1
  104. package/src/core/runner-sessions.ts +8 -1
  105. package/src/db/ephemeral-registration-tokens.ts +15 -1
  106. package/src/db/index.ts +1 -0
  107. package/src/db/reservations.test.ts +537 -0
  108. package/src/db/reservations.ts +116 -17
  109. package/src/db/runner-instances.test.ts +276 -1
  110. package/src/db/runner-instances.ts +39 -3
  111. package/src/db/runner-sessions.ts +39 -15
  112. package/src/db/schema/runner-instances.ts +3 -0
  113. package/src/metrics/index.ts +2 -0
  114. package/src/metrics/instance.ts +18 -0
  115. package/src/metrics/service.test.ts +99 -0
  116. package/src/metrics/service.ts +24 -4
  117. package/src/presentation/routes/late-runner-enrollment.test.ts +230 -0
  118. package/src/presentation/routes/poll-demand.test.ts +116 -3
  119. package/src/presentation/routes/poll-demand.ts +18 -6
  120. package/src/presentation/routes/register.test.ts +116 -2
  121. package/src/presentation/routes/register.ts +7 -0
  122. package/src/presentation/routes/report-runner-instances.test.ts +33 -0
  123. package/src/presentation/routes/report-runner-instances.ts +1 -0
  124. package/src/presentation/routes/runner-enrollment.test.ts +65 -5
  125. package/src/temporal/activities/maintenance-activities.test.ts +24 -31
  126. package/test/env.ts +1 -0
  127. package/test/fixtures/late-runner-enrollment.ts +103 -0
  128. package/test/index.ts +5 -0
  129. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,7 +2,6 @@ import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
2
2
  import type {NodePgDatabase} from '@shipfox/node-drizzle';
3
3
  import {logger} from '@shipfox/node-opentelemetry';
4
4
  import {extractDisplayPrefix, generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';
5
- import {canonicalizeLabels} from '@shipfox/runner-labels';
6
5
  import {and, eq, gt, isNull, notInArray, or, sql} from 'drizzle-orm';
7
6
  import {config} from '#config.js';
8
7
  import {
@@ -11,12 +10,17 @@ import {
11
10
  RunnerInstanceAlreadyAssignedError,
12
11
  RunnerInstanceNotAssignableError,
13
12
  } from '#core/errors.js';
13
+ import {sanitizeRunnerLabels} from '#core/runner-labels.js';
14
14
  import {db, type schema, type Tx} from '#db/db.js';
15
15
  import {assignRunnerInstancesTx} from '#db/runner-assignments.js';
16
16
  import {terminalStates} from '#db/runner-instances.js';
17
17
  import {provisionerTokens} from '#db/schema/provisioner-tokens.js';
18
18
  import {runnerBootstrapTokens, runnerControlSessions} from '#db/schema/runner-control-sessions.js';
19
19
  import {providerRunners} from '#db/schema/runner-instances.js';
20
+ import {
21
+ type RunnerReservationPromotionFailureReason,
22
+ recordRunnerReservationPromotionFailure,
23
+ } from '#metrics/index.js';
20
24
  import {issueRunnerActivationTokenTx} from './runner-activation.js';
21
25
 
22
26
  export class RunnerBootstrapTokenInvalidError extends Error {
@@ -141,8 +145,12 @@ export async function enrollRunnerControlSession(params: {
141
145
  }): Promise<string | null> {
142
146
  return await db().transaction(async (tx) => {
143
147
  const [current] = await tx
144
- .select({intendedReservationId: providerRunners.intendedReservationId})
148
+ .select({
149
+ intendedReservationId: providerRunners.intendedReservationId,
150
+ provisionerScope: provisionerTokens.scope,
151
+ })
145
152
  .from(providerRunners)
153
+ .innerJoin(provisionerTokens, eq(provisionerTokens.id, providerRunners.provisionerId))
146
154
  .where(
147
155
  and(
148
156
  eq(providerRunners.id, params.runnerInstanceId),
@@ -155,13 +163,37 @@ export async function enrollRunnerControlSession(params: {
155
163
  sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${current.intendedReservationId}`}))`,
156
164
  );
157
165
 
158
- const [updated] = await tx
166
+ // Provider reports may populate reservationId before the assignment commits. assignedAt is
167
+ // written by the assignment transaction, so keep the guard on that write boundary.
168
+ const [metadataUpdated] = await tx
159
169
  .update(providerRunners)
160
170
  .set({
161
- labels: [...canonicalizeLabels(params.labels)],
171
+ labels: sanitizeRunnerLabels(params.labels, {
172
+ scope: current.provisionerScope,
173
+ source: 'runner control enrollment',
174
+ }),
162
175
  providerKind: params.providerKind,
163
176
  protocolVersion: params.protocolVersion,
164
177
  capabilities: params.capabilities ?? null,
178
+ })
179
+ .where(
180
+ and(
181
+ eq(providerRunners.id, params.runnerInstanceId),
182
+ eq(providerRunners.provisionerId, params.provisionerId),
183
+ isNull(providerRunners.assignedAt),
184
+ notInArray(providerRunners.state, [...terminalStates]),
185
+ ),
186
+ )
187
+ .returning({id: providerRunners.id});
188
+ if (!metadataUpdated)
189
+ logger().debug(
190
+ {runnerInstanceId: params.runnerInstanceId, provisionerId: params.provisionerId},
191
+ 'Runner enrollment metadata update skipped for assigned or terminal runner',
192
+ );
193
+
194
+ const [updated] = await tx
195
+ .update(providerRunners)
196
+ .set({
165
197
  state: 'running',
166
198
  reportedAt: sql`now()`,
167
199
  updatedAt: sql`now()`,
@@ -198,29 +230,37 @@ export async function enrollRunnerControlSession(params: {
198
230
  });
199
231
  });
200
232
  } catch (error) {
201
- const expectedFailure =
202
- error instanceof ReservationExpiredError ||
203
- error instanceof ReservationNotFoundError ||
204
- error instanceof RunnerInstanceAlreadyAssignedError ||
205
- error instanceof RunnerInstanceNotAssignableError;
233
+ const expectedFailureReason = getRunnerReservationPromotionFailureReason(error);
206
234
  const details = {
207
235
  err: error,
208
236
  runnerInstanceId: params.runnerInstanceId,
209
237
  provisionerId: params.provisionerId,
210
238
  reservationId: intendedReservationId,
211
239
  };
212
- if (expectedFailure)
240
+ if (expectedFailureReason) {
241
+ recordRunnerReservationPromotionFailure(expectedFailureReason);
213
242
  logger().debug(details, 'Runner reservation could not be promoted during enrollment');
214
- else
243
+ } else {
215
244
  logger().error(
216
245
  details,
217
246
  'Unexpected failure promoting runner reservation during enrollment',
218
247
  );
248
+ }
219
249
  return null;
220
250
  }
221
251
  });
222
252
  }
223
253
 
254
+ function getRunnerReservationPromotionFailureReason(
255
+ error: unknown,
256
+ ): RunnerReservationPromotionFailureReason | null {
257
+ if (error instanceof ReservationExpiredError) return 'reservation-expired';
258
+ if (error instanceof ReservationNotFoundError) return 'reservation-not-found';
259
+ if (error instanceof RunnerInstanceAlreadyAssignedError) return 'already-assigned';
260
+ if (error instanceof RunnerInstanceNotAssignableError) return 'not-assignable';
261
+ return null;
262
+ }
263
+
224
264
  export async function attachRunnerControlProviderId(params: {
225
265
  runnerInstanceId: string;
226
266
  provisionerId: string;
@@ -23,6 +23,7 @@ import {
23
23
  import {config} from '../config.js';
24
24
 
25
25
  export interface ReportRunnerInstancesParams {
26
+ scope: 'installation' | 'workspace';
26
27
  workspaceId: string | null;
27
28
  provisionerId: string;
28
29
  events: RunnerInstanceReportEvent[];
@@ -0,0 +1,53 @@
1
+ import {RunnerLabelsReservedError} from './errors.js';
2
+ import {sanitizeRunnerLabels, sanitizeRunnerLabelsOrThrow} from './runner-labels.js';
3
+
4
+ describe('sanitizeRunnerLabels', () => {
5
+ it('strips reserved labels from non-installation sources', () => {
6
+ const labels = sanitizeRunnerLabels([' Linux ', 'shipfox-managed', 'linux'], {
7
+ scope: 'workspace',
8
+ source: 'test',
9
+ reservedLabels: ['Shipfox-Managed'],
10
+ });
11
+
12
+ expect(labels).toEqual(['linux']);
13
+ });
14
+
15
+ it('preserves reserved labels for installation sources', () => {
16
+ const labels = sanitizeRunnerLabels([' Linux ', 'shipfox-managed'], {
17
+ scope: 'installation',
18
+ source: 'test',
19
+ reservedLabels: ['shipfox-managed'],
20
+ });
21
+
22
+ expect(labels).toEqual(['linux', 'shipfox-managed']);
23
+ });
24
+
25
+ it('does not change labels when no reserved labels are configured', () => {
26
+ const labels = sanitizeRunnerLabels([' Linux ', 'linux'], {
27
+ scope: 'manual',
28
+ source: 'test',
29
+ reservedLabels: [],
30
+ });
31
+
32
+ expect(labels).toEqual(['linux']);
33
+ });
34
+
35
+ it('uses the configured reserved labels when none are provided explicitly', () => {
36
+ const labels = sanitizeRunnerLabels(['linux', 'shipfox-managed'], {
37
+ scope: 'workspace',
38
+ source: 'test',
39
+ });
40
+
41
+ expect(labels).toEqual(['linux']);
42
+ });
43
+
44
+ it('throws when sanitization removes every canonical label', () => {
45
+ expect(() =>
46
+ sanitizeRunnerLabelsOrThrow([' Shipfox-Managed '], {
47
+ scope: 'workspace',
48
+ source: 'test',
49
+ reservedLabels: ['shipfox-managed'],
50
+ }),
51
+ ).toThrow(RunnerLabelsReservedError);
52
+ });
53
+ });
@@ -0,0 +1,51 @@
1
+ import {logger} from '@shipfox/node-opentelemetry';
2
+ import {canonicalizeLabels} from '@shipfox/runner-labels';
3
+ import {runnerReservedLabels} from '#config.js';
4
+ import {RunnerLabelsReservedError} from './errors.js';
5
+
6
+ export type RunnerLabelScope = 'installation' | 'workspace' | 'manual';
7
+
8
+ type SanitizeRunnerLabelsParams = {
9
+ scope: RunnerLabelScope;
10
+ source: string;
11
+ logLevel?: 'debug' | 'warn';
12
+ reservedLabels?: readonly string[];
13
+ };
14
+
15
+ function sanitizeCanonicalRunnerLabels(
16
+ canonicalLabels: readonly string[],
17
+ params: SanitizeRunnerLabelsParams,
18
+ ): string[] {
19
+ if (params.scope === 'installation') return [...canonicalLabels];
20
+
21
+ const reservedLabels = new Set(canonicalizeLabels(params.reservedLabels ?? runnerReservedLabels));
22
+ if (reservedLabels.size === 0) return [...canonicalLabels];
23
+
24
+ const removedLabels = canonicalLabels.filter((label) => reservedLabels.has(label));
25
+ if (removedLabels.length === 0) return [...canonicalLabels];
26
+
27
+ const details = {source: params.source, removedLabels};
28
+ if (params.logLevel === 'debug')
29
+ logger().debug(details, 'Removed reserved runner labels from a non-installation source');
30
+ else logger().warn(details, 'Removed reserved runner labels from a non-installation source');
31
+ return canonicalLabels.filter((label) => !reservedLabels.has(label));
32
+ }
33
+
34
+ export function sanitizeRunnerLabels(
35
+ labels: readonly string[],
36
+ params: SanitizeRunnerLabelsParams,
37
+ ): string[] {
38
+ return sanitizeCanonicalRunnerLabels([...canonicalizeLabels(labels)], params);
39
+ }
40
+
41
+ export function sanitizeRunnerLabelsOrThrow(
42
+ labels: readonly string[],
43
+ params: SanitizeRunnerLabelsParams,
44
+ ): string[] {
45
+ const canonicalLabels = [...canonicalizeLabels(labels)];
46
+ const sanitizedLabels = sanitizeCanonicalRunnerLabels(canonicalLabels, params);
47
+ if (sanitizedLabels.length === 0 && canonicalLabels.length > 0) {
48
+ throw new RunnerLabelsReservedError(canonicalLabels);
49
+ }
50
+ return sanitizedLabels;
51
+ }
@@ -9,12 +9,14 @@ import {
9
9
  getRunnerSessionTokenClaims,
10
10
  manualRegistrationTokenFactory,
11
11
  providerRunnerFactory,
12
+ provisionerTokenFactory,
12
13
  runnersTestAuthClient,
13
14
  } from '#test/index.js';
14
15
  import {
15
16
  EmptyRunnerLabelsError,
16
17
  RegistrationTokenConsumedError,
17
18
  RegistrationTokenWorkspaceMismatchError,
19
+ RunnerLabelsReservedError,
18
20
  } from './errors.js';
19
21
  import {issueRunnerActivationToken} from './runner-activation.js';
20
22
  import {registerRunnerSession} from './runner-sessions.js';
@@ -66,6 +68,15 @@ describe('registerRunnerSession', () => {
66
68
  ).rejects.toBeInstanceOf(EmptyRunnerLabelsError);
67
69
  });
68
70
 
71
+ it('names labels removed by the reserved-label policy', () => {
72
+ const error = new RunnerLabelsReservedError(['shipfox-managed']);
73
+
74
+ expect(error.message).toBe(
75
+ 'All supplied runner labels are reserved for installation-scope provisioners: shipfox-managed',
76
+ );
77
+ expect(error.labels).toEqual(['shipfox-managed']);
78
+ });
79
+
69
80
  it('consumes an ephemeral token and creates a one-claim session', async () => {
70
81
  const token = await ephemeralRegistrationTokenFactory.create({workspaceId});
71
82
 
@@ -223,7 +234,8 @@ describe('activation runner sessions', () => {
223
234
 
224
235
  beforeEach(async () => {
225
236
  workspaceId = crypto.randomUUID();
226
- provisionerId = crypto.randomUUID();
237
+ const provisioner = await provisionerTokenFactory.create({workspaceId});
238
+ provisionerId = provisioner.id;
227
239
  const runner = await providerRunnerFactory.create({workspaceId, provisionerId});
228
240
  runnerInstanceId = runner.id;
229
241
  });
@@ -296,4 +308,25 @@ describe('activation runner sessions', () => {
296
308
  expect(storedToken?.consumedAt).toBeInstanceOf(Date);
297
309
  expect(sessions).toHaveLength(1);
298
310
  });
311
+
312
+ it('strips reserved labels from workspace activation registration', async () => {
313
+ const rawToken = await issueRunnerActivationToken({
314
+ runnerInstanceId,
315
+ provisionerId,
316
+ ttlSeconds: 60,
317
+ });
318
+ const [activationToken] = await db()
319
+ .select()
320
+ .from(runnerActivationTokens)
321
+ .where(eq(runnerActivationTokens.runnerInstanceId, runnerInstanceId));
322
+ if (!rawToken || !activationToken) throw new Error('Activation token was not created');
323
+
324
+ const result = await registerRunnerSession({
325
+ auth: runnersTestAuthClient,
326
+ credential: {kind: 'activation', activationTokenId: activationToken.id, workspaceId},
327
+ labels: ['linux', 'shipfox-managed'],
328
+ });
329
+
330
+ expect(result.session.labels).toEqual(['linux']);
331
+ });
299
332
  });
@@ -8,6 +8,7 @@ import {
8
8
  } from '#db/runner-sessions.js';
9
9
  import type {RunnerSession} from './entities/runner-session.js';
10
10
  import {EmptyRunnerLabelsError} from './errors.js';
11
+ import {sanitizeRunnerLabelsOrThrow} from './runner-labels.js';
11
12
 
12
13
  export interface RegisterRunnerSessionResult {
13
14
  session: RunnerSession;
@@ -38,7 +39,13 @@ export async function registerRunnerSession(params: {
38
39
  labels: string[];
39
40
  toolCapabilities?: RunnerToolCapabilitiesDto | null;
40
41
  }): Promise<RegisterRunnerSessionResult> {
41
- const labels = [...canonicalizeLabels(params.labels)];
42
+ const labels =
43
+ params.credential.kind === 'manual'
44
+ ? sanitizeRunnerLabelsOrThrow(params.labels, {
45
+ scope: 'manual',
46
+ source: 'manual runner registration',
47
+ })
48
+ : [...canonicalizeLabels(params.labels)];
42
49
  if (labels.length === 0) throw new EmptyRunnerLabelsError();
43
50
 
44
51
  const mode = params.credential.kind;
@@ -4,6 +4,7 @@ import type {EphemeralRegistrationToken} from '#core/entities/ephemeral-registra
4
4
  import {
5
5
  ActiveEphemeralRegistrationTokenExistsError,
6
6
  ActiveEphemeralRegistrationTokensExistError,
7
+ EmptyRunnerLabelsError,
7
8
  RegistrationTokenBatchExceedsReservationError,
8
9
  RegistrationTokenConsumedError,
9
10
  RegistrationTokenExpiredError,
@@ -11,11 +12,13 @@ import {
11
12
  ReservationExpiredError,
12
13
  ReservationNotFoundError,
13
14
  } from '#core/errors.js';
15
+ import {sanitizeRunnerLabelsOrThrow} from '#core/runner-labels.js';
14
16
  import {db} from './db.js';
15
17
  import {
16
18
  ephemeralRegistrationTokens,
17
19
  toEphemeralRegistrationToken,
18
20
  } from './schema/ephemeral-registration-tokens.js';
21
+ import {provisionerTokens} from './schema/provisioner-tokens.js';
19
22
  import {reservations} from './schema/reservations.js';
20
23
  import {providerRunners} from './schema/runner-instances.js';
21
24
  import {runnerSessions, toRunnerSession} from './schema/runner-sessions.js';
@@ -307,6 +310,17 @@ export async function createRunnerSessionConsumingEphemeralToken(params: {
307
310
  );
308
311
  }
309
312
 
313
+ const [provisioner] = await tx
314
+ .select({scope: provisionerTokens.scope})
315
+ .from(provisionerTokens)
316
+ .where(eq(provisionerTokens.id, consumed[0].provisionerId))
317
+ .limit(1);
318
+ const labels = sanitizeRunnerLabelsOrThrow(params.labels, {
319
+ scope: provisioner?.scope ?? 'workspace',
320
+ source: 'ephemeral runner registration',
321
+ });
322
+ if (labels.length === 0) throw new EmptyRunnerLabelsError();
323
+
310
324
  const [session] = await tx
311
325
  .insert(runnerSessions)
312
326
  .values({
@@ -316,7 +330,7 @@ export async function createRunnerSessionConsumingEphemeralToken(params: {
316
330
  registrationTokenKind: 'ephemeral',
317
331
  provisionerId: consumed[0].provisionerId,
318
332
  providerRunnerId: consumed[0].providerRunnerId,
319
- labels: params.labels,
333
+ labels,
320
334
  toolCapabilities: params.toolCapabilities ?? null,
321
335
  toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,
322
336
  maxClaims: params.maxClaims,
package/src/db/index.ts CHANGED
@@ -90,6 +90,7 @@ export type {
90
90
  } from './runner-instances.js';
91
91
  export {
92
92
  attachRunnerInstanceProviderId,
93
+ countStaleEnrolledRunnerInstances,
93
94
  isTerminalState,
94
95
  listActiveRunnerInstanceCountsByTemplateTx,
95
96
  listActiveRunnerInstances,