@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.
Files changed (48) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-YY6OAEL6.js → chunk-DO2G3JSB.js} +5333 -2205
  3. package/dist/chunk-DO2G3JSB.js.map +1 -0
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +297 -54
  6. package/dist/index.js.map +1 -1
  7. package/package.json +21 -21
  8. package/src/app.ts +415 -147
  9. package/src/auth/managed-auth.ts +32 -16
  10. package/src/http/auth.ts +8 -1
  11. package/src/http/common.ts +6 -2
  12. package/src/http/sse.ts +27 -6
  13. package/src/index.ts +196 -74
  14. package/src/integrations/oauth-client.ts +592 -131
  15. package/src/integrations/provider-domain.ts +4 -1
  16. package/src/mcp/documents.ts +173 -94
  17. package/src/mcp/server.ts +1517 -692
  18. package/src/mcp/session-view.ts +8 -2
  19. package/src/mcp/toolspace.ts +175 -84
  20. package/src/observability.ts +7 -1
  21. package/src/routes/api-keys.ts +39 -23
  22. package/src/routes/billing.ts +180 -65
  23. package/src/routes/capabilities.ts +17 -8
  24. package/src/routes/catalog-assets.ts +5 -2
  25. package/src/routes/codex.ts +244 -63
  26. package/src/routes/connections.ts +72 -34
  27. package/src/routes/documents.ts +242 -92
  28. package/src/routes/enrollments.ts +100 -70
  29. package/src/routes/environments.ts +205 -136
  30. package/src/routes/files.ts +164 -39
  31. package/src/routes/github.ts +123 -50
  32. package/src/routes/install.ts +9 -2
  33. package/src/routes/machines.ts +9 -8
  34. package/src/routes/packs.ts +141 -89
  35. package/src/routes/rigs.ts +189 -0
  36. package/src/routes/scheduled-tasks.ts +51 -9
  37. package/src/routes/sessions.ts +839 -329
  38. package/src/routes/social.ts +50 -38
  39. package/src/routes/workspace-capture.ts +238 -0
  40. package/src/routes/workspaces.ts +159 -13
  41. package/src/sandbox/access.ts +11 -3
  42. package/src/sandbox/auth-callout.ts +5 -1
  43. package/src/sandbox/channel-a.ts +104 -27
  44. package/src/sandbox/enrollment.ts +13 -3
  45. package/src/sandbox/machines.ts +68 -59
  46. package/src/sandbox/metrics-ingestion.ts +238 -17
  47. package/src/sandbox/viewer.ts +172 -46
  48. package/dist/chunk-YY6OAEL6.js.map +0 -1
@@ -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 { validateEnvironmentAttachment } from "@opengeni/core";
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, { message: `pack id ${manifest.id} is a built-in pack and cannot be replaced` });
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(db, workspaceId, `pack:${packId}`);
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 environmentId keeps the stored attachment instead of
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 environment was deleted or its variables changed since. The
121
- // inherited attachment was authorized with environments:use when it was
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
- 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" });
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 (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));
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, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
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
- ...(environmentId ? { environmentId } : {}),
165
+ ...(variableSetId ? { variableSetId } : {}),
144
166
  },
145
167
  });
146
168
  return c.json(installation, existing ? 200 : 201);
147
169
  });
148
170
 
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,
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
- await syncCreatedScheduledTask({ db, workflowClient, task });
204
- return c.json(task, 201);
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(db: ApiRouteDeps["db"], workspaceId: string, connectionIds: string[]): Promise<SocialConnection[]> {
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 = 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");
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, { message: `social connection ${inactive.id} is ${inactive.status}` });
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(db: ApiRouteDeps["db"], workspaceId: string, documentBaseIds: string[]): Promise<void> {
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 { CreateScheduledTaskRequest, TriggerScheduledTaskRequest, UpdateScheduledTaskRequest } from "@opengeni/contracts";
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, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
34
- const task = await createValidatedScheduledTask({ settings, db, objectStorage, grant, payload, toolsProvided: scheduledTaskToolsProvided(rawPayload) });
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({ settings, db, objectStorage, grant, existing, payload, toolsProvided: scheduledTaskToolsProvided(rawPayload) });
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, { accountId: grant.accountId, workspaceId, action: "agent_run:create", quantity: 1, model: task.agentConfig.model ?? deps.settings.openaiModel });
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(workspaceId, task.id, triggerToken);
124
+ const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey(
125
+ workspaceId,
126
+ task.id,
127
+ triggerToken,
128
+ );
95
129
  const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
96
- await workflowClient.triggerScheduledTask({ task, agentRunUsageIdempotencyKey, triggerWorkflowId });
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({ temporalScheduleId: task.temporalScheduleId });
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(await listScheduledTaskRuns(db, workspaceId, task.id, boundedLimit(c.req.query("limit"))));
164
+ return c.json(
165
+ await listScheduledTaskRuns(db, workspaceId, task.id, boundedLimit(c.req.query("limit"))),
166
+ );
125
167
  });
126
168
  }