@opengeni/api-router 0.5.2 → 0.5.4
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.js +1 -1
- package/dist/{chunk-YY6OAEL6.js → chunk-DO2G3JSB.js} +5333 -2205
- package/dist/chunk-DO2G3JSB.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +297 -54
- package/dist/index.js.map +1 -1
- package/package.json +21 -21
- package/src/app.ts +415 -147
- package/src/auth/managed-auth.ts +32 -16
- package/src/http/auth.ts +8 -1
- package/src/http/common.ts +6 -2
- package/src/http/sse.ts +27 -6
- package/src/index.ts +196 -74
- package/src/integrations/oauth-client.ts +592 -131
- package/src/integrations/provider-domain.ts +4 -1
- package/src/mcp/documents.ts +173 -94
- package/src/mcp/server.ts +1517 -692
- package/src/mcp/session-view.ts +8 -2
- package/src/mcp/toolspace.ts +175 -84
- package/src/observability.ts +7 -1
- package/src/routes/api-keys.ts +39 -23
- package/src/routes/billing.ts +180 -65
- package/src/routes/capabilities.ts +17 -8
- package/src/routes/catalog-assets.ts +5 -2
- package/src/routes/codex.ts +244 -63
- package/src/routes/connections.ts +72 -34
- package/src/routes/documents.ts +242 -92
- package/src/routes/enrollments.ts +100 -70
- package/src/routes/environments.ts +205 -136
- package/src/routes/files.ts +164 -39
- package/src/routes/github.ts +123 -50
- package/src/routes/install.ts +9 -2
- package/src/routes/machines.ts +9 -8
- package/src/routes/packs.ts +141 -89
- package/src/routes/rigs.ts +189 -0
- package/src/routes/scheduled-tasks.ts +51 -9
- package/src/routes/sessions.ts +839 -329
- package/src/routes/social.ts +50 -38
- package/src/routes/workspace-capture.ts +238 -0
- package/src/routes/workspaces.ts +159 -13
- package/src/sandbox/access.ts +11 -3
- package/src/sandbox/auth-callout.ts +5 -1
- package/src/sandbox/channel-a.ts +104 -27
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +68 -59
- package/src/sandbox/metrics-ingestion.ts +238 -17
- package/src/sandbox/viewer.ts +172 -46
- package/dist/chunk-YY6OAEL6.js.map +0 -1
package/src/routes/packs.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
23
23
|
import { requireAccessGrant } from "@opengeni/core";
|
|
24
24
|
import { requireLimit } from "@opengeni/core";
|
|
25
25
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
26
|
-
import {
|
|
26
|
+
import { validateVariableSetAttachment } from "@opengeni/core";
|
|
27
27
|
import {
|
|
28
28
|
assertPackSandboxImageCompatible,
|
|
29
29
|
buildMarketingDailyAnalysisAgentConfig,
|
|
@@ -32,10 +32,7 @@ import {
|
|
|
32
32
|
MARKETING_SOCIAL_PACK_ID,
|
|
33
33
|
resolveCapabilityPack,
|
|
34
34
|
} from "@opengeni/core";
|
|
35
|
-
import {
|
|
36
|
-
createValidatedScheduledTask,
|
|
37
|
-
syncCreatedScheduledTask,
|
|
38
|
-
} from "@opengeni/core";
|
|
35
|
+
import { createValidatedScheduledTask, syncCreatedScheduledTask } from "@opengeni/core";
|
|
39
36
|
|
|
40
37
|
export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
41
38
|
const { settings, db, objectStorage, workflowClient } = deps;
|
|
@@ -56,7 +53,9 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
56
53
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
57
54
|
const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
|
|
58
55
|
if (isBuiltInCapabilityPack(manifest.id)) {
|
|
59
|
-
throw new HTTPException(409, {
|
|
56
|
+
throw new HTTPException(409, {
|
|
57
|
+
message: `pack id ${manifest.id} is a built-in pack and cannot be replaced`,
|
|
58
|
+
});
|
|
60
59
|
}
|
|
61
60
|
const { pack, created } = await registerWorkspacePack(db, {
|
|
62
61
|
accountId: grant.accountId,
|
|
@@ -73,7 +72,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
73
72
|
if (isBuiltInCapabilityPack(packId)) {
|
|
74
73
|
throw new HTTPException(409, { message: "built-in packs cannot be unregistered" });
|
|
75
74
|
}
|
|
76
|
-
if (!await getWorkspacePack(db, workspaceId, packId)) {
|
|
75
|
+
if (!(await getWorkspacePack(db, workspaceId, packId))) {
|
|
77
76
|
throw new HTTPException(404, { message: "pack not found" });
|
|
78
77
|
}
|
|
79
78
|
// Disable installations before deleting the registration so a crash in
|
|
@@ -84,7 +83,11 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
84
83
|
if (installation && installation.status === "active") {
|
|
85
84
|
await updatePackInstallationStatus(db, workspaceId, packId, "disabled");
|
|
86
85
|
}
|
|
87
|
-
const capabilityInstallation = await getCapabilityInstallation(
|
|
86
|
+
const capabilityInstallation = await getCapabilityInstallation(
|
|
87
|
+
db,
|
|
88
|
+
workspaceId,
|
|
89
|
+
`pack:${packId}`,
|
|
90
|
+
);
|
|
88
91
|
if (capabilityInstallation && capabilityInstallation.status === "active") {
|
|
89
92
|
await disableCapabilityInstallation(db, workspaceId, `pack:${packId}`);
|
|
90
93
|
}
|
|
@@ -115,22 +118,41 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
115
118
|
await assertPackSandboxImageCompatible(db, workspaceId, pack);
|
|
116
119
|
const existing = await getPackInstallation(db, workspaceId, pack.id);
|
|
117
120
|
const payload = EnablePackRequest.parse(await c.req.json());
|
|
118
|
-
// Re-enabling without
|
|
121
|
+
// Re-enabling without variableSetId keeps the stored attachment instead of
|
|
119
122
|
// silently dropping it; the inherited attachment is re-validated below in
|
|
120
|
-
// case the
|
|
121
|
-
// inherited attachment was authorized with
|
|
123
|
+
// case the variableSet was deleted or its variables changed since. The
|
|
124
|
+
// inherited attachment was authorized with variableSets:use when it was
|
|
122
125
|
// first attached, so only a fresh attachment re-checks that permission.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
// Back-compat: installations enabled before the Variable Set rename stored
|
|
127
|
+
// the attachment under `metadata.environmentId`; read it as a fallback so a
|
|
128
|
+
// re-enable without an explicit id still inherits the existing attachment.
|
|
129
|
+
const storedVariableSetId =
|
|
130
|
+
typeof existing?.metadata.variableSetId === "string"
|
|
131
|
+
? existing.metadata.variableSetId
|
|
132
|
+
: typeof existing?.metadata.environmentId === "string"
|
|
133
|
+
? existing.metadata.environmentId
|
|
134
|
+
: undefined;
|
|
135
|
+
const variableSetId = payload.variableSetId ?? storedVariableSetId;
|
|
136
|
+
if (pack.variableSet?.required && !variableSetId) {
|
|
137
|
+
throw new HTTPException(422, {
|
|
138
|
+
message: "this pack requires a variableSet attachment; pass variableSetId",
|
|
139
|
+
});
|
|
127
140
|
}
|
|
128
|
-
if (
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
if (variableSetId) {
|
|
142
|
+
const variableSet = await validateVariableSetAttachment(
|
|
143
|
+
{ settings, db },
|
|
144
|
+
grant,
|
|
145
|
+
workspaceId,
|
|
146
|
+
variableSetId,
|
|
147
|
+
{ preauthorized: !payload.variableSetId },
|
|
148
|
+
);
|
|
149
|
+
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
150
|
+
(name) => !variableSet.variables.some((variable) => variable.name === name),
|
|
151
|
+
);
|
|
132
152
|
if (missing.length > 0) {
|
|
133
|
-
throw new HTTPException(422, {
|
|
153
|
+
throw new HTTPException(422, {
|
|
154
|
+
message: `variableSet is missing required variable(s): ${missing.join(", ")}`,
|
|
155
|
+
});
|
|
134
156
|
}
|
|
135
157
|
}
|
|
136
158
|
const installation = await enablePackInstallation(db, {
|
|
@@ -140,69 +162,84 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
140
162
|
metadata: {
|
|
141
163
|
...payload.metadata,
|
|
142
164
|
packVersion: pack.version,
|
|
143
|
-
...(
|
|
165
|
+
...(variableSetId ? { variableSetId } : {}),
|
|
144
166
|
},
|
|
145
167
|
});
|
|
146
168
|
return c.json(installation, existing ? 200 : 201);
|
|
147
169
|
});
|
|
148
170
|
|
|
149
|
-
app.post(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
171
|
+
app.post(
|
|
172
|
+
"/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks",
|
|
173
|
+
async (c) => {
|
|
174
|
+
const workspaceId = c.req.param("workspaceId");
|
|
175
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
176
|
+
const pack = await requirePack(db, workspaceId, MARKETING_SOCIAL_PACK_ID);
|
|
177
|
+
const installation = await getPackInstallation(db, workspaceId, pack.id);
|
|
178
|
+
if (installation?.status !== "active") {
|
|
179
|
+
throw new HTTPException(409, {
|
|
180
|
+
message: "enable the marketing social pack before creating its scheduled tasks",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const payload = MarketingDailyAnalysisTaskRequest.parse(await c.req.json());
|
|
184
|
+
await requireLimit(deps, {
|
|
185
|
+
accountId: grant.accountId,
|
|
186
|
+
workspaceId,
|
|
187
|
+
action: "schedule:create",
|
|
188
|
+
quantity: 1,
|
|
189
|
+
});
|
|
190
|
+
const connections = await resolveSocialConnections(db, workspaceId, payload.connectionIds);
|
|
191
|
+
if (connections.length === 0) {
|
|
192
|
+
throw new HTTPException(422, {
|
|
193
|
+
message: "at least one connected social account is required",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
await validateDocumentBaseIds(db, workspaceId, payload.documentBaseIds);
|
|
197
|
+
const agentConfig = buildMarketingDailyAnalysisAgentConfig({
|
|
198
|
+
connections,
|
|
199
|
+
documentBaseIds: payload.documentBaseIds,
|
|
200
|
+
...(payload.promptInstructions ? { promptInstructions: payload.promptInstructions } : {}),
|
|
201
|
+
});
|
|
202
|
+
// Installation-inherited variableSet attachment: it was authorized with
|
|
203
|
+
// variableSets:use at pack-enable time, so the scheduled_tasks:manage
|
|
204
|
+
// caller here is not re-checked for that permission.
|
|
205
|
+
const installationVariableSetId =
|
|
206
|
+
typeof installation.metadata.variableSetId === "string"
|
|
207
|
+
? installation.metadata.variableSetId
|
|
208
|
+
: typeof installation.metadata.environmentId === "string"
|
|
209
|
+
? installation.metadata.environmentId
|
|
210
|
+
: undefined;
|
|
211
|
+
const task = await createValidatedScheduledTask({
|
|
212
|
+
settings,
|
|
213
|
+
db,
|
|
214
|
+
objectStorage,
|
|
215
|
+
grant,
|
|
216
|
+
variableSetPreauthorized: true,
|
|
217
|
+
payload: {
|
|
218
|
+
name: payload.name ?? "Daily social media analysis",
|
|
219
|
+
status: payload.status,
|
|
220
|
+
schedule: {
|
|
221
|
+
type: "calendar",
|
|
222
|
+
timeZone: payload.timeZone,
|
|
223
|
+
hour: payload.hour,
|
|
224
|
+
minute: payload.minute,
|
|
225
|
+
},
|
|
226
|
+
runMode: payload.runMode,
|
|
227
|
+
overlapPolicy: payload.overlapPolicy,
|
|
228
|
+
agentConfig,
|
|
229
|
+
...(installationVariableSetId ? { variableSetId: installationVariableSetId } : {}),
|
|
230
|
+
metadata: {
|
|
231
|
+
packId: pack.id,
|
|
232
|
+
packVersion: pack.version,
|
|
233
|
+
packTemplateId: "daily-social-analysis",
|
|
234
|
+
socialConnectionIds: connections.map((connection) => connection.id),
|
|
235
|
+
documentBaseIds: payload.documentBaseIds,
|
|
236
|
+
},
|
|
200
237
|
},
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
238
|
+
});
|
|
239
|
+
await syncCreatedScheduledTask({ db, workflowClient, task });
|
|
240
|
+
return c.json(task, 201);
|
|
241
|
+
},
|
|
242
|
+
);
|
|
206
243
|
}
|
|
207
244
|
|
|
208
245
|
async function requirePack(db: ApiRouteDeps["db"], workspaceId: string, packId: string) {
|
|
@@ -213,25 +250,40 @@ async function requirePack(db: ApiRouteDeps["db"], workspaceId: string, packId:
|
|
|
213
250
|
return pack;
|
|
214
251
|
}
|
|
215
252
|
|
|
216
|
-
async function resolveSocialConnections(
|
|
253
|
+
async function resolveSocialConnections(
|
|
254
|
+
db: ApiRouteDeps["db"],
|
|
255
|
+
workspaceId: string,
|
|
256
|
+
connectionIds: string[],
|
|
257
|
+
): Promise<SocialConnection[]> {
|
|
217
258
|
const ids = [...new Set(connectionIds)];
|
|
218
|
-
const connections =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
259
|
+
const connections =
|
|
260
|
+
ids.length > 0
|
|
261
|
+
? await Promise.all(
|
|
262
|
+
ids.map(async (id) => {
|
|
263
|
+
const connection = await getSocialConnection(db, workspaceId, id);
|
|
264
|
+
if (!connection) {
|
|
265
|
+
throw new HTTPException(422, { message: `unknown social connection: ${id}` });
|
|
266
|
+
}
|
|
267
|
+
return connection;
|
|
268
|
+
}),
|
|
269
|
+
)
|
|
270
|
+
: (await listSocialConnections(db, workspaceId, 500)).filter(
|
|
271
|
+
(connection) => connection.status === "connected",
|
|
272
|
+
);
|
|
227
273
|
const inactive = connections.find((connection) => connection.status !== "connected");
|
|
228
274
|
if (inactive) {
|
|
229
|
-
throw new HTTPException(422, {
|
|
275
|
+
throw new HTTPException(422, {
|
|
276
|
+
message: `social connection ${inactive.id} is ${inactive.status}`,
|
|
277
|
+
});
|
|
230
278
|
}
|
|
231
279
|
return connections;
|
|
232
280
|
}
|
|
233
281
|
|
|
234
|
-
async function validateDocumentBaseIds(
|
|
282
|
+
async function validateDocumentBaseIds(
|
|
283
|
+
db: ApiRouteDeps["db"],
|
|
284
|
+
workspaceId: string,
|
|
285
|
+
documentBaseIds: string[],
|
|
286
|
+
): Promise<void> {
|
|
235
287
|
for (const baseId of [...new Set(documentBaseIds)]) {
|
|
236
288
|
const base = await getDocumentBase(db, workspaceId, baseId);
|
|
237
289
|
if (!base) {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateRigRequest,
|
|
3
|
+
ProposeRigChangeRequest,
|
|
4
|
+
RigDefinitionEditPayload,
|
|
5
|
+
UpdateRigRequest,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import {
|
|
8
|
+
beginRigChangeVerificationAttempt,
|
|
9
|
+
listRigs,
|
|
10
|
+
RigChangeAlreadyVerifyingError,
|
|
11
|
+
RigChangeTransitionError,
|
|
12
|
+
} from "@opengeni/db";
|
|
13
|
+
import type { Hono } from "hono";
|
|
14
|
+
import { HTTPException } from "hono/http-exception";
|
|
15
|
+
import { requireAccessGrant } from "@opengeni/core";
|
|
16
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
17
|
+
import {
|
|
18
|
+
activateRigVersionForApi,
|
|
19
|
+
createRigForApi,
|
|
20
|
+
createRigVersionForApi,
|
|
21
|
+
deleteRigForApi,
|
|
22
|
+
listRigChangesForApi,
|
|
23
|
+
listRigVersionsForApi,
|
|
24
|
+
promoteVerifiedDefinitionEditChangeForApi,
|
|
25
|
+
proposeRigChangeForApi,
|
|
26
|
+
requireRigChangeForApi,
|
|
27
|
+
requireRigForApi,
|
|
28
|
+
updateRigForApi,
|
|
29
|
+
} from "@opengeni/core";
|
|
30
|
+
import { boundedLimit } from "../http/common";
|
|
31
|
+
|
|
32
|
+
export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
33
|
+
const { db, workflowClient } = deps;
|
|
34
|
+
|
|
35
|
+
async function startChangeVerification(workspaceId: string, changeId: string): Promise<unknown> {
|
|
36
|
+
const startedAt = new Date().toISOString();
|
|
37
|
+
let change;
|
|
38
|
+
try {
|
|
39
|
+
change = await beginRigChangeVerificationAttempt(db, workspaceId, changeId, { startedAt });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (
|
|
42
|
+
error instanceof RigChangeAlreadyVerifyingError ||
|
|
43
|
+
error instanceof RigChangeTransitionError
|
|
44
|
+
) {
|
|
45
|
+
throw new HTTPException(409, { message: error.message });
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const attempt =
|
|
50
|
+
typeof change.verification?.attempt === "number" ? change.verification.attempt : Date.now();
|
|
51
|
+
await workflowClient.startRigVerification({
|
|
52
|
+
workspaceId,
|
|
53
|
+
changeId,
|
|
54
|
+
workflowId: `rig-verification-change-${changeId}-attempt-${attempt}`,
|
|
55
|
+
});
|
|
56
|
+
return change;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function startVersionVerification(workspaceId: string, versionId: string): Promise<void> {
|
|
60
|
+
await workflowClient.startRigVerification({
|
|
61
|
+
workspaceId,
|
|
62
|
+
versionId,
|
|
63
|
+
workflowId: `rig-verification-version-${versionId}-${crypto.randomUUID()}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
app.get("/v1/workspaces/:workspaceId/rigs", async (c) => {
|
|
68
|
+
const workspaceId = c.req.param("workspaceId");
|
|
69
|
+
await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
70
|
+
return c.json(await listRigs(db, workspaceId));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post("/v1/workspaces/:workspaceId/rigs", async (c) => {
|
|
74
|
+
const workspaceId = c.req.param("workspaceId");
|
|
75
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
|
|
76
|
+
const payload = CreateRigRequest.parse(await c.req.json());
|
|
77
|
+
const rig = await createRigForApi({ db }, grant, payload);
|
|
78
|
+
return c.json(rig, 201);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
app.get("/v1/workspaces/:workspaceId/rigs/:rigId", async (c) => {
|
|
82
|
+
const workspaceId = c.req.param("workspaceId");
|
|
83
|
+
await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
84
|
+
return c.json(await requireRigForApi(db, workspaceId, c.req.param("rigId")));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
app.patch("/v1/workspaces/:workspaceId/rigs/:rigId", async (c) => {
|
|
88
|
+
const workspaceId = c.req.param("workspaceId");
|
|
89
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
|
|
90
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
91
|
+
const payload = UpdateRigRequest.parse(await c.req.json());
|
|
92
|
+
return c.json(await updateRigForApi({ db }, grant, rig, payload));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.delete("/v1/workspaces/:workspaceId/rigs/:rigId", async (c) => {
|
|
96
|
+
const workspaceId = c.req.param("workspaceId");
|
|
97
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
|
|
98
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
99
|
+
await deleteRigForApi({ db }, grant, rig);
|
|
100
|
+
return c.json({ ok: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
app.get("/v1/workspaces/:workspaceId/rigs/:rigId/versions", async (c) => {
|
|
104
|
+
const workspaceId = c.req.param("workspaceId");
|
|
105
|
+
await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
106
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
107
|
+
return c.json(await listRigVersionsForApi({ db }, workspaceId, rig.id));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/versions", async (c) => {
|
|
111
|
+
const workspaceId = c.req.param("workspaceId");
|
|
112
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
|
|
113
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
114
|
+
const payload = RigDefinitionEditPayload.parse(await c.req.json());
|
|
115
|
+
return c.json(await createRigVersionForApi({ db }, grant, rig, payload), 201);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Rollback / promote-activate: flips which existing version is active.
|
|
119
|
+
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/versions/:versionId/activate", async (c) => {
|
|
120
|
+
const workspaceId = c.req.param("workspaceId");
|
|
121
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
|
|
122
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
123
|
+
const version = await activateRigVersionForApi({ db }, grant, rig, c.req.param("versionId"));
|
|
124
|
+
return c.json(version);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
app.get("/v1/workspaces/:workspaceId/rigs/:rigId/changes", async (c) => {
|
|
128
|
+
const workspaceId = c.req.param("workspaceId");
|
|
129
|
+
await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
130
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
131
|
+
return c.json(
|
|
132
|
+
await listRigChangesForApi({ db }, workspaceId, rig.id, boundedLimit(c.req.query("limit"))),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Propose a change (rigs:use — the additive, agent-trusted path). The change
|
|
137
|
+
// is recorded `proposed`; verification + auto-merge (setup_append) and the
|
|
138
|
+
// promote gate (definition_edit) land in M4.
|
|
139
|
+
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes", async (c) => {
|
|
140
|
+
const workspaceId = c.req.param("workspaceId");
|
|
141
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
142
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
143
|
+
const request = ProposeRigChangeRequest.parse(await c.req.json());
|
|
144
|
+
const change = await proposeRigChangeForApi({ db }, grant, rig, request);
|
|
145
|
+
const verifying = await startChangeVerification(workspaceId, change.id);
|
|
146
|
+
return c.json(verifying, 201);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.get("/v1/workspaces/:workspaceId/rigs/:rigId/changes/:changeId", async (c) => {
|
|
150
|
+
const workspaceId = c.req.param("workspaceId");
|
|
151
|
+
await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
152
|
+
return c.json(
|
|
153
|
+
await requireRigChangeForApi(db, workspaceId, c.req.param("rigId"), c.req.param("changeId")),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes/:changeId/verify", async (c) => {
|
|
158
|
+
const workspaceId = c.req.param("workspaceId");
|
|
159
|
+
await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
160
|
+
const change = await requireRigChangeForApi(
|
|
161
|
+
db,
|
|
162
|
+
workspaceId,
|
|
163
|
+
c.req.param("rigId"),
|
|
164
|
+
c.req.param("changeId"),
|
|
165
|
+
);
|
|
166
|
+
const verifying = await startChangeVerification(workspaceId, change.id);
|
|
167
|
+
return c.json(verifying, 202);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes/:changeId/promote", async (c) => {
|
|
171
|
+
const workspaceId = c.req.param("workspaceId");
|
|
172
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
|
|
173
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
174
|
+
const change = await requireRigChangeForApi(db, workspaceId, rig.id, c.req.param("changeId"));
|
|
175
|
+
const promoted = await promoteVerifiedDefinitionEditChangeForApi({ db }, grant, rig, change);
|
|
176
|
+
return c.json(promoted.version, 201);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/verify", async (c) => {
|
|
180
|
+
const workspaceId = c.req.param("workspaceId");
|
|
181
|
+
await requireAccessGrant(c, deps, workspaceId, "rigs:use");
|
|
182
|
+
const rig = await requireRigForApi(db, workspaceId, c.req.param("rigId"));
|
|
183
|
+
if (!rig.activeVersion) {
|
|
184
|
+
return c.json({ error: "rig has no active version" }, 422);
|
|
185
|
+
}
|
|
186
|
+
await startVersionVerification(workspaceId, rig.activeVersion.id);
|
|
187
|
+
return c.json({ ok: true, versionId: rig.activeVersion.id }, 202);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CreateScheduledTaskRequest,
|
|
3
|
+
TriggerScheduledTaskRequest,
|
|
4
|
+
UpdateScheduledTaskRequest,
|
|
5
|
+
} from "@opengeni/contracts";
|
|
2
6
|
import {
|
|
3
7
|
deleteScheduledTask,
|
|
4
8
|
listScheduledTaskRuns,
|
|
@@ -30,8 +34,20 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
30
34
|
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
31
35
|
const rawPayload = await c.req.json();
|
|
32
36
|
const payload = CreateScheduledTaskRequest.parse(rawPayload);
|
|
33
|
-
await requireLimit(deps, {
|
|
34
|
-
|
|
37
|
+
await requireLimit(deps, {
|
|
38
|
+
accountId: grant.accountId,
|
|
39
|
+
workspaceId,
|
|
40
|
+
action: "schedule:create",
|
|
41
|
+
quantity: 1,
|
|
42
|
+
});
|
|
43
|
+
const task = await createValidatedScheduledTask({
|
|
44
|
+
settings,
|
|
45
|
+
db,
|
|
46
|
+
objectStorage,
|
|
47
|
+
grant,
|
|
48
|
+
payload,
|
|
49
|
+
toolsProvided: scheduledTaskToolsProvided(rawPayload),
|
|
50
|
+
});
|
|
35
51
|
await syncCreatedScheduledTask({ db, workflowClient, task });
|
|
36
52
|
return c.json(task, 201);
|
|
37
53
|
});
|
|
@@ -55,7 +71,15 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
55
71
|
const existing = await requireScheduledTaskForApi(db, workspaceId, taskId);
|
|
56
72
|
const rawPayload = await c.req.json();
|
|
57
73
|
const payload = UpdateScheduledTaskRequest.parse(rawPayload);
|
|
58
|
-
const update = await validatedScheduledTaskUpdate({
|
|
74
|
+
const update = await validatedScheduledTaskUpdate({
|
|
75
|
+
settings,
|
|
76
|
+
db,
|
|
77
|
+
objectStorage,
|
|
78
|
+
grant,
|
|
79
|
+
existing,
|
|
80
|
+
payload,
|
|
81
|
+
toolsProvided: scheduledTaskToolsProvided(rawPayload),
|
|
82
|
+
});
|
|
59
83
|
const task = await updateScheduledTask(db, workspaceId, taskId, update);
|
|
60
84
|
await syncUpdatedScheduledTask({ db, workflowClient, previous: existing, task });
|
|
61
85
|
return c.json(task);
|
|
@@ -85,15 +109,29 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
85
109
|
// Load the task before the gate so a codex-model scheduled task can be
|
|
86
110
|
// recognised as codex-billed and skip the credit/cost gates at the edge.
|
|
87
111
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
88
|
-
await requireLimit(deps, {
|
|
112
|
+
await requireLimit(deps, {
|
|
113
|
+
accountId: grant.accountId,
|
|
114
|
+
workspaceId,
|
|
115
|
+
action: "agent_run:create",
|
|
116
|
+
quantity: 1,
|
|
117
|
+
model: task.agentConfig.model ?? deps.settings.openaiModel,
|
|
118
|
+
});
|
|
89
119
|
// Body is optional (a bare POST is still a valid trigger); only a present,
|
|
90
120
|
// non-empty body must parse against the contract.
|
|
91
121
|
const body = await c.req.json().catch(() => ({}));
|
|
92
122
|
const { triggerId } = TriggerScheduledTaskRequest.parse(body ?? {});
|
|
93
123
|
const triggerToken = scheduledTaskTriggerToken(triggerId);
|
|
94
|
-
const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey(
|
|
124
|
+
const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey(
|
|
125
|
+
workspaceId,
|
|
126
|
+
task.id,
|
|
127
|
+
triggerToken,
|
|
128
|
+
);
|
|
95
129
|
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
96
|
-
await workflowClient.triggerScheduledTask({
|
|
130
|
+
await workflowClient.triggerScheduledTask({
|
|
131
|
+
task,
|
|
132
|
+
agentRunUsageIdempotencyKey,
|
|
133
|
+
triggerWorkflowId,
|
|
134
|
+
});
|
|
97
135
|
await recordWorkspaceUsage(deps, {
|
|
98
136
|
accountId: grant.accountId,
|
|
99
137
|
workspaceId,
|
|
@@ -112,7 +150,9 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
112
150
|
const workspaceId = c.req.param("workspaceId");
|
|
113
151
|
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
114
152
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
115
|
-
await workflowClient.deleteScheduledTaskSchedule({
|
|
153
|
+
await workflowClient.deleteScheduledTaskSchedule({
|
|
154
|
+
temporalScheduleId: task.temporalScheduleId,
|
|
155
|
+
});
|
|
116
156
|
await deleteScheduledTask(db, workspaceId, task.id);
|
|
117
157
|
return c.json({ ok: true });
|
|
118
158
|
});
|
|
@@ -121,6 +161,8 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
121
161
|
const workspaceId = c.req.param("workspaceId");
|
|
122
162
|
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
123
163
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
124
|
-
return c.json(
|
|
164
|
+
return c.json(
|
|
165
|
+
await listScheduledTaskRuns(db, workspaceId, task.id, boundedLimit(c.req.query("limit"))),
|
|
166
|
+
);
|
|
125
167
|
});
|
|
126
168
|
}
|