@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.
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/{chunk-XTLI3CBH.js → chunk-HHKQMVY7.js} +3009 -1699
- package/dist/chunk-HHKQMVY7.js.map +1 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +8 -1
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/package.json +18 -18
- package/src/app.ts +55 -9
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/github-access.ts +117 -1
- package/src/http/sse.ts +15 -7
- package/src/index.ts +13 -1
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +11 -2
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +136 -32
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +50 -3
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +88 -52
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +405 -62
- package/src/workspace-deletion.ts +64 -0
- 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
|
+
}
|
package/src/routes/packs.ts
CHANGED
|
@@ -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) => {
|
package/src/routes/pr-review.ts
CHANGED
|
@@ -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(
|
|
398
|
+
? (canonicalConfiguredModel(catalogSettings, payload.model) ?? null)
|
|
390
399
|
: null;
|
|
391
|
-
if (model) await assertWorkspaceModelPolicyAllows(db,
|
|
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(
|
|
450
|
-
if (model) await assertWorkspaceModelPolicyAllows(db,
|
|
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 {
|
|
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(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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.
|