@opengeni/api-router 0.2.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 +16 -0
- package/dist/app.js +35 -0
- package/dist/app.js.map +1 -0
- package/dist/chunk-XSYUDIX3.js +6331 -0
- package/dist/chunk-XSYUDIX3.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +567 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/app.ts +351 -0
- package/src/auth/managed-auth.ts +237 -0
- package/src/http/auth.ts +92 -0
- package/src/http/common.ts +16 -0
- package/src/http/sse.ts +89 -0
- package/src/index.ts +362 -0
- package/src/mcp/documents.ts +57 -0
- package/src/mcp/server.ts +961 -0
- package/src/mcp/session-view.ts +281 -0
- package/src/routes/api-keys.ts +65 -0
- package/src/routes/billing.ts +495 -0
- package/src/routes/capabilities.ts +80 -0
- package/src/routes/codex.ts +393 -0
- package/src/routes/documents.ts +185 -0
- package/src/routes/enrollments.ts +357 -0
- package/src/routes/environments.ts +175 -0
- package/src/routes/files.ts +148 -0
- package/src/routes/github.ts +341 -0
- package/src/routes/install.ts +218 -0
- package/src/routes/machines.ts +107 -0
- package/src/routes/packs.ts +241 -0
- package/src/routes/scheduled-tasks.ts +126 -0
- package/src/routes/sessions.ts +1083 -0
- package/src/routes/social.ts +119 -0
- package/src/routes/workspaces.ts +206 -0
- package/src/sandbox/access.ts +89 -0
- package/src/sandbox/auth-callout.ts +178 -0
- package/src/sandbox/channel-a.ts +265 -0
- package/src/sandbox/enrollment.ts +498 -0
- package/src/sandbox/machines.ts +255 -0
- package/src/sandbox/metrics-ingestion.ts +289 -0
- package/src/sandbox/viewer.ts +993 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EnablePackRequest,
|
|
3
|
+
MarketingDailyAnalysisTaskRequest,
|
|
4
|
+
RegisterCapabilityPackRequest,
|
|
5
|
+
type SocialConnection,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import {
|
|
8
|
+
deleteWorkspacePack,
|
|
9
|
+
disableCapabilityInstallation,
|
|
10
|
+
enablePackInstallation,
|
|
11
|
+
getCapabilityInstallation,
|
|
12
|
+
getPackInstallation,
|
|
13
|
+
getWorkspacePack,
|
|
14
|
+
getSocialConnection,
|
|
15
|
+
listPackInstallations,
|
|
16
|
+
listSocialConnections,
|
|
17
|
+
registerWorkspacePack,
|
|
18
|
+
updatePackInstallationStatus,
|
|
19
|
+
} from "@opengeni/db";
|
|
20
|
+
import { getDocumentBase } from "@opengeni/documents";
|
|
21
|
+
import type { Hono } from "hono";
|
|
22
|
+
import { HTTPException } from "hono/http-exception";
|
|
23
|
+
import { requireAccessGrant } from "@opengeni/core";
|
|
24
|
+
import { requireLimit } from "@opengeni/core";
|
|
25
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
26
|
+
import { validateEnvironmentAttachment } from "@opengeni/core";
|
|
27
|
+
import {
|
|
28
|
+
assertPackSandboxImageCompatible,
|
|
29
|
+
buildMarketingDailyAnalysisAgentConfig,
|
|
30
|
+
isBuiltInCapabilityPack,
|
|
31
|
+
listWorkspaceCapabilityPacks,
|
|
32
|
+
MARKETING_SOCIAL_PACK_ID,
|
|
33
|
+
resolveCapabilityPack,
|
|
34
|
+
} from "@opengeni/core";
|
|
35
|
+
import {
|
|
36
|
+
createValidatedScheduledTask,
|
|
37
|
+
syncCreatedScheduledTask,
|
|
38
|
+
} from "@opengeni/core";
|
|
39
|
+
|
|
40
|
+
export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
41
|
+
const { settings, db, objectStorage, workflowClient } = deps;
|
|
42
|
+
|
|
43
|
+
app.get("/v1/workspaces/:workspaceId/packs", async (c) => {
|
|
44
|
+
const workspaceId = c.req.param("workspaceId");
|
|
45
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
46
|
+
return c.json({
|
|
47
|
+
packs: await listWorkspaceCapabilityPacks(db, workspaceId),
|
|
48
|
+
installations: await listPackInstallations(db, workspaceId),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Registers (or replaces) a workspace-scoped pack from a manifest payload.
|
|
53
|
+
// Built-in pack ids stay reserved so a registration can never shadow them.
|
|
54
|
+
app.post("/v1/workspaces/:workspaceId/packs", async (c) => {
|
|
55
|
+
const workspaceId = c.req.param("workspaceId");
|
|
56
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
57
|
+
const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
|
|
58
|
+
if (isBuiltInCapabilityPack(manifest.id)) {
|
|
59
|
+
throw new HTTPException(409, { message: `pack id ${manifest.id} is a built-in pack and cannot be replaced` });
|
|
60
|
+
}
|
|
61
|
+
const { pack, created } = await registerWorkspacePack(db, {
|
|
62
|
+
accountId: grant.accountId,
|
|
63
|
+
workspaceId,
|
|
64
|
+
pack: manifest,
|
|
65
|
+
});
|
|
66
|
+
return c.json(pack, created ? 201 : 200);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.delete("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
|
|
70
|
+
const workspaceId = c.req.param("workspaceId");
|
|
71
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
72
|
+
const packId = c.req.param("packId");
|
|
73
|
+
if (isBuiltInCapabilityPack(packId)) {
|
|
74
|
+
throw new HTTPException(409, { message: "built-in packs cannot be unregistered" });
|
|
75
|
+
}
|
|
76
|
+
if (!await getWorkspacePack(db, workspaceId, packId)) {
|
|
77
|
+
throw new HTTPException(404, { message: "pack not found" });
|
|
78
|
+
}
|
|
79
|
+
// Disable installations before deleting the registration so a crash in
|
|
80
|
+
// between can never orphan an active installation whose manifest is
|
|
81
|
+
// gone; the capability installation row for pack:{packId} would
|
|
82
|
+
// otherwise keep a future re-registration looking enabled.
|
|
83
|
+
const installation = await getPackInstallation(db, workspaceId, packId);
|
|
84
|
+
if (installation && installation.status === "active") {
|
|
85
|
+
await updatePackInstallationStatus(db, workspaceId, packId, "disabled");
|
|
86
|
+
}
|
|
87
|
+
const capabilityInstallation = await getCapabilityInstallation(db, workspaceId, `pack:${packId}`);
|
|
88
|
+
if (capabilityInstallation && capabilityInstallation.status === "active") {
|
|
89
|
+
await disableCapabilityInstallation(db, workspaceId, `pack:${packId}`);
|
|
90
|
+
}
|
|
91
|
+
await deleteWorkspacePack(db, workspaceId, packId);
|
|
92
|
+
return c.body(null, 204);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.get("/v1/workspaces/:workspaceId/packs/installations", async (c) => {
|
|
96
|
+
const workspaceId = c.req.param("workspaceId");
|
|
97
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
98
|
+
return c.json(await listPackInstallations(db, workspaceId));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
app.get("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
|
|
102
|
+
const workspaceId = c.req.param("workspaceId");
|
|
103
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
104
|
+
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
105
|
+
return c.json({
|
|
106
|
+
pack,
|
|
107
|
+
installation: await getPackInstallation(db, workspaceId, pack.id),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
app.post("/v1/workspaces/:workspaceId/packs/:packId/enable", async (c) => {
|
|
112
|
+
const workspaceId = c.req.param("workspaceId");
|
|
113
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
114
|
+
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
115
|
+
await assertPackSandboxImageCompatible(db, workspaceId, pack);
|
|
116
|
+
const existing = await getPackInstallation(db, workspaceId, pack.id);
|
|
117
|
+
const payload = EnablePackRequest.parse(await c.req.json());
|
|
118
|
+
// Re-enabling without environmentId keeps the stored attachment instead of
|
|
119
|
+
// silently dropping it; the inherited attachment is re-validated below in
|
|
120
|
+
// case the environment was deleted or its variables changed since. The
|
|
121
|
+
// inherited attachment was authorized with environments:use when it was
|
|
122
|
+
// first attached, so only a fresh attachment re-checks that permission.
|
|
123
|
+
const storedEnvironmentId = typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : undefined;
|
|
124
|
+
const environmentId = payload.environmentId ?? storedEnvironmentId;
|
|
125
|
+
if (pack.environment?.required && !environmentId) {
|
|
126
|
+
throw new HTTPException(422, { message: "this pack requires an environment attachment; pass environmentId" });
|
|
127
|
+
}
|
|
128
|
+
if (environmentId) {
|
|
129
|
+
const environment = await validateEnvironmentAttachment({ settings, db }, grant, workspaceId, environmentId, { preauthorized: !payload.environmentId });
|
|
130
|
+
const missing = (pack.environment?.requiredVariables ?? [])
|
|
131
|
+
.filter((name) => !environment.variables.some((variable) => variable.name === name));
|
|
132
|
+
if (missing.length > 0) {
|
|
133
|
+
throw new HTTPException(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const installation = await enablePackInstallation(db, {
|
|
137
|
+
accountId: grant.accountId,
|
|
138
|
+
workspaceId,
|
|
139
|
+
packId: pack.id,
|
|
140
|
+
metadata: {
|
|
141
|
+
...payload.metadata,
|
|
142
|
+
packVersion: pack.version,
|
|
143
|
+
...(environmentId ? { environmentId } : {}),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
return c.json(installation, existing ? 200 : 201);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.post("/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks", async (c) => {
|
|
150
|
+
const workspaceId = c.req.param("workspaceId");
|
|
151
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
152
|
+
const pack = await requirePack(db, workspaceId, MARKETING_SOCIAL_PACK_ID);
|
|
153
|
+
const installation = await getPackInstallation(db, workspaceId, pack.id);
|
|
154
|
+
if (installation?.status !== "active") {
|
|
155
|
+
throw new HTTPException(409, { message: "enable the marketing social pack before creating its scheduled tasks" });
|
|
156
|
+
}
|
|
157
|
+
const payload = MarketingDailyAnalysisTaskRequest.parse(await c.req.json());
|
|
158
|
+
await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
|
|
159
|
+
const connections = await resolveSocialConnections(db, workspaceId, payload.connectionIds);
|
|
160
|
+
if (connections.length === 0) {
|
|
161
|
+
throw new HTTPException(422, { message: "at least one connected social account is required" });
|
|
162
|
+
}
|
|
163
|
+
await validateDocumentBaseIds(db, workspaceId, payload.documentBaseIds);
|
|
164
|
+
const agentConfig = buildMarketingDailyAnalysisAgentConfig({
|
|
165
|
+
connections,
|
|
166
|
+
documentBaseIds: payload.documentBaseIds,
|
|
167
|
+
...(payload.promptInstructions ? { promptInstructions: payload.promptInstructions } : {}),
|
|
168
|
+
});
|
|
169
|
+
// Installation-inherited environment attachment: it was authorized with
|
|
170
|
+
// environments:use at pack-enable time, so the scheduled_tasks:manage
|
|
171
|
+
// caller here is not re-checked for that permission.
|
|
172
|
+
const installationEnvironmentId = typeof installation.metadata.environmentId === "string"
|
|
173
|
+
? installation.metadata.environmentId
|
|
174
|
+
: undefined;
|
|
175
|
+
const task = await createValidatedScheduledTask({
|
|
176
|
+
settings,
|
|
177
|
+
db,
|
|
178
|
+
objectStorage,
|
|
179
|
+
grant,
|
|
180
|
+
environmentPreauthorized: true,
|
|
181
|
+
payload: {
|
|
182
|
+
name: payload.name ?? "Daily social media analysis",
|
|
183
|
+
status: payload.status,
|
|
184
|
+
schedule: {
|
|
185
|
+
type: "calendar",
|
|
186
|
+
timeZone: payload.timeZone,
|
|
187
|
+
hour: payload.hour,
|
|
188
|
+
minute: payload.minute,
|
|
189
|
+
},
|
|
190
|
+
runMode: payload.runMode,
|
|
191
|
+
overlapPolicy: payload.overlapPolicy,
|
|
192
|
+
agentConfig,
|
|
193
|
+
...(installationEnvironmentId ? { environmentId: installationEnvironmentId } : {}),
|
|
194
|
+
metadata: {
|
|
195
|
+
packId: pack.id,
|
|
196
|
+
packVersion: pack.version,
|
|
197
|
+
packTemplateId: "daily-social-analysis",
|
|
198
|
+
socialConnectionIds: connections.map((connection) => connection.id),
|
|
199
|
+
documentBaseIds: payload.documentBaseIds,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
await syncCreatedScheduledTask({ db, workflowClient, task });
|
|
204
|
+
return c.json(task, 201);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function requirePack(db: ApiRouteDeps["db"], workspaceId: string, packId: string) {
|
|
209
|
+
const pack = await resolveCapabilityPack(db, workspaceId, packId);
|
|
210
|
+
if (!pack) {
|
|
211
|
+
throw new HTTPException(404, { message: "pack not found" });
|
|
212
|
+
}
|
|
213
|
+
return pack;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function resolveSocialConnections(db: ApiRouteDeps["db"], workspaceId: string, connectionIds: string[]): Promise<SocialConnection[]> {
|
|
217
|
+
const ids = [...new Set(connectionIds)];
|
|
218
|
+
const connections = ids.length > 0
|
|
219
|
+
? await Promise.all(ids.map(async (id) => {
|
|
220
|
+
const connection = await getSocialConnection(db, workspaceId, id);
|
|
221
|
+
if (!connection) {
|
|
222
|
+
throw new HTTPException(422, { message: `unknown social connection: ${id}` });
|
|
223
|
+
}
|
|
224
|
+
return connection;
|
|
225
|
+
}))
|
|
226
|
+
: (await listSocialConnections(db, workspaceId, 500)).filter((connection) => connection.status === "connected");
|
|
227
|
+
const inactive = connections.find((connection) => connection.status !== "connected");
|
|
228
|
+
if (inactive) {
|
|
229
|
+
throw new HTTPException(422, { message: `social connection ${inactive.id} is ${inactive.status}` });
|
|
230
|
+
}
|
|
231
|
+
return connections;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function validateDocumentBaseIds(db: ApiRouteDeps["db"], workspaceId: string, documentBaseIds: string[]): Promise<void> {
|
|
235
|
+
for (const baseId of [...new Set(documentBaseIds)]) {
|
|
236
|
+
const base = await getDocumentBase(db, workspaceId, baseId);
|
|
237
|
+
if (!base) {
|
|
238
|
+
throw new HTTPException(422, { message: `unknown document base: ${baseId}` });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CreateScheduledTaskRequest, TriggerScheduledTaskRequest, UpdateScheduledTaskRequest } from "@opengeni/contracts";
|
|
2
|
+
import {
|
|
3
|
+
deleteScheduledTask,
|
|
4
|
+
listScheduledTaskRuns,
|
|
5
|
+
listScheduledTasks,
|
|
6
|
+
updateScheduledTask,
|
|
7
|
+
} from "@opengeni/db";
|
|
8
|
+
import type { Hono } from "hono";
|
|
9
|
+
import { requireAccessGrant } from "@opengeni/core";
|
|
10
|
+
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
11
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
12
|
+
import {
|
|
13
|
+
createValidatedScheduledTask,
|
|
14
|
+
manualScheduledTaskTriggerUsageKey,
|
|
15
|
+
manualScheduledTaskTriggerWorkflowId,
|
|
16
|
+
scheduledTaskToolsProvided,
|
|
17
|
+
scheduledTaskTriggerToken,
|
|
18
|
+
requireScheduledTaskForApi,
|
|
19
|
+
syncCreatedScheduledTask,
|
|
20
|
+
syncUpdatedScheduledTask,
|
|
21
|
+
validatedScheduledTaskUpdate,
|
|
22
|
+
} from "@opengeni/core";
|
|
23
|
+
import { boundedLimit } from "../http/common";
|
|
24
|
+
|
|
25
|
+
export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
26
|
+
const { settings, db, workflowClient, objectStorage } = deps;
|
|
27
|
+
|
|
28
|
+
app.post("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
|
|
29
|
+
const workspaceId = c.req.param("workspaceId");
|
|
30
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
31
|
+
const rawPayload = await c.req.json();
|
|
32
|
+
const payload = CreateScheduledTaskRequest.parse(rawPayload);
|
|
33
|
+
await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
|
|
34
|
+
const task = await createValidatedScheduledTask({ settings, db, objectStorage, grant, payload, toolsProvided: scheduledTaskToolsProvided(rawPayload) });
|
|
35
|
+
await syncCreatedScheduledTask({ db, workflowClient, task });
|
|
36
|
+
return c.json(task, 201);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.get("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
|
|
40
|
+
const workspaceId = c.req.param("workspaceId");
|
|
41
|
+
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
42
|
+
return c.json(await listScheduledTasks(db, workspaceId, boundedLimit(c.req.query("limit"))));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
46
|
+
const workspaceId = c.req.param("workspaceId");
|
|
47
|
+
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
48
|
+
return c.json(await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId")));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.patch("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
52
|
+
const workspaceId = c.req.param("workspaceId");
|
|
53
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
54
|
+
const taskId = c.req.param("taskId");
|
|
55
|
+
const existing = await requireScheduledTaskForApi(db, workspaceId, taskId);
|
|
56
|
+
const rawPayload = await c.req.json();
|
|
57
|
+
const payload = UpdateScheduledTaskRequest.parse(rawPayload);
|
|
58
|
+
const update = await validatedScheduledTaskUpdate({ settings, db, objectStorage, grant, existing, payload, toolsProvided: scheduledTaskToolsProvided(rawPayload) });
|
|
59
|
+
const task = await updateScheduledTask(db, workspaceId, taskId, update);
|
|
60
|
+
await syncUpdatedScheduledTask({ db, workflowClient, previous: existing, task });
|
|
61
|
+
return c.json(task);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/pause", async (c) => {
|
|
65
|
+
const workspaceId = c.req.param("workspaceId");
|
|
66
|
+
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
67
|
+
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
68
|
+
const task = await updateScheduledTask(db, workspaceId, existing.id, { status: "paused" });
|
|
69
|
+
await syncUpdatedScheduledTask({ db, workflowClient, previous: existing, task });
|
|
70
|
+
return c.json(task);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/resume", async (c) => {
|
|
74
|
+
const workspaceId = c.req.param("workspaceId");
|
|
75
|
+
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
76
|
+
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
77
|
+
const task = await updateScheduledTask(db, workspaceId, existing.id, { status: "active" });
|
|
78
|
+
await syncUpdatedScheduledTask({ db, workflowClient, previous: existing, task });
|
|
79
|
+
return c.json(task);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/trigger", async (c) => {
|
|
83
|
+
const workspaceId = c.req.param("workspaceId");
|
|
84
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
85
|
+
// Load the task before the gate so a codex-model scheduled task can be
|
|
86
|
+
// recognised as codex-billed and skip the credit/cost gates at the edge.
|
|
87
|
+
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
88
|
+
await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "agent_run:create", quantity: 1, model: task.agentConfig.model ?? deps.settings.openaiModel });
|
|
89
|
+
// Body is optional (a bare POST is still a valid trigger); only a present,
|
|
90
|
+
// non-empty body must parse against the contract.
|
|
91
|
+
const body = await c.req.json().catch(() => ({}));
|
|
92
|
+
const { triggerId } = TriggerScheduledTaskRequest.parse(body ?? {});
|
|
93
|
+
const triggerToken = scheduledTaskTriggerToken(triggerId);
|
|
94
|
+
const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey(workspaceId, task.id, triggerToken);
|
|
95
|
+
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
96
|
+
await workflowClient.triggerScheduledTask({ task, agentRunUsageIdempotencyKey, triggerWorkflowId });
|
|
97
|
+
await recordWorkspaceUsage(deps, {
|
|
98
|
+
accountId: grant.accountId,
|
|
99
|
+
workspaceId,
|
|
100
|
+
subjectId: grant.subjectId,
|
|
101
|
+
eventType: "agent_run.created",
|
|
102
|
+
quantity: 1,
|
|
103
|
+
unit: "run",
|
|
104
|
+
sourceResourceType: "scheduled_task",
|
|
105
|
+
sourceResourceId: task.id,
|
|
106
|
+
idempotencyKey: agentRunUsageIdempotencyKey,
|
|
107
|
+
});
|
|
108
|
+
return c.json(task, 202);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
app.delete("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
112
|
+
const workspaceId = c.req.param("workspaceId");
|
|
113
|
+
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
114
|
+
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
115
|
+
await workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId: task.temporalScheduleId });
|
|
116
|
+
await deleteScheduledTask(db, workspaceId, task.id);
|
|
117
|
+
return c.json({ ok: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/runs", async (c) => {
|
|
121
|
+
const workspaceId = c.req.param("workspaceId");
|
|
122
|
+
await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:run");
|
|
123
|
+
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
124
|
+
return c.json(await listScheduledTaskRuns(db, workspaceId, task.id, boundedLimit(c.req.query("limit"))));
|
|
125
|
+
});
|
|
126
|
+
}
|