@opengeni/api-router 2.6.4 → 2.8.1-canary.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 (43) hide show
  1. package/dist/app.d.ts +2 -1
  2. package/dist/app.js +3 -1
  3. package/dist/{chunk-XTLI3CBH.js → chunk-HHKQMVY7.js} +3009 -1699
  4. package/dist/chunk-HHKQMVY7.js.map +1 -0
  5. package/dist/github-access.d.ts +25 -2
  6. package/dist/index.js +14 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/integrations/personal-github-repositories.d.ts +41 -2
  9. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  10. package/dist/model-catalog.d.ts +5 -31
  11. package/dist/routes/codex.d.ts +8 -1
  12. package/dist/routes/github.d.ts +2 -0
  13. package/dist/routes/organization-model-providers.d.ts +3 -0
  14. package/dist/workspace-deletion.d.ts +6 -0
  15. package/package.json +18 -18
  16. package/src/app.ts +55 -9
  17. package/src/auth/organization-user-setup.ts +3 -3
  18. package/src/github-access.ts +117 -1
  19. package/src/http/sse.ts +15 -7
  20. package/src/index.ts +13 -1
  21. package/src/integrations/personal-github-repositories.ts +238 -9
  22. package/src/integrations/slack-app-home.ts +2 -2
  23. package/src/integrations/slack-interactions.ts +77 -37
  24. package/src/mcp/company-brain-governed-writes.ts +2 -2
  25. package/src/mcp/company-profile-agent-admin.ts +1 -1
  26. package/src/mcp/remember.ts +1 -1
  27. package/src/mcp/server.ts +11 -2
  28. package/src/model-catalog.ts +45 -337
  29. package/src/routes/automations.ts +178 -19
  30. package/src/routes/codex.ts +136 -32
  31. package/src/routes/connections.ts +271 -16
  32. package/src/routes/github.ts +116 -15
  33. package/src/routes/organization-memberships.ts +50 -3
  34. package/src/routes/organization-model-providers.ts +231 -0
  35. package/src/routes/packs.ts +1 -0
  36. package/src/routes/personal-github.ts +39 -0
  37. package/src/routes/pr-review.ts +72 -4
  38. package/src/routes/scheduled-tasks.ts +40 -13
  39. package/src/routes/sessions.ts +88 -52
  40. package/src/routes/supergrok.ts +3 -2
  41. package/src/routes/workspaces.ts +405 -62
  42. package/src/workspace-deletion.ts +64 -0
  43. package/dist/chunk-XTLI3CBH.js.map +0 -1
@@ -0,0 +1,231 @@
1
+ import {
2
+ CreateOrganizationProviderCustomModelRequest,
3
+ DeleteOrganizationProviderCustomModelRequest,
4
+ OrganizationModelProviderConnectionResponse,
5
+ OrganizationModelProviderKind,
6
+ OrganizationProviderCustomModel,
7
+ OrganizationProviderCustomModelsResponse,
8
+ RevokeOrganizationModelProviderConnectionRequest,
9
+ UpsertOrganizationModelProviderConnectionRequest,
10
+ } from "@opengeni/contracts";
11
+ import { requireEnvironmentEncryption, type ApiRouteDeps } from "@opengeni/core";
12
+ import {
13
+ createOrganizationModelProviderCustomModel,
14
+ encryptEnvironmentValue,
15
+ getOrganizationModelProviderConnection,
16
+ listOrganizationModelProviderCustomModels,
17
+ organizationModelProviderCredentialDigest,
18
+ OrganizationModelProviderConflictError,
19
+ OrganizationModelProviderLimitError,
20
+ retireOrganizationModelProviderCustomModel,
21
+ revokeOrganizationModelProviderConnection,
22
+ upsertOrganizationModelProviderConnection,
23
+ } from "@opengeni/db";
24
+ import type { Context, Hono } from "hono";
25
+ import { HTTPException } from "hono/http-exception";
26
+ import { z } from "zod";
27
+
28
+ import { requireOrganizationCodexHuman, requireSameOriginBrowserMutation } from "./codex";
29
+
30
+ const OrganizationId = z.string().uuid();
31
+
32
+ function parseOrganizationId(value: string): string {
33
+ const parsed = OrganizationId.safeParse(value);
34
+ if (!parsed.success) throw new HTTPException(404, { message: "organization not found" });
35
+ return parsed.data;
36
+ }
37
+
38
+ async function jsonBody<T>(c: Context, schema: z.ZodType<T>, message: string): Promise<T> {
39
+ const parsed = schema.safeParse(await c.req.json().catch(() => null));
40
+ if (!parsed.success) throw new HTTPException(422, { message });
41
+ return parsed.data;
42
+ }
43
+
44
+ function providerKind(value: string) {
45
+ const parsed = OrganizationModelProviderKind.safeParse(value);
46
+ if (!parsed.success) throw new HTTPException(404, { message: "model provider not found" });
47
+ return parsed.data;
48
+ }
49
+
50
+ function connectionJson(connection: {
51
+ providerKind: "vercel_gateway" | "openrouter";
52
+ status: "active" | "revoked";
53
+ version: number;
54
+ createdAt: Date;
55
+ updatedAt: Date;
56
+ }) {
57
+ return OrganizationModelProviderConnectionResponse.parse({
58
+ ...connection,
59
+ createdAt: connection.createdAt.toISOString(),
60
+ updatedAt: connection.updatedAt.toISOString(),
61
+ });
62
+ }
63
+
64
+ function modelJson(model: {
65
+ id: string;
66
+ upstreamModelId: string;
67
+ label: string | null;
68
+ version: number;
69
+ createdAt: Date;
70
+ updatedAt: Date;
71
+ }) {
72
+ return OrganizationProviderCustomModel.parse({
73
+ ...model,
74
+ createdAt: model.createdAt.toISOString(),
75
+ updatedAt: model.updatedAt.toISOString(),
76
+ });
77
+ }
78
+
79
+ function conflict(error: unknown): never {
80
+ if (error instanceof OrganizationModelProviderConflictError) {
81
+ throw new HTTPException(409, { message: "organization model provider version conflict" });
82
+ }
83
+ if (error instanceof OrganizationModelProviderLimitError) {
84
+ throw new HTTPException(409, { message: "organization custom model limit reached" });
85
+ }
86
+ throw error;
87
+ }
88
+
89
+ export function registerOrganizationModelProviderRoutes(app: Hono, deps: ApiRouteDeps): void {
90
+ app.get("/v1/organizations/:organizationId/model-providers/:providerKind", async (c) => {
91
+ c.header("cache-control", "private, no-store");
92
+ const organizationId = parseOrganizationId(c.req.param("organizationId"));
93
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
94
+ const connection = await getOrganizationModelProviderConnection(deps.db, {
95
+ organizationId,
96
+ actorSubjectId: human.subjectId,
97
+ providerKind: providerKind(c.req.param("providerKind")),
98
+ });
99
+ return connection ? c.json(connectionJson(connection)) : c.json(null);
100
+ });
101
+
102
+ app.put("/v1/organizations/:organizationId/model-providers/:providerKind", async (c) => {
103
+ c.header("cache-control", "private, no-store");
104
+ requireSameOriginBrowserMutation(c, deps);
105
+ const organizationId = parseOrganizationId(c.req.param("organizationId"));
106
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
107
+ const payload = await jsonBody(
108
+ c,
109
+ UpsertOrganizationModelProviderConnectionRequest,
110
+ "invalid organization model provider connection",
111
+ );
112
+ try {
113
+ const connection = await upsertOrganizationModelProviderConnection(deps.db, {
114
+ organizationId,
115
+ actorSubjectId: human.subjectId,
116
+ providerKind: providerKind(c.req.param("providerKind")),
117
+ credentialEncrypted: encryptEnvironmentValue(
118
+ requireEnvironmentEncryption(deps.settings),
119
+ payload.apiKey,
120
+ ),
121
+ credentialDigest: organizationModelProviderCredentialDigest(payload.apiKey),
122
+ operationId: payload.operationId,
123
+ ...(payload.expectedVersion === undefined
124
+ ? {}
125
+ : { expectedVersion: payload.expectedVersion }),
126
+ });
127
+ return c.json(connectionJson(connection));
128
+ } catch (error) {
129
+ conflict(error);
130
+ }
131
+ });
132
+
133
+ app.delete("/v1/organizations/:organizationId/model-providers/:providerKind", async (c) => {
134
+ c.header("cache-control", "private, no-store");
135
+ requireSameOriginBrowserMutation(c, deps);
136
+ const organizationId = parseOrganizationId(c.req.param("organizationId"));
137
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
138
+ const payload = await jsonBody(
139
+ c,
140
+ RevokeOrganizationModelProviderConnectionRequest,
141
+ "invalid organization model provider disconnect",
142
+ );
143
+ try {
144
+ const connection = await revokeOrganizationModelProviderConnection(deps.db, {
145
+ organizationId,
146
+ actorSubjectId: human.subjectId,
147
+ providerKind: providerKind(c.req.param("providerKind")),
148
+ ...payload,
149
+ });
150
+ return c.json(connectionJson(connection));
151
+ } catch (error) {
152
+ conflict(error);
153
+ }
154
+ });
155
+
156
+ app.get(
157
+ "/v1/organizations/:organizationId/model-providers/:providerKind/custom-models",
158
+ async (c) => {
159
+ c.header("cache-control", "private, no-store");
160
+ const organizationId = parseOrganizationId(c.req.param("organizationId"));
161
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
162
+ const models = await listOrganizationModelProviderCustomModels(deps.db, {
163
+ organizationId,
164
+ actorSubjectId: human.subjectId,
165
+ providerKind: providerKind(c.req.param("providerKind")),
166
+ });
167
+ return c.json(
168
+ OrganizationProviderCustomModelsResponse.parse({ models: models.map(modelJson) }),
169
+ );
170
+ },
171
+ );
172
+
173
+ app.post(
174
+ "/v1/organizations/:organizationId/model-providers/:providerKind/custom-models",
175
+ async (c) => {
176
+ c.header("cache-control", "private, no-store");
177
+ requireSameOriginBrowserMutation(c, deps);
178
+ const organizationId = parseOrganizationId(c.req.param("organizationId"));
179
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
180
+ const payload = await jsonBody(
181
+ c,
182
+ CreateOrganizationProviderCustomModelRequest,
183
+ "invalid organization custom model",
184
+ );
185
+ try {
186
+ const model = await createOrganizationModelProviderCustomModel(deps.db, {
187
+ organizationId,
188
+ actorSubjectId: human.subjectId,
189
+ providerKind: providerKind(c.req.param("providerKind")),
190
+ operationId: payload.operationId,
191
+ upstreamModelId: payload.upstreamModelId,
192
+ ...(payload.label === undefined ? {} : { label: payload.label }),
193
+ });
194
+ return c.json(modelJson(model), 201);
195
+ } catch (error) {
196
+ conflict(error);
197
+ }
198
+ },
199
+ );
200
+
201
+ app.delete(
202
+ "/v1/organizations/:organizationId/model-providers/:providerKind/custom-models/:customModelId",
203
+ async (c) => {
204
+ c.header("cache-control", "private, no-store");
205
+ requireSameOriginBrowserMutation(c, deps);
206
+ const organizationId = parseOrganizationId(c.req.param("organizationId"));
207
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
208
+ const payload = await jsonBody(
209
+ c,
210
+ DeleteOrganizationProviderCustomModelRequest,
211
+ "invalid organization custom model deletion",
212
+ );
213
+ const customModelId = z.string().uuid().safeParse(c.req.param("customModelId"));
214
+ if (!customModelId.success) {
215
+ throw new HTTPException(422, { message: "invalid organization custom model id" });
216
+ }
217
+ try {
218
+ const model = await retireOrganizationModelProviderCustomModel(deps.db, {
219
+ organizationId,
220
+ actorSubjectId: human.subjectId,
221
+ providerKind: providerKind(c.req.param("providerKind")),
222
+ customModelId: customModelId.data,
223
+ ...payload,
224
+ });
225
+ return c.json(modelJson(model));
226
+ } catch (error) {
227
+ conflict(error);
228
+ }
229
+ },
230
+ );
231
+ }
@@ -277,6 +277,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
277
277
  sourcePath: inline.sourcePath,
278
278
  name: inline.name,
279
279
  description: inline.description,
280
+ activationMode: inline.activationMode,
280
281
  contentSha256: inline.contentSha256,
281
282
  totalBytes: inline.totalBytes,
282
283
  files: inline.files,
@@ -3,10 +3,15 @@ import {
3
3
  ListPersonalGitHubRepositoriesResponse,
4
4
  PersonalGitHubConnectionStatusResponse,
5
5
  PersonalGitHubOAuthStartRequest,
6
+ PersonalGitHubRepositoryId,
6
7
  PersonalGitHubRepositorySelectionState,
7
8
  ReplacePersonalGitHubRepositorySelectionsRequest,
8
9
  VerifyPersonalGitHubRepositorySelectionsRequest,
9
10
  } from "@opengeni/contracts/personal-github";
11
+ import {
12
+ ListGitHubRepositoryBranchesQuery,
13
+ GitHubRepositoryBranchesResponse,
14
+ } from "@opengeni/contracts/github-repository-contracts";
10
15
  import {
11
16
  requireAccessGrant,
12
17
  requireAccessGrantAuthorization,
@@ -31,6 +36,7 @@ import {
31
36
  startPersonalGitHubOAuth,
32
37
  } from "../integrations/personal-github";
33
38
  import {
39
+ listLivePersonalGitHubRepositoryBranches,
34
40
  listLivePersonalGitHubRepositories,
35
41
  personalGitHubRepositoryProviderHttpError,
36
42
  PersonalGitHubRepositoryProviderError,
@@ -187,6 +193,39 @@ export function registerPersonalGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
187
193
  },
188
194
  );
189
195
 
196
+ app.get(
197
+ "/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories/:repositoryId/branches",
198
+ async (c) => {
199
+ const workspaceId = c.req.param("workspaceId");
200
+ const connectionId = c.req.param("connectionId");
201
+ const access = await requireAccessGrantAuthorization(
202
+ c,
203
+ deps,
204
+ workspaceId,
205
+ "connections:read",
206
+ );
207
+ assertPersonalConnectionOwnerPrincipal(access, "My GitHub repositories");
208
+ const query = ListGitHubRepositoryBranchesQuery.parse(c.req.query());
209
+ const repositoryId = PersonalGitHubRepositoryId.parse(c.req.param("repositoryId"));
210
+ try {
211
+ return c.json(
212
+ GitHubRepositoryBranchesResponse.parse(
213
+ await listLivePersonalGitHubRepositoryBranches(deps, {
214
+ accountId: access.grant.accountId,
215
+ workspaceId,
216
+ subjectId: access.grant.subjectId,
217
+ connectionId,
218
+ repositoryId,
219
+ query,
220
+ }),
221
+ ),
222
+ );
223
+ } catch (error) {
224
+ throw personalGitHubRepositoryRouteError(error);
225
+ }
226
+ },
227
+ );
228
+
190
229
  app.post(
191
230
  "/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories/verify",
192
231
  async (c) => {
@@ -13,12 +13,15 @@ import {
13
13
  canonicalConfiguredModel,
14
14
  defaultPrReviewProviderBaseUrl,
15
15
  getCapabilityPack,
16
+ workspaceCustomModelReference,
17
+ lockActiveCustomModelForAdmission,
16
18
  prReviewWebhookAuthKind,
17
19
  normalizePrReviewProviderBaseUrl,
18
20
  prReviewPackConnectorId,
19
21
  PR_REVIEW_AUTOMATION_TEMPLATE_ID,
20
22
  requireAccessGrant,
21
23
  requirePermission,
24
+ resolveWorkspaceCatalogSettings,
22
25
  type ApiRouteDeps,
23
26
  } from "@opengeni/core";
24
27
  import {
@@ -385,10 +388,24 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
385
388
  throw error;
386
389
  }
387
390
  }
391
+ const catalogSettings = (
392
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
393
+ accountId: grant.accountId,
394
+ workspaceId,
395
+ })
396
+ ).settings;
388
397
  const model = payload.model
389
- ? (canonicalConfiguredModel(settings, payload.model) ?? null)
398
+ ? (canonicalConfiguredModel(catalogSettings, payload.model) ?? null)
390
399
  : null;
391
- if (model) await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
400
+ if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
401
+ const beforeCreateCommit = model
402
+ ? prReviewCustomModelCommitGuard({
403
+ settings: catalogSettings,
404
+ accountId: grant.accountId,
405
+ workspaceId,
406
+ modelId: model,
407
+ })
408
+ : undefined;
392
409
  const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
393
410
  (candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
394
411
  );
@@ -417,6 +434,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
417
434
  eventTypes: template.eventTypes,
418
435
  configuration: template.configuration,
419
436
  sessionTemplate: template.sessionTemplate,
437
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
420
438
  }),
421
439
  "This repository is already bound to the selected PR Review registration",
422
440
  );
@@ -441,13 +459,40 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
441
459
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
442
460
  await requirePrReviewPackActive(db, workspaceId);
443
461
  const payload = UpdatePrReviewRepositoryBindingRequest.parse(await c.req.json());
462
+ const catalogSettings = (
463
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
464
+ accountId: grant.accountId,
465
+ workspaceId,
466
+ })
467
+ ).settings;
444
468
  const model =
445
469
  payload.model === undefined
446
470
  ? undefined
447
471
  : payload.model === null
448
472
  ? null
449
- : (canonicalConfiguredModel(settings, payload.model) ?? null);
450
- if (model) await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
473
+ : (canonicalConfiguredModel(catalogSettings, payload.model) ?? null);
474
+ if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
475
+ const beforeUpdateCommit = async (
476
+ tx: ApiRouteDeps["db"],
477
+ context: {
478
+ currentModel: string | null;
479
+ currentStatus: "active" | "disabled";
480
+ nextModel: string | null;
481
+ nextStatus: "active" | "disabled";
482
+ },
483
+ ): Promise<void> => {
484
+ const materialExecutionChange =
485
+ payload.model !== undefined ||
486
+ payload.additionalInstructions !== undefined ||
487
+ (context.currentStatus === "disabled" && context.nextStatus === "active");
488
+ if (!materialExecutionChange || !context.nextModel) return;
489
+ await prReviewCustomModelCommitGuard({
490
+ settings: catalogSettings,
491
+ accountId: grant.accountId,
492
+ workspaceId,
493
+ modelId: context.nextModel,
494
+ })?.(tx);
495
+ };
451
496
  const binding = await updatePrReviewRepositoryBinding(db, {
452
497
  accountId: grant.accountId,
453
498
  workspaceId,
@@ -458,6 +503,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
458
503
  ? { additionalInstructions: payload.additionalInstructions }
459
504
  : {}),
460
505
  ...(payload.status !== undefined ? { status: payload.status } : {}),
506
+ beforeUpdateCommit,
461
507
  });
462
508
  if (!binding)
463
509
  throw new HTTPException(404, {
@@ -501,6 +547,28 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
501
547
  });
502
548
  }
503
549
 
550
+ function prReviewCustomModelCommitGuard(input: {
551
+ settings: ApiRouteDeps["settings"];
552
+ accountId: string;
553
+ workspaceId: string;
554
+ modelId: string;
555
+ }): ((tx: ApiRouteDeps["db"]) => Promise<void>) | undefined {
556
+ const reference = workspaceCustomModelReference(input.settings, input.modelId);
557
+ if (!reference) return undefined;
558
+ return async (tx): Promise<void> => {
559
+ const active = await lockActiveCustomModelForAdmission(tx, {
560
+ accountId: input.accountId,
561
+ workspaceId: input.workspaceId,
562
+ reference,
563
+ });
564
+ if (!active) {
565
+ throw new HTTPException(422, {
566
+ message: `model is not available: ${input.modelId}`,
567
+ });
568
+ }
569
+ };
570
+ }
571
+
504
572
  function assertPrReviewSandboxBackend(deps: ApiRouteDeps): void {
505
573
  if (deps.settings.sandboxBackend === "selfhosted") {
506
574
  throw new HTTPException(409, {
@@ -6,7 +6,7 @@ import {
6
6
  import { listScheduledTaskRuns, listScheduledTasks } from "@opengeni/db";
7
7
  import type { Hono } from "hono";
8
8
  import { HTTPException } from "hono/http-exception";
9
- import { requireAccessGrant } from "@opengeni/core";
9
+ import { requireAccessGrant, resolveWorkspaceCatalogSettings } from "@opengeni/core";
10
10
  import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
11
11
  import type { ApiRouteDeps } from "@opengeni/core";
12
12
  import {
@@ -30,7 +30,7 @@ import { boundedLimit } from "../http/common";
30
30
  import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
31
31
 
32
32
  export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void {
33
- const { settings, db, workflowClient, objectStorage } = deps;
33
+ const { db, workflowClient, objectStorage } = deps;
34
34
 
35
35
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
36
36
  const workspaceId = c.req.param("workspaceId");
@@ -43,6 +43,12 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
43
43
  });
44
44
  }
45
45
  const payload = parsedPayload.data;
46
+ const catalogSettings = (
47
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
48
+ accountId: grant.accountId,
49
+ workspaceId,
50
+ })
51
+ ).settings;
46
52
  await requireLimit(deps, {
47
53
  accountId: grant.accountId,
48
54
  workspaceId,
@@ -50,7 +56,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
50
56
  quantity: 1,
51
57
  });
52
58
  const task = await createValidatedScheduledTask({
53
- settings,
59
+ settings: catalogSettings,
54
60
  db,
55
61
  objectStorage,
56
62
  grant,
@@ -91,8 +97,14 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
91
97
  });
92
98
  }
93
99
  const payload = parsedPayload.data;
100
+ const catalogSettings = (
101
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
102
+ accountId: grant.accountId,
103
+ workspaceId,
104
+ })
105
+ ).settings;
94
106
  const update = await validatedScheduledTaskUpdate({
95
- settings,
107
+ settings: catalogSettings,
96
108
  db,
97
109
  objectStorage,
98
110
  grant,
@@ -124,8 +136,14 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
124
136
  const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
125
137
  const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
126
138
  const previous = await captureScheduledTaskRestoreState(db, existing);
139
+ const catalogSettings = (
140
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
141
+ accountId: grant.accountId,
142
+ workspaceId,
143
+ })
144
+ ).settings;
127
145
  const update = await validatedScheduledTaskUpdate({
128
- settings,
146
+ settings: catalogSettings,
129
147
  db,
130
148
  objectStorage,
131
149
  grant,
@@ -146,6 +164,12 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
146
164
  // recognised as codex-billed and skip the credit/cost gates at the edge.
147
165
  const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
148
166
  if (task.action.kind === "agent_turn") {
167
+ const catalogSettings = (
168
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
169
+ accountId: grant.accountId,
170
+ workspaceId,
171
+ })
172
+ ).settings;
149
173
  await validateScheduledTaskTarget({
150
174
  db,
151
175
  sessionAuthorization: deps.sessionAuthorization,
@@ -159,20 +183,23 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
159
183
  missingTargetStatus: 404,
160
184
  });
161
185
  await validateScheduledTaskMachineTarget({
162
- settings,
186
+ settings: catalogSettings,
163
187
  db,
164
188
  grant,
165
189
  runMode: task.runMode,
166
190
  agentConfig: task.agentConfig,
167
191
  requireOnline: true,
168
192
  });
169
- await requireLimit(deps, {
170
- accountId: grant.accountId,
171
- workspaceId,
172
- action: "agent_run:create",
173
- quantity: 1,
174
- model: task.agentConfig.model ?? deps.settings.openaiModel,
175
- });
193
+ await requireLimit(
194
+ { ...deps, settings: catalogSettings },
195
+ {
196
+ accountId: grant.accountId,
197
+ workspaceId,
198
+ action: "agent_run:create",
199
+ quantity: 1,
200
+ model: task.agentConfig.model ?? catalogSettings.openaiModel,
201
+ },
202
+ );
176
203
  }
177
204
  // Body is optional (a bare POST is still a valid trigger); only a present,
178
205
  // non-empty body must parse against the contract.