@opengeni/api-router 0.21.14 → 0.23.1
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 +1 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/{chunk-R5PDSH2A.js → chunk-T4T2PGU4.js} +3989 -1356
- package/dist/chunk-T4T2PGU4.js.map +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.js +29 -9
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +8 -0
- package/dist/integrations/slack-interactions.d.ts +7 -1
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +350 -0
- package/dist/mcp/toolspace.d.ts +9 -0
- package/dist/routes/transcription-recordings.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +5 -1
- package/dist/transcription/segmenter.d.ts +10 -0
- package/dist/transcription/service.d.ts +5 -0
- package/package.json +12 -12
- package/src/app.ts +39 -6
- package/src/auth/managed-auth.ts +0 -16
- package/src/http/sse.ts +101 -6
- package/src/index.ts +28 -3
- package/src/integrations/oauth-client.ts +36 -56
- package/src/integrations/slack-interactions.ts +123 -15
- package/src/mcp/documents.ts +42 -25
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +608 -0
- package/src/mcp/server.ts +812 -182
- package/src/mcp/toolspace.ts +75 -71
- package/src/observability.ts +3 -3
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/codex.ts +7 -4
- package/src/routes/connections.ts +74 -3
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +175 -65
- package/src/routes/install.ts +31 -1
- package/src/routes/machines.ts +1 -1
- package/src/routes/scheduled-tasks.ts +39 -14
- package/src/routes/sessions.ts +77 -12
- package/src/routes/transcription-recordings.ts +754 -0
- package/src/routes/transcriptions.ts +2 -0
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +124 -7
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/viewer.ts +29 -20
- package/src/transcription/providers/azure-openai.ts +4 -3
- package/src/transcription/providers/codex-subscription.ts +4 -1
- package/src/transcription/providers/openai.ts +7 -2
- package/src/transcription/segmenter.ts +260 -0
- package/src/transcription/service.ts +111 -10
- package/dist/chunk-R5PDSH2A.js.map +0 -1
package/src/routes/files.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
9
9
|
RetainedArtifactMetadataSchema,
|
|
10
10
|
retainedArtifactReferenceFromFile,
|
|
11
|
+
retainedScreenshotReferenceFromFile,
|
|
11
12
|
resolveRetainedOutputRange,
|
|
12
13
|
type RetainedArtifactMetadata,
|
|
13
14
|
type RetainedOutputUnavailableReason,
|
|
@@ -19,10 +20,12 @@ import {
|
|
|
19
20
|
createFileUpload,
|
|
20
21
|
getFileUpload,
|
|
21
22
|
getRetainedFileArtifact,
|
|
23
|
+
getRetainedScreenshotArtifact,
|
|
22
24
|
requireFile,
|
|
23
25
|
type RetainedFileArtifact,
|
|
26
|
+
type RetainedScreenshotArtifact,
|
|
24
27
|
} from "@opengeni/db";
|
|
25
|
-
import type { Hono } from "hono";
|
|
28
|
+
import type { Context, Hono } from "hono";
|
|
26
29
|
import { HTTPException } from "hono/http-exception";
|
|
27
30
|
import { requireAccessGrant } from "@opengeni/core";
|
|
28
31
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
@@ -36,7 +39,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
36
39
|
app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => {
|
|
37
40
|
const workspaceId = c.req.param("workspaceId");
|
|
38
41
|
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
39
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
42
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
43
|
+
enableJsonResponse: true,
|
|
44
|
+
});
|
|
40
45
|
const server = buildFilesMcpServer(deps, grant);
|
|
41
46
|
await server.connect(transport);
|
|
42
47
|
return await transport.handleRequest(c.req.raw);
|
|
@@ -46,7 +51,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
46
51
|
const workspaceId = c.req.param("workspaceId");
|
|
47
52
|
const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload");
|
|
48
53
|
if (!objectStorage) {
|
|
49
|
-
throw new HTTPException(503, {
|
|
54
|
+
throw new HTTPException(503, {
|
|
55
|
+
message: "object storage is not configured",
|
|
56
|
+
});
|
|
50
57
|
}
|
|
51
58
|
const payload = CreateFileUploadRequest.parse(await c.req.json());
|
|
52
59
|
await requireLimit(deps, {
|
|
@@ -98,7 +105,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
98
105
|
const workspaceId = c.req.param("workspaceId");
|
|
99
106
|
const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload");
|
|
100
107
|
if (!objectStorage) {
|
|
101
|
-
throw new HTTPException(503, {
|
|
108
|
+
throw new HTTPException(503, {
|
|
109
|
+
message: "object storage is not configured",
|
|
110
|
+
});
|
|
102
111
|
}
|
|
103
112
|
const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
|
|
104
113
|
if (!upload) {
|
|
@@ -186,7 +195,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
186
195
|
terminalStatus,
|
|
187
196
|
});
|
|
188
197
|
if (!settled) {
|
|
189
|
-
throw new HTTPException(409, {
|
|
198
|
+
throw new HTTPException(409, {
|
|
199
|
+
message: "file upload cleanup claim was superseded",
|
|
200
|
+
});
|
|
190
201
|
}
|
|
191
202
|
throw new HTTPException(status, { message });
|
|
192
203
|
};
|
|
@@ -275,75 +286,63 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
275
286
|
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
276
287
|
}
|
|
277
288
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
return await serveRetainedArtifactContent(
|
|
290
|
+
c,
|
|
291
|
+
objectStorage,
|
|
292
|
+
artifactId,
|
|
293
|
+
artifact.file,
|
|
294
|
+
retainedArtifactMetadata(artifact),
|
|
295
|
+
);
|
|
296
|
+
});
|
|
285
297
|
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId", async (c) => {
|
|
299
|
+
const workspaceId = c.req.param("workspaceId");
|
|
300
|
+
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
301
|
+
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
302
|
+
const artifact = await getRetainedScreenshotArtifact(
|
|
303
|
+
db,
|
|
304
|
+
workspaceId,
|
|
305
|
+
c.req.param("sessionId"),
|
|
306
|
+
artifactId,
|
|
291
307
|
);
|
|
292
|
-
if (
|
|
293
|
-
return c.json(
|
|
294
|
-
{
|
|
295
|
-
message: "invalid retained artifact byte range",
|
|
296
|
-
reason: range.reason,
|
|
297
|
-
maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
298
|
-
},
|
|
299
|
-
400,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
if (range.kind === "unsatisfiable") {
|
|
303
|
-
return c.json(
|
|
304
|
-
{ message: "retained artifact byte range is not satisfiable", reason: range.reason },
|
|
305
|
-
416,
|
|
306
|
-
{
|
|
307
|
-
"Accept-Ranges": "bytes",
|
|
308
|
-
"Content-Range": range.contentRange,
|
|
309
|
-
"Cache-Control": "private, no-store",
|
|
310
|
-
},
|
|
311
|
-
);
|
|
308
|
+
if (!artifact) {
|
|
309
|
+
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
312
310
|
}
|
|
311
|
+
return c.json(retainedScreenshotMetadata(artifact));
|
|
312
|
+
});
|
|
313
313
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"
|
|
318
|
-
"
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
314
|
+
app.get(
|
|
315
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId/content",
|
|
316
|
+
async (c) => {
|
|
317
|
+
const workspaceId = c.req.param("workspaceId");
|
|
318
|
+
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
319
|
+
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
320
|
+
const artifact = await getRetainedScreenshotArtifact(
|
|
321
|
+
db,
|
|
322
|
+
workspaceId,
|
|
323
|
+
c.req.param("sessionId"),
|
|
324
|
+
artifactId,
|
|
325
|
+
);
|
|
326
|
+
if (!artifact) {
|
|
327
|
+
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
325
328
|
}
|
|
326
|
-
return
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
if (bytes.byteLength !== range.length) {
|
|
337
|
-
throw new HTTPException(502, { message: "object storage returned an invalid byte range" });
|
|
338
|
-
}
|
|
339
|
-
return c.body(new Uint8Array(bytes), range.status, headers);
|
|
340
|
-
});
|
|
329
|
+
return await serveRetainedArtifactContent(
|
|
330
|
+
c,
|
|
331
|
+
objectStorage,
|
|
332
|
+
artifactId,
|
|
333
|
+
artifact.file,
|
|
334
|
+
retainedScreenshotMetadata(artifact),
|
|
335
|
+
);
|
|
336
|
+
},
|
|
337
|
+
);
|
|
341
338
|
|
|
342
339
|
app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
|
|
343
340
|
const workspaceId = c.req.param("workspaceId");
|
|
344
341
|
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
345
342
|
if (!objectStorage) {
|
|
346
|
-
throw new HTTPException(503, {
|
|
343
|
+
throw new HTTPException(503, {
|
|
344
|
+
message: "object storage is not configured",
|
|
345
|
+
});
|
|
347
346
|
}
|
|
348
347
|
const file = await requireFile(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
349
348
|
if (!file) {
|
|
@@ -362,6 +361,81 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
362
361
|
});
|
|
363
362
|
}
|
|
364
363
|
|
|
364
|
+
async function serveRetainedArtifactContent(
|
|
365
|
+
c: Context,
|
|
366
|
+
objectStorage: ApiRouteDeps["objectStorage"],
|
|
367
|
+
artifactId: string,
|
|
368
|
+
file: RetainedFileArtifact["file"],
|
|
369
|
+
metadata: RetainedArtifactMetadata,
|
|
370
|
+
) {
|
|
371
|
+
if (!metadata.available) {
|
|
372
|
+
return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
|
|
373
|
+
}
|
|
374
|
+
if (!objectStorage) {
|
|
375
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const rangeHeader = c.req.header("range");
|
|
379
|
+
const range = resolveRetainedOutputRange(
|
|
380
|
+
rangeHeader,
|
|
381
|
+
metadata.originalBytes,
|
|
382
|
+
rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
|
|
383
|
+
);
|
|
384
|
+
if (range.kind === "invalid") {
|
|
385
|
+
return c.json(
|
|
386
|
+
{
|
|
387
|
+
message: "invalid retained artifact byte range",
|
|
388
|
+
reason: range.reason,
|
|
389
|
+
maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
390
|
+
},
|
|
391
|
+
400,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (range.kind === "unsatisfiable") {
|
|
395
|
+
return c.json(
|
|
396
|
+
{
|
|
397
|
+
message: "retained artifact byte range is not satisfiable",
|
|
398
|
+
reason: range.reason,
|
|
399
|
+
},
|
|
400
|
+
416,
|
|
401
|
+
{
|
|
402
|
+
"Accept-Ranges": "bytes",
|
|
403
|
+
"Content-Range": range.contentRange,
|
|
404
|
+
"Cache-Control": "private, no-store",
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const headers = {
|
|
410
|
+
"Accept-Ranges": range.acceptRanges,
|
|
411
|
+
"Cache-Control": "private, no-store",
|
|
412
|
+
"Content-Length": String(range.length),
|
|
413
|
+
"Content-Type": metadata.contentType,
|
|
414
|
+
"X-Content-Type-Options": "nosniff",
|
|
415
|
+
...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
|
|
416
|
+
};
|
|
417
|
+
if (range.kind === "empty") {
|
|
418
|
+
if (!(await objectStorage.fileExists(file))) {
|
|
419
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
420
|
+
}
|
|
421
|
+
return c.body(null, 200, headers);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const bytes = await objectStorage.getFileRange(file, {
|
|
425
|
+
start: range.start,
|
|
426
|
+
end: range.end,
|
|
427
|
+
});
|
|
428
|
+
if (!bytes) {
|
|
429
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
430
|
+
}
|
|
431
|
+
if (bytes.byteLength !== range.length) {
|
|
432
|
+
throw new HTTPException(502, {
|
|
433
|
+
message: "object storage returned an invalid byte range",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return c.body(new Uint8Array(bytes), range.status, headers);
|
|
437
|
+
}
|
|
438
|
+
|
|
365
439
|
export function sanitizeFilename(filename: string): string {
|
|
366
440
|
const trimmed = filename.trim().replace(/[/\\]/g, "_");
|
|
367
441
|
const safe = trimmed
|
|
@@ -388,7 +462,11 @@ function retainedArtifactUnavailable(
|
|
|
388
462
|
artifactId: string,
|
|
389
463
|
reason: RetainedOutputUnavailableReason,
|
|
390
464
|
): RetainedArtifactMetadata {
|
|
391
|
-
return RetainedArtifactMetadataSchema.parse({
|
|
465
|
+
return RetainedArtifactMetadataSchema.parse({
|
|
466
|
+
available: false,
|
|
467
|
+
artifactId,
|
|
468
|
+
reason,
|
|
469
|
+
});
|
|
392
470
|
}
|
|
393
471
|
|
|
394
472
|
function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtifactMetadata {
|
|
@@ -417,6 +495,35 @@ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtif
|
|
|
417
495
|
return retainedArtifactUnavailable(file.id, "unsupported");
|
|
418
496
|
}
|
|
419
497
|
|
|
498
|
+
function retainedScreenshotMetadata(
|
|
499
|
+
artifact: RetainedScreenshotArtifact,
|
|
500
|
+
): RetainedArtifactMetadata {
|
|
501
|
+
if (
|
|
502
|
+
artifact.status === "ready" &&
|
|
503
|
+
artifact.sessionId &&
|
|
504
|
+
artifact.retentionExpiresAt.getTime() > Date.now()
|
|
505
|
+
) {
|
|
506
|
+
const reference = retainedScreenshotReferenceFromFile({
|
|
507
|
+
...artifact.file,
|
|
508
|
+
sessionId: artifact.sessionId,
|
|
509
|
+
width: artifact.width,
|
|
510
|
+
height: artifact.height,
|
|
511
|
+
expiresAt: artifact.retentionExpiresAt.toISOString(),
|
|
512
|
+
});
|
|
513
|
+
if (reference) return reference;
|
|
514
|
+
}
|
|
515
|
+
if (artifact.status === "deleted") {
|
|
516
|
+
return retainedArtifactUnavailable(artifact.artifactId, "deleted");
|
|
517
|
+
}
|
|
518
|
+
if (artifact.status === "expired" || artifact.retentionExpiresAt.getTime() <= Date.now()) {
|
|
519
|
+
return retainedArtifactUnavailable(artifact.artifactId, "expired");
|
|
520
|
+
}
|
|
521
|
+
if (artifact.status === "pending" || artifact.status === "reconciling") {
|
|
522
|
+
return retainedArtifactUnavailable(artifact.artifactId, "pending");
|
|
523
|
+
}
|
|
524
|
+
return retainedArtifactUnavailable(artifact.artifactId, "failed");
|
|
525
|
+
}
|
|
526
|
+
|
|
420
527
|
function retainedArtifactUnavailableStatus(
|
|
421
528
|
reason: RetainedOutputUnavailableReason,
|
|
422
529
|
): 404 | 409 | 410 | 422 {
|
|
@@ -429,6 +536,9 @@ function retainedArtifactUnavailableStatus(
|
|
|
429
536
|
case "unsupported":
|
|
430
537
|
case "not_retained":
|
|
431
538
|
case "storage_write_failed":
|
|
539
|
+
case "quota_exceeded":
|
|
540
|
+
case "invalid_content":
|
|
541
|
+
case "oversized":
|
|
432
542
|
return 422;
|
|
433
543
|
case "pending":
|
|
434
544
|
case "failed":
|
package/src/routes/install.ts
CHANGED
|
@@ -200,6 +200,31 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
200
200
|
return serveAsset(asset, `${releasesBase}/download/${stableAgentTag}/${asset}`);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
// Signed self-update channel manifests. Each deployment exposes its own
|
|
204
|
+
// immutable promotion pointer, so an enrolled machine never depends on the
|
|
205
|
+
// public get.opengeni.ai hostname. The manifest and signature are ordinary
|
|
206
|
+
// assets on the selected immutable agent release.
|
|
207
|
+
for (const [channel, version] of [
|
|
208
|
+
["stable", deps.settings.agentStableVersion],
|
|
209
|
+
["beta", deps.settings.agentBetaVersion],
|
|
210
|
+
] as const) {
|
|
211
|
+
app.get(`/agent/${channel}/:manifestAsset`, (c) => {
|
|
212
|
+
const manifestAsset = c.req.param("manifestAsset");
|
|
213
|
+
if (manifestAsset !== "manifest.json" && manifestAsset !== "manifest.json.minisig") {
|
|
214
|
+
throw new HTTPException(404, { message: "update manifest asset not found" });
|
|
215
|
+
}
|
|
216
|
+
if (!version) {
|
|
217
|
+
throw new HTTPException(404, { message: `${channel} agent channel is not configured` });
|
|
218
|
+
}
|
|
219
|
+
return new Response(null, {
|
|
220
|
+
status: 302,
|
|
221
|
+
headers: {
|
|
222
|
+
location: `${releasesBase}/download/agent-v${version}/${manifestAsset}`,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
// The version segment is the literal `v<ver>` (e.g. `v1.2.3`) — Hono cannot bind
|
|
204
229
|
// a param glued to a literal prefix, so the whole segment is the param and the
|
|
205
230
|
// `v` prefix is validated/stripped here. The release tag is `agent-v<ver>`.
|
|
@@ -220,5 +245,10 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
220
245
|
export const installExactPaths: ReadonlySet<string> = new Set(Object.keys(TEXT_ASSETS));
|
|
221
246
|
|
|
222
247
|
export function isInstallRedirectPath(path: string): boolean {
|
|
223
|
-
return
|
|
248
|
+
return (
|
|
249
|
+
path.startsWith("/agent/latest/") ||
|
|
250
|
+
path.startsWith("/agent/v") ||
|
|
251
|
+
path.startsWith("/agent/stable/") ||
|
|
252
|
+
path.startsWith("/agent/beta/")
|
|
253
|
+
);
|
|
224
254
|
}
|
package/src/routes/machines.ts
CHANGED
|
@@ -73,7 +73,7 @@ export function registerMachineRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
73
73
|
// Validate the machine belongs to this workspace (RLS already scopes the read,
|
|
74
74
|
// but a clear 404 beats an empty series for an unknown/cross-workspace id).
|
|
75
75
|
const enrollment = await getEnrollment(db, workspaceId, enrollmentId);
|
|
76
|
-
if (!enrollment) {
|
|
76
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
77
77
|
throw new HTTPException(404, { message: "machine not found in this workspace" });
|
|
78
78
|
}
|
|
79
79
|
const windowMs = SERIES_WINDOWS_MS[c.req.query("window") ?? ""] ?? DEFAULT_SERIES_WINDOW_MS;
|
|
@@ -19,10 +19,13 @@ import {
|
|
|
19
19
|
manualScheduledTaskTriggerUsageKey,
|
|
20
20
|
manualScheduledTaskTriggerWorkflowId,
|
|
21
21
|
scheduledTaskToolsProvided,
|
|
22
|
+
scheduledTaskForGrant,
|
|
23
|
+
scheduledTaskRunForGrant,
|
|
22
24
|
scheduledTaskTriggerToken,
|
|
23
25
|
requireScheduledTaskForApi,
|
|
24
26
|
syncCreatedScheduledTask,
|
|
25
27
|
syncUpdatedScheduledTask,
|
|
28
|
+
validateScheduledTaskTarget,
|
|
26
29
|
validatedScheduledTaskUpdate,
|
|
27
30
|
} from "@opengeni/core";
|
|
28
31
|
import { boundedLimit } from "../http/common";
|
|
@@ -48,21 +51,25 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
48
51
|
grant,
|
|
49
52
|
payload,
|
|
50
53
|
toolsProvided: scheduledTaskToolsProvided(rawPayload),
|
|
54
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
55
|
+
authorizationSurface: "http",
|
|
51
56
|
});
|
|
52
57
|
await syncCreatedScheduledTask({ db, workflowClient, task });
|
|
53
|
-
return c.json(task, 201);
|
|
58
|
+
return c.json(scheduledTaskForGrant(task, grant), 201);
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
app.get("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
|
|
57
62
|
const workspaceId = c.req.param("workspaceId");
|
|
58
|
-
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
59
|
-
|
|
63
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
64
|
+
const tasks = await listScheduledTasks(db, workspaceId, boundedLimit(c.req.query("limit")));
|
|
65
|
+
return c.json(tasks.map((task) => scheduledTaskForGrant(task, grant)));
|
|
60
66
|
});
|
|
61
67
|
|
|
62
68
|
app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
63
69
|
const workspaceId = c.req.param("workspaceId");
|
|
64
|
-
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
65
|
-
|
|
70
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
71
|
+
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
72
|
+
return c.json(scheduledTaskForGrant(task, grant));
|
|
66
73
|
});
|
|
67
74
|
|
|
68
75
|
app.patch("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
@@ -81,30 +88,32 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
81
88
|
existing,
|
|
82
89
|
payload,
|
|
83
90
|
toolsProvided: scheduledTaskToolsProvided(rawPayload),
|
|
91
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
92
|
+
authorizationSurface: "http",
|
|
84
93
|
});
|
|
85
94
|
const task = await updateScheduledTask(db, workspaceId, taskId, update);
|
|
86
95
|
await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
|
|
87
|
-
return c.json(task);
|
|
96
|
+
return c.json(scheduledTaskForGrant(task, grant));
|
|
88
97
|
});
|
|
89
98
|
|
|
90
99
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/pause", async (c) => {
|
|
91
100
|
const workspaceId = c.req.param("workspaceId");
|
|
92
|
-
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
101
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
93
102
|
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
94
103
|
const previous = await captureScheduledTaskRestoreState(db, existing);
|
|
95
104
|
const task = await updateScheduledTask(db, workspaceId, existing.id, { status: "paused" });
|
|
96
105
|
await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
|
|
97
|
-
return c.json(task);
|
|
106
|
+
return c.json(scheduledTaskForGrant(task, grant));
|
|
98
107
|
});
|
|
99
108
|
|
|
100
109
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/resume", async (c) => {
|
|
101
110
|
const workspaceId = c.req.param("workspaceId");
|
|
102
|
-
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
111
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
103
112
|
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
104
113
|
const previous = await captureScheduledTaskRestoreState(db, existing);
|
|
105
114
|
const task = await updateScheduledTask(db, workspaceId, existing.id, { status: "active" });
|
|
106
115
|
await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
|
|
107
|
-
return c.json(task);
|
|
116
|
+
return c.json(scheduledTaskForGrant(task, grant));
|
|
108
117
|
});
|
|
109
118
|
|
|
110
119
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/trigger", async (c) => {
|
|
@@ -113,6 +122,18 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
113
122
|
// Load the task before the gate so a codex-model scheduled task can be
|
|
114
123
|
// recognised as codex-billed and skip the credit/cost gates at the edge.
|
|
115
124
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
125
|
+
await validateScheduledTaskTarget({
|
|
126
|
+
db,
|
|
127
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
128
|
+
authorizationSurface: "http",
|
|
129
|
+
grant,
|
|
130
|
+
targetSessionId: task.targetSessionId,
|
|
131
|
+
runMode: task.runMode,
|
|
132
|
+
variableSetId: task.variableSetId,
|
|
133
|
+
rigId: task.rigId,
|
|
134
|
+
agentConfig: task.agentConfig,
|
|
135
|
+
missingTargetStatus: 404,
|
|
136
|
+
});
|
|
116
137
|
await requireLimit(deps, {
|
|
117
138
|
accountId: grant.accountId,
|
|
118
139
|
workspaceId,
|
|
@@ -148,7 +169,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
148
169
|
sourceResourceId: task.id,
|
|
149
170
|
idempotencyKey: agentRunUsageIdempotencyKey,
|
|
150
171
|
});
|
|
151
|
-
return c.json(task, 202);
|
|
172
|
+
return c.json(scheduledTaskForGrant(task, grant), 202);
|
|
152
173
|
});
|
|
153
174
|
|
|
154
175
|
app.delete("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
@@ -164,10 +185,14 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
164
185
|
|
|
165
186
|
app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/runs", async (c) => {
|
|
166
187
|
const workspaceId = c.req.param("workspaceId");
|
|
167
|
-
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
188
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
168
189
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
const taskRuns = await listScheduledTaskRuns(
|
|
191
|
+
db,
|
|
192
|
+
workspaceId,
|
|
193
|
+
task.id,
|
|
194
|
+
boundedLimit(c.req.query("limit")),
|
|
171
195
|
);
|
|
196
|
+
return c.json(taskRuns.map((run) => scheduledTaskRunForGrant(run, grant)));
|
|
172
197
|
});
|
|
173
198
|
}
|