@opengeni/api-router 0.5.3 → 0.5.5

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 (49) hide show
  1. package/dist/app.d.ts +9 -1
  2. package/dist/app.js +7 -1
  3. package/dist/{chunk-3HIA43CC.js → chunk-HBEJMWD3.js} +5470 -2223
  4. package/dist/chunk-HBEJMWD3.js.map +1 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +279 -55
  7. package/dist/index.js.map +1 -1
  8. package/package.json +20 -20
  9. package/src/app.ts +583 -166
  10. package/src/auth/managed-auth.ts +32 -16
  11. package/src/http/auth.ts +8 -1
  12. package/src/http/common.ts +6 -2
  13. package/src/http/sse.ts +84 -8
  14. package/src/index.ts +178 -75
  15. package/src/integrations/oauth-client.ts +403 -120
  16. package/src/integrations/provider-domain.ts +4 -1
  17. package/src/mcp/documents.ts +173 -94
  18. package/src/mcp/server.ts +1600 -693
  19. package/src/mcp/session-view.ts +8 -2
  20. package/src/mcp/toolspace.ts +175 -84
  21. package/src/observability.ts +7 -1
  22. package/src/routes/api-keys.ts +39 -23
  23. package/src/routes/billing.ts +180 -65
  24. package/src/routes/capabilities.ts +17 -8
  25. package/src/routes/catalog-assets.ts +5 -2
  26. package/src/routes/codex.ts +244 -63
  27. package/src/routes/connections.ts +71 -33
  28. package/src/routes/documents.ts +242 -92
  29. package/src/routes/enrollments.ts +100 -70
  30. package/src/routes/environments.ts +205 -136
  31. package/src/routes/files.ts +164 -39
  32. package/src/routes/github.ts +123 -50
  33. package/src/routes/install.ts +9 -2
  34. package/src/routes/machines.ts +9 -8
  35. package/src/routes/packs.ts +141 -89
  36. package/src/routes/rigs.ts +189 -0
  37. package/src/routes/scheduled-tasks.ts +51 -9
  38. package/src/routes/sessions.ts +870 -328
  39. package/src/routes/social.ts +50 -38
  40. package/src/routes/workspace-capture.ts +238 -0
  41. package/src/routes/workspaces.ts +146 -13
  42. package/src/sandbox/access.ts +11 -3
  43. package/src/sandbox/auth-callout.ts +5 -1
  44. package/src/sandbox/channel-a.ts +104 -27
  45. package/src/sandbox/enrollment.ts +13 -3
  46. package/src/sandbox/machines.ts +68 -59
  47. package/src/sandbox/metrics-ingestion.ts +238 -17
  48. package/src/sandbox/viewer.ts +172 -46
  49. package/dist/chunk-3HIA43CC.js.map +0 -1
package/src/app.ts CHANGED
@@ -3,16 +3,27 @@ import {
3
3
  configuredAllowedReasoningEfforts,
4
4
  configuredModels,
5
5
  } from "@opengeni/config";
6
- import { ClientConfig, type AccessGrant } from "@opengeni/contracts";
7
- import { createDocumentServices, indexDocumentNow, type DocumentServices } from "@opengeni/documents";
8
- import { dbSql } from "@opengeni/db";
6
+ import {
7
+ ClientConfig,
8
+ OPENGENI_API_CONTRACT_HEADER,
9
+ OPENGENI_API_CONTRACT_REVISION,
10
+ resolveWorkspaceMemoryEnabled,
11
+ type AccessGrant,
12
+ } from "@opengeni/contracts";
13
+ import {
14
+ createDocumentServices,
15
+ indexDocumentNow,
16
+ type DocumentServices,
17
+ } from "@opengeni/documents";
18
+ import { dbSql, getWorkspace } from "@opengeni/db";
9
19
  import { createObservability } from "@opengeni/observability";
10
20
  import { createObjectStorage } from "@opengeni/storage";
11
21
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
12
22
  import { Hono } from "hono";
23
+ import { bodyLimit } from "hono/body-limit";
13
24
  import { cors } from "hono/cors";
14
25
  import { HTTPException } from "hono/http-exception";
15
- import type { ApiRouteDeps, AppDependencies, ObjectStorageDependency, SessionWorkflowClient } from "@opengeni/core";
26
+ import type { ApiRouteDeps, AppDependencies } from "@opengeni/core";
16
27
  import { hasPermission, requireAccessGrant, requirePermission } from "@opengeni/core";
17
28
  import { createManagedAuth } from "./auth/managed-auth";
18
29
  import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
@@ -34,6 +45,7 @@ import { registerBillingRoutes } from "./routes/billing";
34
45
  import { registerGitHubRoutes } from "./routes/github";
35
46
  import { registerInstallRoutes } from "./routes/install";
36
47
  import { registerPackRoutes } from "./routes/packs";
48
+ import { registerRigRoutes } from "./routes/rigs";
37
49
  import { registerScheduledTaskRoutes } from "./routes/scheduled-tasks";
38
50
  import { registerSessionRoutes } from "./routes/sessions";
39
51
  import { registerSocialRoutes } from "./routes/social";
@@ -57,26 +69,51 @@ export {
57
69
  withDefaultEnabledCapabilityMcpTools,
58
70
  } from "@opengeni/core";
59
71
  export { workflowIdForSession } from "@opengeni/core";
60
- export { replaySessionEvents, sseSessionStream } from "./http/sse";
72
+ export { replaySessionEvents, sseSessionStream, sseWorkspaceControlStream } from "./http/sse";
73
+
74
+ export const API_MAX_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
61
75
 
62
76
  export function createApp(deps: AppDependencies): Hono {
63
77
  const managedAuth = deps.managedAuth ?? createManagedAuth(deps.settings, deps.db);
64
- const objectStorage = createObjectStorage(deps.settings);
78
+ const objectStorage =
79
+ deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
65
80
  let documentServices: DocumentServices | null = deps.documentServices ?? null;
66
81
  const getDocumentServices = () => {
67
82
  documentServices ??= createDocumentServices(deps.settings);
68
83
  return documentServices;
69
84
  };
70
85
  const documentIndexer = deps.documentIndexer ?? {
71
- indexDocument: async ({ accountId, workspaceId, documentId }: { accountId: string; workspaceId: string; documentId: string }) => {
86
+ indexDocument: async ({
87
+ accountId,
88
+ workspaceId,
89
+ documentId,
90
+ }: {
91
+ accountId: string;
92
+ workspaceId: string;
93
+ documentId: string;
94
+ }) => {
72
95
  if (!objectStorage) {
73
- throw new HTTPException(503, { message: "object storage is not configured" });
96
+ throw new HTTPException(503, {
97
+ message: "object storage is not configured",
98
+ });
74
99
  }
75
- return await indexDocumentNow(deps.db, objectStorage, workspaceId, documentId, getDocumentServices(), {
76
- beforeEmbed: async ({ chunkCount }) => {
77
- await requireLimit(routeDeps, { accountId, workspaceId, action: "document:index", quantity: chunkCount });
100
+ return await indexDocumentNow(
101
+ deps.db,
102
+ objectStorage,
103
+ workspaceId,
104
+ documentId,
105
+ getDocumentServices(),
106
+ {
107
+ beforeEmbed: async ({ chunkCount }) => {
108
+ await requireLimit(routeDeps, {
109
+ accountId,
110
+ workspaceId,
111
+ action: "document:index",
112
+ quantity: chunkCount,
113
+ });
114
+ },
78
115
  },
79
- });
116
+ );
80
117
  },
81
118
  };
82
119
  // The API process's own agent-loop-free sandbox client — the API-direct
@@ -87,7 +124,8 @@ export function createApp(deps: AppDependencies): Hono {
87
124
  const resumeBoxById = deps.resumeBoxById ?? makeResumeBoxById(sandboxClient);
88
125
  const routeDeps: ApiRouteDeps = {
89
126
  ...deps,
90
- githubStateSecret: deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
127
+ githubStateSecret:
128
+ deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
91
129
  managedAuth,
92
130
  objectStorage,
93
131
  documentIndexer,
@@ -96,17 +134,39 @@ export function createApp(deps: AppDependencies): Hono {
96
134
  resumeBoxById,
97
135
  };
98
136
  const app = new Hono();
99
- const observability = deps.observability ?? createObservability(deps.settings, { component: "api" });
137
+ const observability =
138
+ deps.observability ?? createObservability(deps.settings, { component: "api" });
100
139
 
101
- app.use("*", cors({
102
- credentials: true,
103
- origin: (origin) => {
104
- if (!origin) {
105
- return null;
106
- }
107
- return allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? origin : null;
108
- },
109
- }));
140
+ app.use(
141
+ "*",
142
+ cors({
143
+ credentials: true,
144
+ allowHeaders: [
145
+ "Accept",
146
+ "Authorization",
147
+ "Content-Type",
148
+ "X-OpenGeni-Access-Key",
149
+ "X-OpenGeni-Api-Contract",
150
+ "X-OpenGeni-Subject",
151
+ ],
152
+ exposeHeaders: ["X-OpenGeni-Api-Contract"],
153
+ origin: (origin) => {
154
+ if (!origin) {
155
+ return null;
156
+ }
157
+ return allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? origin : null;
158
+ },
159
+ }),
160
+ );
161
+
162
+ app.use(
163
+ "*",
164
+ bodyLimit({
165
+ maxSize: API_MAX_REQUEST_BODY_BYTES,
166
+ onError: (c) =>
167
+ c.json({ code: "PAYLOAD_TOO_LARGE", message: "Request body is too large." }, 413),
168
+ }),
169
+ );
110
170
 
111
171
  app.use("*", async (c, next) => {
112
172
  const url = new URL(c.req.url);
@@ -121,7 +181,12 @@ export function createApp(deps: AppDependencies): Hono {
121
181
  await next();
122
182
  const status = c.res.status || 200;
123
183
  const durationSeconds = (performance.now() - start) / 1000;
124
- observability.recordHttpRequest({ method: c.req.method, route, status, durationSeconds });
184
+ observability.recordHttpRequest({
185
+ method: c.req.method,
186
+ route,
187
+ status,
188
+ durationSeconds,
189
+ });
125
190
  span.end({
126
191
  attributes: {
127
192
  "http.response.status_code": status,
@@ -139,7 +204,12 @@ export function createApp(deps: AppDependencies): Hono {
139
204
  } catch (error) {
140
205
  const status = httpStatusForError(error);
141
206
  const durationSeconds = (performance.now() - start) / 1000;
142
- observability.recordHttpRequest({ method: c.req.method, route, status, durationSeconds });
207
+ observability.recordHttpRequest({
208
+ method: c.req.method,
209
+ route,
210
+ status,
211
+ durationSeconds,
212
+ });
143
213
  span.end({
144
214
  attributes: {
145
215
  "http.response.status_code": status,
@@ -162,61 +232,92 @@ export function createApp(deps: AppDependencies): Hono {
162
232
 
163
233
  app.use("*", requireAccessKey(deps.settings));
164
234
 
235
+ app.use("/v1/*", async (c, next) => {
236
+ c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
237
+ if (
238
+ deps.settings.environment !== "test" &&
239
+ isApiContractProtectedMutation(c.req.method, new URL(c.req.url).pathname) &&
240
+ c.req.header(OPENGENI_API_CONTRACT_HEADER) !== OPENGENI_API_CONTRACT_REVISION
241
+ ) {
242
+ return c.json(
243
+ {
244
+ code: "API_CONTRACT_CHANGED",
245
+ message: "OpenGeni updated. Reload this client before changing state.",
246
+ apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
247
+ },
248
+ 409,
249
+ );
250
+ }
251
+ await next();
252
+ });
253
+
165
254
  if (managedAuth) {
166
255
  app.on(["GET", "POST"], "/v1/auth/*", (c) => managedAuth.handler(c.req.raw));
167
256
  }
168
257
 
169
- app.get("/healthz", (c) => c.json({
170
- service: deps.settings.serviceName,
171
- environment: deps.settings.environment,
172
- deploymentRevision: deps.settings.deploymentRevision,
173
- ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
174
- ok: true,
175
- }));
258
+ app.get("/healthz", (c) =>
259
+ c.json({
260
+ service: deps.settings.serviceName,
261
+ environment: deps.settings.environment,
262
+ deploymentRevision: deps.settings.deploymentRevision,
263
+ ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
264
+ ok: true,
265
+ }),
266
+ );
176
267
 
177
268
  app.get("/readyz", async (c) => {
178
269
  const result = await runReadinessChecks(readinessChecks(deps), 2_000);
179
270
  return c.json(result, result.ok ? 200 : 503);
180
271
  });
181
272
 
182
- app.get("/metrics", async (c) => c.text(await observability.prometheusMetrics(), 200, {
183
- "content-type": "text/plain; version=0.0.4; charset=utf-8",
184
- }));
273
+ app.get("/metrics", async (c) =>
274
+ c.text(await observability.prometheusMetrics(), 200, {
275
+ "content-type": "text/plain; version=0.0.4; charset=utf-8",
276
+ }),
277
+ );
185
278
 
186
- app.get("/v1/config/client", (c) => c.json(ClientConfig.parse({
187
- deploymentRevision: deps.settings.deploymentRevision,
188
- ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
189
- defaultModel: deps.settings.openaiModel,
190
- allowedModels: configuredAllowedModels(deps.settings),
191
- // Provider-grouped model list for the picker. configuredModels() carries the
192
- // union of the built-in allow-list and every registry provider's models, in
193
- // selection order (default model first); project each to the client-safe
194
- // ClientModel shape (ConfiguredModel.providerId → ClientModel.provider).
195
- models: configuredModels(deps.settings).map((model) => ({
196
- id: model.id,
197
- label: model.label,
198
- provider: model.providerId,
199
- providerLabel: model.providerLabel,
200
- api: model.api,
201
- ...(model.contextWindowTokens === undefined ? {} : { contextWindowTokens: model.contextWindowTokens }),
202
- })),
203
- defaultReasoningEffort: deps.settings.openaiReasoningEffort,
204
- allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
205
- mcpServers: deps.settings.mcpServers.map((server) => ({
206
- id: server.id,
207
- name: server.name ?? server.id,
208
- })),
209
- fileUploads: {
210
- enabled: objectStorage !== null,
211
- maxSizeBytes: objectStorage?.maxSinglePutSizeBytes ?? 5_000_000_000,
212
- },
213
- productAccessMode: deps.settings.productAccessMode,
214
- auth: clientAuthConfig(deps.settings),
215
- // Channel-A structured services (P4.4) ride exec/readFile/createEditor,
216
- // available on every real backend; `none` has no box so they are all off.
217
- // Per-session availability is still negotiated on /stream-capabilities.
218
- structuredServices: structuredServicesHint(deps.settings.sandboxBackend),
219
- })));
279
+ app.get("/v1/config/client", (c) => {
280
+ c.header("cache-control", "no-store");
281
+ return c.json(
282
+ ClientConfig.parse({
283
+ deploymentRevision: deps.settings.deploymentRevision,
284
+ apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
285
+ ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
286
+ defaultModel: deps.settings.openaiModel,
287
+ allowedModels: configuredAllowedModels(deps.settings),
288
+ // Provider-grouped model list for the picker. configuredModels() carries the
289
+ // union of the built-in allow-list and every registry provider's models, in
290
+ // selection order (default model first); project each to the client-safe
291
+ // ClientModel shape (ConfiguredModel.providerId → ClientModel.provider).
292
+ models: configuredModels(deps.settings).map((model) => ({
293
+ id: model.id,
294
+ label: model.label,
295
+ provider: model.providerId,
296
+ providerLabel: model.providerLabel,
297
+ api: model.api,
298
+ ...(model.contextWindowTokens === undefined
299
+ ? {}
300
+ : { contextWindowTokens: model.contextWindowTokens }),
301
+ })),
302
+ defaultReasoningEffort: deps.settings.openaiReasoningEffort,
303
+ allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
304
+ mcpServers: deps.settings.mcpServers.map((server) => ({
305
+ id: server.id,
306
+ name: server.name ?? server.id,
307
+ })),
308
+ fileUploads: {
309
+ enabled: objectStorage !== null,
310
+ maxSizeBytes: objectStorage?.maxSinglePutSizeBytes ?? 5_000_000_000,
311
+ },
312
+ productAccessMode: deps.settings.productAccessMode,
313
+ auth: clientAuthConfig(deps.settings),
314
+ // Channel-A structured services (P4.4) ride exec/readFile/createEditor,
315
+ // available on every real backend; `none` has no box so they are all off.
316
+ // Per-session availability is still negotiated on /stream-capabilities.
317
+ structuredServices: structuredServicesHint(deps.settings.sandboxBackend),
318
+ }),
319
+ );
320
+ });
220
321
 
221
322
  app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
222
323
  const workspaceId = c.req.param("workspaceId");
@@ -224,10 +325,15 @@ export function createApp(deps: AppDependencies): Hono {
224
325
  const toolspace = isToolspaceGrant(routeDeps.settings, grant)
225
326
  ? await prepareToolspaceMcpSurface({ deps: routeDeps, grant })
226
327
  : null;
227
- const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
328
+ const workspace = await getWorkspace(routeDeps.db, workspaceId);
329
+ const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
330
+ const transport = new WebStandardStreamableHTTPServerTransport({
331
+ enableJsonResponse: true,
332
+ });
228
333
  const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
229
334
  requestOrigin: new URL(c.req.url).origin,
230
335
  toolspace,
336
+ workspaceMemoryEnabled,
231
337
  });
232
338
  try {
233
339
  await mcp.connect(transport);
@@ -251,6 +357,7 @@ export function createApp(deps: AppDependencies): Hono {
251
357
  registerEnrollmentRoutes(app, routeDeps);
252
358
  registerMachineRoutes(app, routeDeps);
253
359
  registerEnvironmentRoutes(app, routeDeps);
360
+ registerRigRoutes(app, routeDeps);
254
361
  registerPackRoutes(app, routeDeps);
255
362
  registerSessionRoutes(app, routeDeps);
256
363
  registerScheduledTaskRoutes(app, routeDeps);
@@ -259,7 +366,11 @@ export function createApp(deps: AppDependencies): Hono {
259
366
  return app;
260
367
  }
261
368
 
262
- async function requireMcpAccessGrant(c: Parameters<typeof requireAccessGrant>[0], deps: ApiRouteDeps, workspaceId: string): Promise<AccessGrant> {
369
+ async function requireMcpAccessGrant(
370
+ c: Parameters<typeof requireAccessGrant>[0],
371
+ deps: ApiRouteDeps,
372
+ workspaceId: string,
373
+ ): Promise<AccessGrant> {
263
374
  const grant = await requireAccessGrant(c, deps, workspaceId);
264
375
  if (hasPermission(grant.permissions, "workspace:read")) {
265
376
  return grant;
@@ -276,15 +387,26 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
276
387
  return { mode: "managedSession" as const, session: "cookie" as const };
277
388
  }
278
389
  if (settings.productAccessMode === "configured") {
279
- return { mode: "configuredToken" as const, headerName: "authorization" as const, scheme: "bearer" as const };
390
+ return {
391
+ mode: "configuredToken" as const,
392
+ headerName: "authorization" as const,
393
+ scheme: "bearer" as const,
394
+ };
280
395
  }
281
396
  if (settings.authRequired) {
282
- return { mode: "deploymentKey" as const, headerName: "x-opengeni-access-key" as const };
397
+ return {
398
+ mode: "deploymentKey" as const,
399
+ headerName: "x-opengeni-access-key" as const,
400
+ };
283
401
  }
284
402
  return { mode: "none" as const };
285
403
  }
286
404
 
287
- function structuredServicesHint(backend: string): { fileSystem: boolean; git: boolean; terminalEvents: boolean } {
405
+ function structuredServicesHint(backend: string): {
406
+ fileSystem: boolean;
407
+ git: boolean;
408
+ terminalEvents: boolean;
409
+ } {
288
410
  const hasBox = backend !== "none";
289
411
  return { fileSystem: hasBox, git: hasBox, terminalEvents: hasBox };
290
412
  }
@@ -305,36 +427,56 @@ type ReadinessChecks = Record<ReadinessCheckName, () => Promise<void> | void>;
305
427
 
306
428
  function readinessChecks(deps: AppDependencies): ReadinessChecks {
307
429
  return {
308
- db: deps.readinessChecks?.db ?? (async () => {
309
- await deps.db.execute(dbSql`select 1`);
310
- }),
311
- nats: deps.readinessChecks?.nats ?? (() => {
312
- if (deps.bus.isConnected && !deps.bus.isConnected()) {
313
- throw new Error("NATS is not connected");
314
- }
315
- }),
316
- temporal: deps.readinessChecks?.temporal ?? deps.workflowClient.check ?? (() => {
317
- throw new Error("Temporal readiness check unavailable");
318
- }),
430
+ db:
431
+ deps.readinessChecks?.db ??
432
+ (async () => {
433
+ await deps.db.execute(dbSql`select 1`);
434
+ }),
435
+ nats:
436
+ deps.readinessChecks?.nats ??
437
+ (() => {
438
+ if (deps.bus.isConnected && !deps.bus.isConnected()) {
439
+ throw new Error("NATS is not connected");
440
+ }
441
+ }),
442
+ temporal:
443
+ deps.readinessChecks?.temporal ??
444
+ deps.workflowClient.check ??
445
+ (() => {
446
+ throw new Error("Temporal readiness check unavailable");
447
+ }),
319
448
  };
320
449
  }
321
450
 
322
- async function runReadinessChecks(checks: ReadinessChecks, timeoutMs: number): Promise<{
451
+ async function runReadinessChecks(
452
+ checks: ReadinessChecks,
453
+ timeoutMs: number,
454
+ ): Promise<{
323
455
  ok: boolean;
324
456
  checks: Record<ReadinessCheckName, { ok: boolean; error?: string }>;
325
457
  }> {
326
458
  const entries = await Promise.all(
327
- (Object.entries(checks) as Array<[ReadinessCheckName, () => Promise<void> | void]>)
328
- .map(async ([name, check]) => {
459
+ (Object.entries(checks) as Array<[ReadinessCheckName, () => Promise<void> | void]>).map(
460
+ async ([name, check]) => {
329
461
  try {
330
462
  await withTimeout(Promise.resolve().then(check), timeoutMs);
331
463
  return [name, { ok: true }] as const;
332
464
  } catch (error) {
333
- return [name, { ok: false, error: error instanceof Error ? error.message : String(error) }] as const;
465
+ return [
466
+ name,
467
+ {
468
+ ok: false,
469
+ error: error instanceof Error ? error.message : String(error),
470
+ },
471
+ ] as const;
334
472
  }
335
- }),
473
+ },
474
+ ),
336
475
  );
337
- const result = Object.fromEntries(entries) as Record<ReadinessCheckName, { ok: boolean; error?: string }>;
476
+ const result = Object.fromEntries(entries) as Record<
477
+ ReadinessCheckName,
478
+ { ok: boolean; error?: string }
479
+ >;
338
480
  return {
339
481
  ok: Object.values(result).every((check) => check.ok),
340
482
  checks: result,
@@ -347,7 +489,10 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T
347
489
  return await Promise.race([
348
490
  promise,
349
491
  new Promise<never>((_, reject) => {
350
- timer = setTimeout(() => reject(new Error(`readiness check timed out after ${timeoutMs}ms`)), timeoutMs);
492
+ timer = setTimeout(
493
+ () => reject(new Error(`readiness check timed out after ${timeoutMs}ms`)),
494
+ timeoutMs,
495
+ );
351
496
  }),
352
497
  ]);
353
498
  } finally {
@@ -360,88 +505,334 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T
360
505
  const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
361
506
  { pattern: /^\/healthz$/, label: "/healthz" },
362
507
  { pattern: /^\/readyz$/, label: "/readyz" },
363
- { pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/, label: "/v1/workspaces/:workspaceId/codex/connect/start" },
364
- { pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/poll$/, label: "/v1/workspaces/:workspaceId/codex/connect/poll" },
365
- { pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/, label: "/v1/workspaces/:workspaceId/codex/status" },
366
- { pattern: /^\/v1\/workspaces\/[^/]+\/codex\/usage$/, label: "/v1/workspaces/:workspaceId/codex/usage" },
367
- { pattern: /^\/v1\/workspaces\/[^/]+\/codex$/, label: "/v1/workspaces/:workspaceId/codex" },
508
+ {
509
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/,
510
+ label: "/v1/workspaces/:workspaceId/codex/connect/start",
511
+ },
512
+ {
513
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/poll$/,
514
+ label: "/v1/workspaces/:workspaceId/codex/connect/poll",
515
+ },
516
+ {
517
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/,
518
+ label: "/v1/workspaces/:workspaceId/codex/status",
519
+ },
520
+ {
521
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/usage$/,
522
+ label: "/v1/workspaces/:workspaceId/codex/usage",
523
+ },
524
+ {
525
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex$/,
526
+ label: "/v1/workspaces/:workspaceId/codex",
527
+ },
368
528
  { pattern: /^\/metrics$/, label: "/metrics" },
369
529
  { pattern: /^\/v1\/config\/client$/, label: "/v1/config/client" },
370
530
  { pattern: /^\/v1\/billing$/, label: "/v1/billing" },
371
531
  { pattern: /^\/v1\/billing\/checkout$/, label: "/v1/billing/checkout" },
372
532
  { pattern: /^\/v1\/billing\/usage$/, label: "/v1/billing/usage" },
373
- { pattern: /^\/v1\/billing\/entitlements$/, label: "/v1/billing/entitlements" },
533
+ {
534
+ pattern: /^\/v1\/billing\/entitlements$/,
535
+ label: "/v1/billing/entitlements",
536
+ },
374
537
  { pattern: /^\/v1\/webhooks\/stripe$/, label: "/v1/webhooks/stripe" },
375
- { pattern: /^\/v1\/workspaces\/[^/]+\/mcp$/, label: "/v1/workspaces/:workspaceId/mcp" },
376
- { pattern: /^\/v1\/workspaces\/[^/]+\/mcp\/docs$/, label: "/v1/workspaces/:workspaceId/mcp/docs" },
377
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/, label: "/v1/workspaces/:workspaceId/sessions" },
378
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events\/stream$/, label: "/v1/workspaces/:workspaceId/sessions/:id/events/stream" },
379
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events$/, label: "/v1/workspaces/:workspaceId/sessions/:id/events" },
380
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/turns\/reorder$/, label: "/v1/workspaces/:workspaceId/sessions/:id/turns/reorder" },
381
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/turns\/[^/]+$/, label: "/v1/workspaces/:workspaceId/sessions/:id/turns/:turnId" },
382
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/turns$/, label: "/v1/workspaces/:workspaceId/sessions/:id/turns" },
383
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/stream-capabilities$/, label: "/v1/workspaces/:workspaceId/sessions/:id/stream-capabilities" },
384
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/viewers\/[^/]+\/heartbeat$/, label: "/v1/workspaces/:workspaceId/sessions/:id/viewers/:viewerId/heartbeat" },
385
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/viewers\/[^/]+$/, label: "/v1/workspaces/:workspaceId/sessions/:id/viewers/:viewerId" },
386
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/viewers$/, label: "/v1/workspaces/:workspaceId/sessions/:id/viewers" },
387
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/goal$/, label: "/v1/workspaces/:workspaceId/sessions/:id/goal" },
388
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+$/, label: "/v1/workspaces/:workspaceId/sessions/:id" },
389
- { pattern: /^\/v1\/workspaces\/[^/]+\/files\/uploads$/, label: "/v1/workspaces/:workspaceId/files/uploads" },
390
- { pattern: /^\/v1\/workspaces\/[^/]+\/files\/uploads\/[^/]+\/complete$/, label: "/v1/workspaces/:workspaceId/files/uploads/:id/complete" },
391
- { pattern: /^\/v1\/workspaces\/[^/]+\/files\/[^/]+\/download-url$/, label: "/v1/workspaces/:workspaceId/files/:id/download-url" },
392
- { pattern: /^\/v1\/workspaces\/[^/]+\/files\/[^/]+$/, label: "/v1/workspaces/:workspaceId/files/:id" },
393
- { pattern: /^\/v1\/workspaces\/[^/]+\/api-keys$/, label: "/v1/workspaces/:workspaceId/api-keys" },
394
- { pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/, label: "/v1/workspaces/:workspaceId/api-keys/:id" },
395
- { pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/, label: "/v1/workspaces/:workspaceId/scheduled-tasks" },
396
- { pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/pause$/, label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/pause" },
397
- { pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/resume$/, label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/resume" },
398
- { pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/trigger$/, label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/trigger" },
399
- { pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/runs$/, label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/runs" },
400
- { pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+$/, label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id" },
401
- { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases$/, label: "/v1/workspaces/:workspaceId/document-bases" },
402
- { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents\/[^/]+\/reindex$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/documents/:documentId/reindex" },
403
- { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents\/[^/]+$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/documents/:documentId" },
404
- { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/documents" },
405
- { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/search$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/search" },
406
- { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+$/, label: "/v1/workspaces/:workspaceId/document-bases/:id" },
407
- { pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/search$/, label: "/v1/workspaces/:workspaceId/knowledge/search" },
408
- { pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/memories\/[^/]+$/, label: "/v1/workspaces/:workspaceId/knowledge/memories/:id" },
409
- { pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/memories$/, label: "/v1/workspaces/:workspaceId/knowledge/memories" },
410
- { pattern: /^\/v1\/workspaces\/[^/]+\/github\/app$/, label: "/v1/workspaces/:workspaceId/github/app" },
411
- { pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories$/, label: "/v1/workspaces/:workspaceId/github/repositories" },
412
- { pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories\/sync$/, label: "/v1/workspaces/:workspaceId/github/repositories/sync" },
413
- { pattern: /^\/v1\/workspaces\/[^/]+\/github\/app-manifest$/, label: "/v1/workspaces/:workspaceId/github/app-manifest" },
414
- { pattern: /^\/v1\/workspaces\/[^/]+\/capabilities$/, label: "/v1/workspaces/:workspaceId/capabilities" },
415
- { pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/discovery\/mcp-registry$/, label: "/v1/workspaces/:workspaceId/capabilities/discovery/mcp-registry" },
416
- { pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/[^/]+\/enable$/, label: "/v1/workspaces/:workspaceId/capabilities/:id/enable" },
417
- { pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/[^/]+\/disable$/, label: "/v1/workspaces/:workspaceId/capabilities/:id/disable" },
418
- { pattern: /^\/v1\/workspaces\/[^/]+\/environments$/, label: "/v1/workspaces/:workspaceId/environments" },
419
- { pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+\/variables\/[^/]+$/, label: "/v1/workspaces/:workspaceId/environments/:id/variables/:name" },
420
- { pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+$/, label: "/v1/workspaces/:workspaceId/environments/:id" },
421
- { pattern: /^\/v1\/workspaces\/[^/]+\/packs$/, label: "/v1/workspaces/:workspaceId/packs" },
422
- { pattern: /^\/v1\/workspaces\/[^/]+\/packs\/installations$/, label: "/v1/workspaces/:workspaceId/packs/installations" },
423
- { pattern: /^\/v1\/workspaces\/[^/]+\/packs\/marketing-social-daily-analysis\/scheduled-tasks$/, label: "/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks" },
424
- { pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+\/enable$/, label: "/v1/workspaces/:workspaceId/packs/:id/enable" },
425
- { pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/, label: "/v1/workspaces/:workspaceId/packs/:id" },
426
- { pattern: /^\/v1\/workspaces\/[^/]+\/social\/connections$/, label: "/v1/workspaces/:workspaceId/social/connections" },
427
- { pattern: /^\/v1\/workspaces\/[^/]+\/social\/posts$/, label: "/v1/workspaces/:workspaceId/social/posts" },
428
- { pattern: /^\/v1\/workspaces\/[^/]+\/connections$/, label: "/v1/workspaces/:workspaceId/connections" },
429
- { pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/, label: "/v1/workspaces/:workspaceId/connections/oauth/start" },
430
- { pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/, label: "/v1/workspaces/:workspaceId/connections/:connectionId" },
538
+ {
539
+ pattern: /^\/v1\/workspaces\/[^/]+\/mcp$/,
540
+ label: "/v1/workspaces/:workspaceId/mcp",
541
+ },
542
+ {
543
+ pattern: /^\/v1\/workspaces\/[^/]+\/mcp\/docs$/,
544
+ label: "/v1/workspaces/:workspaceId/mcp/docs",
545
+ },
546
+ {
547
+ pattern: /^\/v1\/workspaces\/[^/]+\/default-rig$/,
548
+ label: "/v1/workspaces/:workspaceId/default-rig",
549
+ },
550
+ {
551
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/,
552
+ label: "/v1/workspaces/:workspaceId/sessions",
553
+ },
554
+ {
555
+ pattern: /^\/v1\/workspaces\/[^/]+\/control-events\/stream$/,
556
+ label: "/v1/workspaces/:workspaceId/control-events/stream",
557
+ },
558
+ {
559
+ pattern: /^\/v1\/workspaces\/[^/]+\/control-events$/,
560
+ label: "/v1/workspaces/:workspaceId/control-events",
561
+ },
562
+ {
563
+ pattern: /^\/v1\/workspaces\/[^/]+\/inference-control$/,
564
+ label: "/v1/workspaces/:workspaceId/inference-control",
565
+ },
566
+ {
567
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events\/stream$/,
568
+ label: "/v1/workspaces/:workspaceId/sessions/:id/events/stream",
569
+ },
570
+ {
571
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/lineage$/,
572
+ label: "/v1/workspaces/:workspaceId/sessions/:id/lineage",
573
+ },
574
+ {
575
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events$/,
576
+ label: "/v1/workspaces/:workspaceId/sessions/:id/events",
577
+ },
578
+ {
579
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue\/[^/]+\/(move|edit|steer|delete)$/,
580
+ label: "/v1/workspaces/:workspaceId/sessions/:id/queue/:turnId/:action",
581
+ },
582
+ {
583
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue$/,
584
+ label: "/v1/workspaces/:workspaceId/sessions/:id/queue",
585
+ },
586
+ {
587
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/composer-draft$/,
588
+ label: "/v1/workspaces/:workspaceId/sessions/:id/composer-draft",
589
+ },
590
+ {
591
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/(control|steer)$/,
592
+ label: "/v1/workspaces/:workspaceId/sessions/:id/:controlAction",
593
+ },
594
+ {
595
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/stream-capabilities$/,
596
+ label: "/v1/workspaces/:workspaceId/sessions/:id/stream-capabilities",
597
+ },
598
+ {
599
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/viewers\/[^/]+\/heartbeat$/,
600
+ label: "/v1/workspaces/:workspaceId/sessions/:id/viewers/:viewerId/heartbeat",
601
+ },
602
+ {
603
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/viewers\/[^/]+$/,
604
+ label: "/v1/workspaces/:workspaceId/sessions/:id/viewers/:viewerId",
605
+ },
606
+ {
607
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/viewers$/,
608
+ label: "/v1/workspaces/:workspaceId/sessions/:id/viewers",
609
+ },
610
+ {
611
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/goal$/,
612
+ label: "/v1/workspaces/:workspaceId/sessions/:id/goal",
613
+ },
614
+ {
615
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+$/,
616
+ label: "/v1/workspaces/:workspaceId/sessions/:id",
617
+ },
618
+ {
619
+ pattern: /^\/v1\/workspaces\/[^/]+\/files\/uploads$/,
620
+ label: "/v1/workspaces/:workspaceId/files/uploads",
621
+ },
622
+ {
623
+ pattern: /^\/v1\/workspaces\/[^/]+\/files\/uploads\/[^/]+\/complete$/,
624
+ label: "/v1/workspaces/:workspaceId/files/uploads/:id/complete",
625
+ },
626
+ {
627
+ pattern: /^\/v1\/workspaces\/[^/]+\/files\/[^/]+\/download-url$/,
628
+ label: "/v1/workspaces/:workspaceId/files/:id/download-url",
629
+ },
630
+ {
631
+ pattern: /^\/v1\/workspaces\/[^/]+\/files\/[^/]+$/,
632
+ label: "/v1/workspaces/:workspaceId/files/:id",
633
+ },
634
+ {
635
+ pattern: /^\/v1\/workspaces\/[^/]+\/api-keys$/,
636
+ label: "/v1/workspaces/:workspaceId/api-keys",
637
+ },
638
+ {
639
+ pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/,
640
+ label: "/v1/workspaces/:workspaceId/api-keys/:id",
641
+ },
642
+ {
643
+ pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/,
644
+ label: "/v1/workspaces/:workspaceId/scheduled-tasks",
645
+ },
646
+ {
647
+ pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/pause$/,
648
+ label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/pause",
649
+ },
650
+ {
651
+ pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/resume$/,
652
+ label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/resume",
653
+ },
654
+ {
655
+ pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/trigger$/,
656
+ label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/trigger",
657
+ },
658
+ {
659
+ pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+\/runs$/,
660
+ label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id/runs",
661
+ },
662
+ {
663
+ pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks\/[^/]+$/,
664
+ label: "/v1/workspaces/:workspaceId/scheduled-tasks/:id",
665
+ },
666
+ {
667
+ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases$/,
668
+ label: "/v1/workspaces/:workspaceId/document-bases",
669
+ },
670
+ {
671
+ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents\/[^/]+\/reindex$/,
672
+ label: "/v1/workspaces/:workspaceId/document-bases/:id/documents/:documentId/reindex",
673
+ },
674
+ {
675
+ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents\/[^/]+$/,
676
+ label: "/v1/workspaces/:workspaceId/document-bases/:id/documents/:documentId",
677
+ },
678
+ {
679
+ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents$/,
680
+ label: "/v1/workspaces/:workspaceId/document-bases/:id/documents",
681
+ },
682
+ {
683
+ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/search$/,
684
+ label: "/v1/workspaces/:workspaceId/document-bases/:id/search",
685
+ },
686
+ {
687
+ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+$/,
688
+ label: "/v1/workspaces/:workspaceId/document-bases/:id",
689
+ },
690
+ {
691
+ pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/search$/,
692
+ label: "/v1/workspaces/:workspaceId/knowledge/search",
693
+ },
694
+ {
695
+ pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/memories\/[^/]+$/,
696
+ label: "/v1/workspaces/:workspaceId/knowledge/memories/:id",
697
+ },
698
+ {
699
+ pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/memories$/,
700
+ label: "/v1/workspaces/:workspaceId/knowledge/memories",
701
+ },
702
+ {
703
+ pattern: /^\/v1\/workspaces\/[^/]+\/github\/app$/,
704
+ label: "/v1/workspaces/:workspaceId/github/app",
705
+ },
706
+ {
707
+ pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories$/,
708
+ label: "/v1/workspaces/:workspaceId/github/repositories",
709
+ },
710
+ {
711
+ pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories\/sync$/,
712
+ label: "/v1/workspaces/:workspaceId/github/repositories/sync",
713
+ },
714
+ {
715
+ pattern: /^\/v1\/workspaces\/[^/]+\/github\/app-manifest$/,
716
+ label: "/v1/workspaces/:workspaceId/github/app-manifest",
717
+ },
718
+ {
719
+ pattern: /^\/v1\/workspaces\/[^/]+\/capabilities$/,
720
+ label: "/v1/workspaces/:workspaceId/capabilities",
721
+ },
722
+ {
723
+ pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/discovery\/mcp-registry$/,
724
+ label: "/v1/workspaces/:workspaceId/capabilities/discovery/mcp-registry",
725
+ },
726
+ {
727
+ pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/[^/]+\/enable$/,
728
+ label: "/v1/workspaces/:workspaceId/capabilities/:id/enable",
729
+ },
730
+ {
731
+ pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/[^/]+\/disable$/,
732
+ label: "/v1/workspaces/:workspaceId/capabilities/:id/disable",
733
+ },
734
+ {
735
+ pattern: /^\/v1\/workspaces\/[^/]+\/environments$/,
736
+ label: "/v1/workspaces/:workspaceId/environments",
737
+ },
738
+ {
739
+ pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+\/variables\/[^/]+$/,
740
+ label: "/v1/workspaces/:workspaceId/environments/:id/variables/:name",
741
+ },
742
+ {
743
+ pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+$/,
744
+ label: "/v1/workspaces/:workspaceId/environments/:id",
745
+ },
746
+ {
747
+ pattern: /^\/v1\/workspaces\/[^/]+\/packs$/,
748
+ label: "/v1/workspaces/:workspaceId/packs",
749
+ },
750
+ {
751
+ pattern: /^\/v1\/workspaces\/[^/]+\/packs\/installations$/,
752
+ label: "/v1/workspaces/:workspaceId/packs/installations",
753
+ },
754
+ {
755
+ pattern: /^\/v1\/workspaces\/[^/]+\/packs\/marketing-social-daily-analysis\/scheduled-tasks$/,
756
+ label: "/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks",
757
+ },
758
+ {
759
+ pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+\/enable$/,
760
+ label: "/v1/workspaces/:workspaceId/packs/:id/enable",
761
+ },
762
+ {
763
+ pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/,
764
+ label: "/v1/workspaces/:workspaceId/packs/:id",
765
+ },
766
+ {
767
+ pattern: /^\/v1\/workspaces\/[^/]+\/social\/connections$/,
768
+ label: "/v1/workspaces/:workspaceId/social/connections",
769
+ },
770
+ {
771
+ pattern: /^\/v1\/workspaces\/[^/]+\/social\/posts$/,
772
+ label: "/v1/workspaces/:workspaceId/social/posts",
773
+ },
774
+ {
775
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections$/,
776
+ label: "/v1/workspaces/:workspaceId/connections",
777
+ },
778
+ {
779
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/,
780
+ label: "/v1/workspaces/:workspaceId/connections/oauth/start",
781
+ },
782
+ {
783
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/,
784
+ label: "/v1/workspaces/:workspaceId/connections/:connectionId",
785
+ },
431
786
  { pattern: /^\/v1\/catalog-assets\/.+$/, label: "/v1/catalog-assets/*" },
432
- { pattern: /^\/v1\/integrations\/oauth\/callback$/, label: "/v1/integrations/oauth/callback" },
433
- { pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/, label: "/v1/integrations/oauth/client-metadata.json" },
434
- { pattern: /^\/v1\/enrollments\/device\/start$/, label: "/v1/enrollments/device/start" },
435
- { pattern: /^\/v1\/enrollments\/device\/poll$/, label: "/v1/enrollments/device/poll" },
436
- { pattern: /^\/v1\/workspaces\/[^/]+\/enrollments\/device\/approve$/, label: "/v1/workspaces/:workspaceId/enrollments/device/approve" },
437
- { pattern: /^\/v1\/workspaces\/[^/]+\/enrollments\/[^/]+\/revoke$/, label: "/v1/workspaces/:workspaceId/enrollments/:id/revoke" },
438
- { pattern: /^\/v1\/workspaces\/[^/]+\/enrollments$/, label: "/v1/workspaces/:workspaceId/enrollments" },
439
- { pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/metrics\/series$/, label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series" },
440
- { pattern: /^\/v1\/workspaces\/[^/]+\/machines$/, label: "/v1/workspaces/:workspaceId/machines" },
441
- { pattern: /^\/v1\/github\/app-manifest\/callback$/, label: "/v1/github/app-manifest/callback" },
787
+ {
788
+ pattern: /^\/v1\/integrations\/oauth\/callback$/,
789
+ label: "/v1/integrations/oauth/callback",
790
+ },
791
+ {
792
+ pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/,
793
+ label: "/v1/integrations/oauth/client-metadata.json",
794
+ },
795
+ {
796
+ pattern: /^\/v1\/enrollments\/device\/start$/,
797
+ label: "/v1/enrollments/device/start",
798
+ },
799
+ {
800
+ pattern: /^\/v1\/enrollments\/device\/poll$/,
801
+ label: "/v1/enrollments/device/poll",
802
+ },
803
+ {
804
+ pattern: /^\/v1\/workspaces\/[^/]+\/enrollments\/device\/approve$/,
805
+ label: "/v1/workspaces/:workspaceId/enrollments/device/approve",
806
+ },
807
+ {
808
+ pattern: /^\/v1\/workspaces\/[^/]+\/enrollments\/[^/]+\/revoke$/,
809
+ label: "/v1/workspaces/:workspaceId/enrollments/:id/revoke",
810
+ },
811
+ {
812
+ pattern: /^\/v1\/workspaces\/[^/]+\/enrollments$/,
813
+ label: "/v1/workspaces/:workspaceId/enrollments",
814
+ },
815
+ {
816
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/metrics\/series$/,
817
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series",
818
+ },
819
+ {
820
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines$/,
821
+ label: "/v1/workspaces/:workspaceId/machines",
822
+ },
823
+ {
824
+ pattern: /^\/v1\/github\/app-manifest\/callback$/,
825
+ label: "/v1/github/app-manifest/callback",
826
+ },
442
827
  { pattern: /^\/v1\/github\/setup$/, label: "/v1/github/setup" },
443
- { pattern: /^\/v1\/github\/install\/callback$/, label: "/v1/github/install/callback" },
444
- { pattern: /^\/v1\/github\/oauth\/callback$/, label: "/v1/github/oauth/callback" },
828
+ {
829
+ pattern: /^\/v1\/github\/install\/callback$/,
830
+ label: "/v1/github/install/callback",
831
+ },
832
+ {
833
+ pattern: /^\/v1\/github\/oauth\/callback$/,
834
+ label: "/v1/github/oauth/callback",
835
+ },
445
836
  ];
446
837
 
447
838
  export function routeLabel(pathname: string): string {
@@ -451,3 +842,29 @@ export function routeLabel(pathname: string): string {
451
842
  }
452
843
  return pathname.startsWith("/v1/") ? "/v1/unknown" : "/unknown";
453
844
  }
845
+
846
+ /**
847
+ * State-changing OpenGeni HTTP calls must never cross an incompatible rollout
848
+ * boundary. Standard third-party protocols and externally initiated callbacks
849
+ * are intentionally outside this product API contract.
850
+ */
851
+ export function isApiContractProtectedMutation(method: string, pathname: string): boolean {
852
+ if (!new Set(["POST", "PUT", "PATCH", "DELETE"]).has(method.toUpperCase())) {
853
+ return false;
854
+ }
855
+ if (!pathname.startsWith("/v1/")) {
856
+ return false;
857
+ }
858
+ if (
859
+ pathname.startsWith("/v1/auth/") ||
860
+ pathname.startsWith("/v1/webhooks/") ||
861
+ pathname.startsWith("/v1/integrations/oauth/") ||
862
+ pathname.startsWith("/v1/github/") ||
863
+ pathname === "/v1/enrollments/device/start" ||
864
+ pathname === "/v1/enrollments/device/poll" ||
865
+ pathname === "/v1/enrollments/token/exchange"
866
+ ) {
867
+ return false;
868
+ }
869
+ return !pathname.split("/").includes("mcp");
870
+ }