@opengeni/api-router 2.13.0-canary.0 → 2.13.0-canary.2

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 (52) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-BJWHVMOL.js → chunk-K2NU4KRK.js} +3165 -3662
  3. package/dist/{chunk-BJWHVMOL.js.map → chunk-K2NU4KRK.js.map} +1 -1
  4. package/dist/index.js +5 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/integrations/github-lens-connect.d.ts +2 -2
  7. package/dist/integrations/slack-interactions.d.ts +1 -1
  8. package/dist/mcp/documents.d.ts +5 -13
  9. package/dist/mcp/files.d.ts +2 -2
  10. package/dist/mcp/first-party-tool-permissions.d.ts +36 -0
  11. package/dist/mcp/knowledge-entries.d.ts +5 -0
  12. package/dist/mcp/server.d.ts +4 -4
  13. package/dist/routes/host-mcp-resolvers.d.ts +3 -0
  14. package/dist/routes/knowledge.d.ts +3 -0
  15. package/dist/sandbox-file-artifacts.d.ts +6 -2
  16. package/dist/workspace-state-projection.d.ts +3 -5
  17. package/dist/workspace-tool-gateway.d.ts +2 -1
  18. package/package.json +19 -19
  19. package/src/access-grant-rls.ts +13 -1
  20. package/src/app.ts +4 -11
  21. package/src/codemode.ts +4 -8
  22. package/src/index.ts +4 -3
  23. package/src/integrations/atlassian.ts +31 -28
  24. package/src/integrations/google-drive.ts +33 -30
  25. package/src/integrations/marketplace-plugin.ts +1 -1
  26. package/src/integrations/provider-oauth.ts +11 -2
  27. package/src/mcp/documents.ts +57 -428
  28. package/src/mcp/files.ts +76 -58
  29. package/src/mcp/first-party-tool-permissions.ts +9 -0
  30. package/src/mcp/knowledge-entries.ts +276 -0
  31. package/src/mcp/server.ts +7 -361
  32. package/src/routes/company-brain.ts +58 -107
  33. package/src/routes/documents.ts +78 -294
  34. package/src/routes/files.ts +68 -3
  35. package/src/routes/host-mcp-resolvers.ts +41 -0
  36. package/src/routes/knowledge.ts +386 -0
  37. package/src/routes/organization-memberships.ts +33 -0
  38. package/src/routes/scheduled-tasks.ts +9 -1
  39. package/src/routes/sessions.ts +20 -3
  40. package/src/routes/skill-content.ts +8 -2
  41. package/src/routes/video-generation.ts +7 -5
  42. package/src/routes/workspace-learning.ts +12 -82
  43. package/src/routes/workspace-state.ts +34 -25
  44. package/src/routes/workspaces.ts +20 -10
  45. package/src/sandbox-file-artifacts.ts +54 -2
  46. package/src/scheduled-task-deletion.ts +3 -2
  47. package/src/workspace-state-projection.ts +18 -148
  48. package/src/workspace-tool-gateway.ts +14 -25
  49. package/dist/mcp/company-brain-governed-writes.d.ts +0 -26
  50. package/dist/mcp/remember.d.ts +0 -28
  51. package/src/mcp/company-brain-governed-writes.ts +0 -271
  52. package/src/mcp/remember.ts +0 -191
@@ -0,0 +1,386 @@
1
+ import { retryWhileMissing } from "@opengeni/storage";
2
+ import { allowedFirstPartyMcpToolsForSession } from "@opengeni/config";
3
+ import {
4
+ type FirstPartyMcpToolName,
5
+ AgentLearningContext,
6
+ AgentLearningOverridePatch,
7
+ KnowledgeEntryListRequest,
8
+ KnowledgeReviewBatchListRequest,
9
+ KnowledgeEntryBatchReviewRequest,
10
+ AgentInstructionReviewRequest,
11
+ KnowledgeEntryReviewRequest,
12
+ KnowledgeEntryRestoreRequest,
13
+ KnowledgeEntrySaveRequest,
14
+ KnowledgeOriginalFileDownload,
15
+ } from "@opengeni/contracts";
16
+ import {
17
+ prepareKnowledgeFile,
18
+ searchKnowledgeEntries,
19
+ hasPermission,
20
+ knowledgeContextForAccess,
21
+ requireAccessGrantAuthorization,
22
+ requireSessionAuthorization,
23
+ requireWorkspaceSettingsGrant,
24
+ type ApiRouteDeps,
25
+ } from "@opengeni/core";
26
+ import {
27
+ getKnowledgeOriginalFile,
28
+ recordAuditEvent,
29
+ archiveKnowledgeEntry,
30
+ getAgentLearningSettings,
31
+ getKnowledgeEntry,
32
+ listAgentLearningOverrides,
33
+ listKnowledgeEntryHistory,
34
+ listKnowledgeReviewBatches,
35
+ nestedPostgresSqlState,
36
+ restoreKnowledgeEntry,
37
+ reviewKnowledgeEntry,
38
+ saveAgentLearningSettings,
39
+ saveKnowledgeEntry,
40
+ reviewKnowledgeEntries,
41
+ reviewAgentInstruction,
42
+ listAgentInstructionReviews,
43
+ } from "@opengeni/db";
44
+ import type { Context, Hono } from "hono";
45
+ import { HTTPException } from "hono/http-exception";
46
+ import { z } from "zod";
47
+ import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
48
+
49
+ const ReadSettings = z
50
+ .object({
51
+ scope: z.enum(["workspace", "personal", "context"]).default("workspace"),
52
+ source: AgentLearningContext.optional(),
53
+ })
54
+ .strict();
55
+ const WriteSettings = ReadSettings.extend({
56
+ scope: z.enum(["workspace", "personal"]).default("workspace"),
57
+ operationId: z.uuid(),
58
+ expectedVersion: z.number().int().nonnegative(),
59
+ settings: AgentLearningOverridePatch,
60
+ });
61
+ const Id = z.uuid();
62
+ function knowledgeHttpError(error: unknown): never {
63
+ if (error instanceof HTTPException) throw error;
64
+ if (error instanceof z.ZodError)
65
+ throw new HTTPException(422, {
66
+ message: error.issues[0]?.message ?? "Invalid Knowledge request",
67
+ });
68
+ const state = nestedPostgresSqlState(error);
69
+ if (state === "40001" || state === "23505")
70
+ throw new HTTPException(409, { message: "This item changed. Refresh it and try again." });
71
+ if (state === "42501")
72
+ throw new HTTPException(403, {
73
+ message: "This Knowledge operation is not available with your access or learning settings.",
74
+ });
75
+ if (state === "22023" || state === "23514")
76
+ throw new HTTPException(422, { message: "Invalid Knowledge operation or reference." });
77
+ throw error;
78
+ }
79
+
80
+ export function registerKnowledgeRoutes(app: Hono, deps: ApiRouteDeps) {
81
+ const base = "/v1/workspaces/:workspaceId/knowledge/entries";
82
+ async function run<T>(
83
+ c: Context,
84
+ write: boolean,
85
+ fn: (context: Awaited<ReturnType<typeof knowledgeContextForAccess>>) => Promise<T>,
86
+ ) {
87
+ try {
88
+ const access = await requireAccessGrantAuthorization(
89
+ c,
90
+ deps,
91
+ c.req.param("workspaceId")!,
92
+ write ? "documents:manage" : "documents:search",
93
+ );
94
+ return await withAccessGrantSessionRlsContext(deps, access.grant, async () => {
95
+ const context = await knowledgeContextForAccess(
96
+ deps,
97
+ access,
98
+ write ? "documents:manage" : "documents:search",
99
+ );
100
+ c.header("cache-control", "private, no-store");
101
+ return fn(context);
102
+ });
103
+ } catch (error) {
104
+ knowledgeHttpError(error);
105
+ }
106
+ }
107
+ app.post("/v1/workspaces/:workspaceId/knowledge/files/:fileId/prepare", (c) =>
108
+ run(c, false, async (context) => {
109
+ const access = await requireAccessGrantAuthorization(
110
+ c,
111
+ deps,
112
+ context.workspaceId,
113
+ "files:read",
114
+ );
115
+ const selected = access.grant.metadata?.firstPartyMcpTools as
116
+ | FirstPartyMcpToolName[]
117
+ | undefined;
118
+ if (
119
+ context.actor.kind !== "agent" ||
120
+ !selected ||
121
+ !allowedFirstPartyMcpToolsForSession(deps.settings, selected).includes(
122
+ "knowledge_retain_file",
123
+ )
124
+ )
125
+ throw new HTTPException(403, {
126
+ message: "Source preparation is unavailable for this task",
127
+ });
128
+ return c.json(await prepareKnowledgeFile(deps, context, Id.parse(c.req.param("fileId"))));
129
+ }),
130
+ );
131
+ app.post(`${base}/:entryId/file/download-url`, (c) =>
132
+ run(c, false, async (context) => {
133
+ const access = await requireAccessGrantAuthorization(
134
+ c,
135
+ deps,
136
+ context.workspaceId,
137
+ "files:read",
138
+ );
139
+ const { revisionId } = z
140
+ .object({ revisionId: z.uuid().optional() })
141
+ .strict()
142
+ .parse(await c.req.json());
143
+ const entryId = Id.parse(c.req.param("entryId"));
144
+ const file = await getKnowledgeOriginalFile(deps.db, context, entryId, revisionId);
145
+ if (!file) throw new HTTPException(404, { message: "Original file unavailable" });
146
+ if (!deps.objectStorage)
147
+ throw new HTTPException(503, { message: "File storage is unavailable" });
148
+ if (
149
+ !(await retryWhileMissing(async () =>
150
+ (await deps.objectStorage!.fileExists(file)) ? true : null,
151
+ ))
152
+ )
153
+ throw new HTTPException(410, { message: "Original file bytes are unavailable" });
154
+ const signed = await deps.objectStorage.createGetUrl({ key: file.objectKey });
155
+ await recordAuditEvent(deps.db, {
156
+ accountId: context.accountId,
157
+ workspaceId: context.workspaceId,
158
+ subjectId: access.grant.subjectId,
159
+ action: "file.signed_url.issued",
160
+ targetType: "knowledge_entry",
161
+ targetId: entryId,
162
+ metadata: { fileId: file.id, kind: "download", expiresAt: signed.expiresAt.toISOString() },
163
+ });
164
+ return c.json(
165
+ KnowledgeOriginalFileDownload.parse({
166
+ url: signed.url,
167
+ expiresAt: signed.expiresAt.toISOString(),
168
+ fileId: file.id,
169
+ filename: file.filename,
170
+ contentType: file.contentType,
171
+ sizeBytes: file.sizeBytes,
172
+ }),
173
+ );
174
+ }),
175
+ );
176
+ app.post(`${base}/search`, (c) =>
177
+ run(c, false, async (context) =>
178
+ c.json(
179
+ await searchKnowledgeEntries(
180
+ deps.db,
181
+ context,
182
+ KnowledgeEntryListRequest.parse(await c.req.json()),
183
+ () => deps.getDocumentServices().embedder,
184
+ ),
185
+ ),
186
+ ),
187
+ );
188
+ app.get("/v1/workspaces/:workspaceId/knowledge/review-groups", (c) =>
189
+ run(c, true, async (context) => {
190
+ const query = c.req.query();
191
+ return c.json(
192
+ await listKnowledgeReviewBatches(
193
+ deps.db,
194
+ context,
195
+ KnowledgeReviewBatchListRequest.parse({
196
+ ...query,
197
+ ...(query.limit ? { limit: Number(query.limit) } : {}),
198
+ }),
199
+ ),
200
+ );
201
+ }),
202
+ );
203
+ app.post(`${base}/review`, (c) =>
204
+ run(c, true, async (context) =>
205
+ c.json(
206
+ await reviewKnowledgeEntries(
207
+ deps.db,
208
+ context,
209
+ KnowledgeEntryBatchReviewRequest.parse(await c.req.json()),
210
+ ),
211
+ ),
212
+ ),
213
+ );
214
+ app.get(base, (c) =>
215
+ run(c, false, async (context) => {
216
+ const query = c.req.query();
217
+ return c.json(
218
+ await searchKnowledgeEntries(
219
+ deps.db,
220
+ context,
221
+ KnowledgeEntryListRequest.parse({
222
+ ...query,
223
+ ...(query.limit ? { limit: Number(query.limit) } : {}),
224
+ }),
225
+ () => deps.getDocumentServices().embedder,
226
+ ),
227
+ );
228
+ }),
229
+ );
230
+ app.post(base, (c) =>
231
+ run(c, true, async (context) => {
232
+ const result = await saveKnowledgeEntry(
233
+ deps.db,
234
+ context,
235
+ KnowledgeEntrySaveRequest.parse(await c.req.json()),
236
+ );
237
+ return c.json(result, result.replayed ? 200 : 201);
238
+ }),
239
+ );
240
+ app.get(`${base}/:entryId/history`, (c) =>
241
+ run(c, false, async (context) =>
242
+ c.json(
243
+ await listKnowledgeEntryHistory(
244
+ deps.db,
245
+ context,
246
+ Id.parse(c.req.param("entryId")),
247
+ c.req.query("beforeRevision")
248
+ ? z.coerce.number().int().positive().parse(c.req.query("beforeRevision"))
249
+ : undefined,
250
+ ),
251
+ ),
252
+ ),
253
+ );
254
+ app.get(`${base}/:entryId`, (c) =>
255
+ run(c, false, async (context) => {
256
+ const options = z
257
+ .object({
258
+ revisionId: Id.optional(),
259
+ view: z.enum(["published", "needs_review", "archived", "rejected"]).optional(),
260
+ })
261
+ .strict()
262
+ .parse(c.req.query());
263
+ const entry = await getKnowledgeEntry(
264
+ deps.db,
265
+ context,
266
+ Id.parse(c.req.param("entryId")),
267
+ options,
268
+ );
269
+ if (!entry) throw new HTTPException(404, { message: "Knowledge entry not found" });
270
+ return c.json(entry);
271
+ }),
272
+ );
273
+ app.post(`${base}/:entryId/review`, (c) =>
274
+ run(c, true, async (context) => {
275
+ const request = KnowledgeEntryReviewRequest.parse({
276
+ ...(await c.req.json()),
277
+ entryId: c.req.param("entryId"),
278
+ });
279
+ return c.json(await reviewKnowledgeEntry(deps.db, context, request));
280
+ }),
281
+ );
282
+ app.post(`${base}/:entryId/restore`, (c) =>
283
+ run(c, true, async (context) => {
284
+ const request = KnowledgeEntryRestoreRequest.parse({
285
+ ...(await c.req.json()),
286
+ entryId: c.req.param("entryId"),
287
+ });
288
+ return c.json(await restoreKnowledgeEntry(deps.db, context, request));
289
+ }),
290
+ );
291
+ app.post(`${base}/:entryId/archive`, (c) =>
292
+ run(c, true, async (context) => {
293
+ const request = z
294
+ .object({ operationId: Id, expectedVersion: z.number().int().positive() })
295
+ .strict()
296
+ .parse(await c.req.json());
297
+ return c.json(
298
+ await archiveKnowledgeEntry(deps.db, context, {
299
+ ...request,
300
+ entryId: Id.parse(c.req.param("entryId")),
301
+ }),
302
+ );
303
+ }),
304
+ );
305
+
306
+ const learning = "/v1/workspaces/:workspaceId/agent-learning";
307
+ async function instructionReviewContext(
308
+ c: Context,
309
+ context: Awaited<ReturnType<typeof knowledgeContextForAccess>>,
310
+ ) {
311
+ if (context.actor.kind !== "human")
312
+ throw new HTTPException(403, {
313
+ message: "Instruction review requires an authenticated person",
314
+ });
315
+ await requireWorkspaceSettingsGrant(c, deps, context.workspaceId);
316
+ context.actor.settingsScopes = ["workspace"];
317
+ return context;
318
+ }
319
+ app.get(`${learning}/instructions/reviews`, (c) =>
320
+ run(c, false, async (context) =>
321
+ c.json(
322
+ await listAgentInstructionReviews(
323
+ deps.db,
324
+ await instructionReviewContext(c, context),
325
+ c.req.query("cursor"),
326
+ ),
327
+ ),
328
+ ),
329
+ );
330
+ app.post(`${learning}/instructions/review`, (c) =>
331
+ run(c, false, async (context) =>
332
+ c.json(
333
+ await reviewAgentInstruction(
334
+ deps.db,
335
+ await instructionReviewContext(c, context),
336
+ AgentInstructionReviewRequest.parse(await c.req.json()),
337
+ ),
338
+ ),
339
+ ),
340
+ );
341
+ app.post(`${learning}/read`, (c) =>
342
+ run(c, false, async (context) => {
343
+ const request = ReadSettings.parse(await c.req.json());
344
+ return c.json(
345
+ await getAgentLearningSettings(deps.db, context, request.scope, request.source),
346
+ );
347
+ }),
348
+ );
349
+ app.get(`${learning}/overrides`, (c) =>
350
+ run(c, false, async (context) => {
351
+ const scope = z
352
+ .enum(["workspace", "personal"])
353
+ .default("workspace")
354
+ .parse(c.req.query("scope"));
355
+ return c.json(await listAgentLearningOverrides(deps.db, context, scope));
356
+ }),
357
+ );
358
+ app.post(learning, (c) =>
359
+ run(c, false, async (context) => {
360
+ const request = WriteSettings.parse(await c.req.json());
361
+ if (context.actor.kind !== "human")
362
+ throw new HTTPException(403, { message: "Agents cannot change learning settings" });
363
+ const access = await requireAccessGrantAuthorization(c, deps, context.workspaceId);
364
+ if (request.source?.kind === "chat") {
365
+ if (!hasPermission(access.grant.permissions, "sessions:control"))
366
+ throw new HTTPException(403, { message: "Chat settings require session control" });
367
+ await requireSessionAuthorization(deps, access.grant, {
368
+ sessionId: request.source.id,
369
+ operation: "session.context.write",
370
+ surface: "http",
371
+ });
372
+ context.actor.settingsScopes = [request.scope];
373
+ } else if (request.source?.kind === "scheduled_task") {
374
+ if (!hasPermission(access.grant.permissions, "scheduled_tasks:manage"))
375
+ throw new HTTPException(403, {
376
+ message: "Scheduled task settings require task management",
377
+ });
378
+ context.actor.settingsScopes = [request.scope];
379
+ } else if (request.scope === "workspace") {
380
+ await requireWorkspaceSettingsGrant(c, deps, context.workspaceId);
381
+ context.actor.settingsScopes = ["workspace"];
382
+ }
383
+ return c.json(await saveAgentLearningSettings(deps.db, context, request));
384
+ }),
385
+ );
386
+ }
@@ -1,3 +1,7 @@
1
+ import {
2
+ lookupExternalIdentityForRequest,
3
+ cancelExternalWorkspaceMemberGrantForRequest,
4
+ } from "@opengeni/core";
1
5
  import {
2
6
  AcceptOrganizationInvitationRequest,
3
7
  CreateAdditionalOrganizationRequest,
@@ -165,6 +169,22 @@ function rethrowMembershipError(error: unknown, resourceLimitMessage?: string):
165
169
  }
166
170
 
167
171
  export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDeps): void {
172
+ // POST carries an opaque identity reference; this lookup never provisions.
173
+ app.post("/v1/organizations/:organizationId/external-identities/lookup", async (context) => {
174
+ const organizationId = parseId(
175
+ OrganizationId,
176
+ context.req.param("organizationId"),
177
+ "organization id",
178
+ );
179
+ return context.json(
180
+ await lookupExternalIdentityForRequest(
181
+ context,
182
+ deps,
183
+ organizationId,
184
+ await context.req.json().catch(() => null),
185
+ ),
186
+ );
187
+ });
168
188
  app.patch("/v1/organizations/:organizationId/external-members/:membershipId", async (context) => {
169
189
  const organizationId = parseId(
170
190
  OrganizationId,
@@ -461,6 +481,19 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
461
481
  app.post(
462
482
  "/v1/organizations/:organizationId/workspaces/:workspaceId/members/:membershipId/revoke",
463
483
  async (context) => {
484
+ const input: unknown = await context.req.json().catch(() => null);
485
+ if (input && typeof input === "object" && "cancelGrantOperationId" in input) {
486
+ return context.json(
487
+ await cancelExternalWorkspaceMemberGrantForRequest(
488
+ context,
489
+ deps,
490
+ parseId(OrganizationId, context.req.param("organizationId"), "organization id"),
491
+ parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id"),
492
+ parseId(MembershipId, context.req.param("membershipId"), "membership id"),
493
+ input,
494
+ ),
495
+ );
496
+ }
464
497
  const { subjectId } = await requireManagedHuman(context, deps);
465
498
  const organizationId = parseId(
466
499
  OrganizationId,
@@ -150,7 +150,15 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
150
150
  sessionAuthorization: deps.sessionAuthorization,
151
151
  authorizationSurface: "http",
152
152
  });
153
- const task = await updateScheduledTaskForApi(db, workspaceId, taskId, update);
153
+ const task = await updateScheduledTaskForApi(
154
+ db,
155
+ workspaceId,
156
+ taskId,
157
+ update,
158
+ payload.agentLearning
159
+ ? { authorization, request: payload.agentLearning, restoreState: previous }
160
+ : undefined,
161
+ );
154
162
  await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
155
163
  return c.json(scheduledTaskForGrant(task, grant));
156
164
  });
@@ -599,13 +599,24 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
599
599
 
600
600
  app.get("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
601
601
  const workspaceId = c.req.param("workspaceId");
602
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
602
+ const authorization = await requireAccessGrantAuthorization(
603
+ c,
604
+ deps,
605
+ workspaceId,
606
+ "sessions:read",
607
+ );
608
+ const grant = authorization.grant;
603
609
  const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
604
610
  accountId: grant.accountId,
605
611
  workspaceId,
606
612
  });
607
613
  return c.json(
608
- await getActorNewSessionDraft({ settings: catalog.settings, db }, grant, workspaceId),
614
+ await getActorNewSessionDraft(
615
+ { settings: catalog.settings, db },
616
+ grant,
617
+ workspaceId,
618
+ authorization,
619
+ ),
609
620
  );
610
621
  });
611
622
 
@@ -3999,12 +4010,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3999
4010
 
4000
4011
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/publish", async (c) => {
4001
4012
  const workspaceId = c.req.param("workspaceId");
4002
- await requireAccessGrant(c, deps, workspaceId, "files:upload");
4013
+ const authorization = await requireAccessGrantAuthorization(
4014
+ c,
4015
+ deps,
4016
+ workspaceId,
4017
+ "files:upload",
4018
+ );
4003
4019
  const ctx = await channelAPreamble(c, "files:read", "artifact.publish");
4004
4020
  const request = await parseChannelABody(c, PublishSandboxFileArtifactRequest);
4005
4021
  return c.json(
4006
4022
  await publishSandboxFileArtifact(deps, {
4007
4023
  grant: ctx.grant,
4024
+ authorization,
4008
4025
  session: ctx.session,
4009
4026
  path: request.path,
4010
4027
  signal: ctx.waitSignal,
@@ -4,6 +4,7 @@ import { HTTPException } from "hono/http-exception";
4
4
  import { SkillFile, type SkillWriteReceipt } from "@opengeni/contracts";
5
5
  import {
6
6
  approveSkill,
7
+ rejectSkill,
7
8
  listSkills,
8
9
  readSkill,
9
10
  requireAccessGrant,
@@ -168,7 +169,7 @@ export function registerSkillContentRoutes(app: Hono, deps: ApiRouteDeps): void
168
169
  );
169
170
  return c.json(receipt);
170
171
  });
171
- for (const operation of ["approve", "restore"] as const) {
172
+ for (const operation of ["approve", "reject", "restore"] as const) {
172
173
  app.post(`${base}/:skillId/${operation}`, async (c) => {
173
174
  const workspaceId = c.req.param("workspaceId");
174
175
  const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
@@ -181,7 +182,12 @@ export function registerSkillContentRoutes(app: Hono, deps: ApiRouteDeps): void
181
182
  );
182
183
  if (!current) throw new HTTPException(404, { message: "Skill not found" });
183
184
  authorizePreferenceRegistryScopeMutation(access, current.scope);
184
- const apply = operation === "approve" ? approveSkill : restoreSkill;
185
+ const apply =
186
+ operation === "approve"
187
+ ? approveSkill
188
+ : operation === "reject"
189
+ ? rejectSkill
190
+ : restoreSkill;
185
191
  return c.json(
186
192
  await skillMutation(() =>
187
193
  apply(deps.db, {
@@ -6,6 +6,7 @@ import {
6
6
  } from "@opengeni/contracts";
7
7
  import {
8
8
  getVideoGenerationOperationSummary,
9
+ withSessionRlsActorContext,
9
10
  getWorkspaceVercelAiGatewayConnectionMetadata,
10
11
  getWorkspaceVideoGenerationPolicy,
11
12
  updateWorkspaceVideoGenerationPolicy,
@@ -14,6 +15,8 @@ import {
14
15
  } from "@opengeni/db";
15
16
  import {
16
17
  requireAccessGrant,
18
+ requireAccessGrantAuthorization,
19
+ fileOwnerContextForAccess,
17
20
  requireWorkspaceSettingsGrant,
18
21
  VIDEO_GENERATION_MODEL_CATALOG,
19
22
  videoGenerationModelSupportsFundingSource,
@@ -108,11 +111,10 @@ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): vo
108
111
 
109
112
  app.get("/v1/workspaces/:workspaceId/video-generation/operations/:operationId", async (c) => {
110
113
  const workspaceId = c.req.param("workspaceId");
111
- await requireAccessGrant(c, deps, workspaceId, "workspace:read");
112
- const summary = await getVideoGenerationOperationSummary(
113
- deps.db,
114
- workspaceId,
115
- c.req.param("operationId"),
114
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
115
+ const actor = await fileOwnerContextForAccess(deps, access, "workspace:read");
116
+ const summary = await withSessionRlsActorContext(actor, () =>
117
+ getVideoGenerationOperationSummary(deps.db, workspaceId, c.req.param("operationId")),
116
118
  );
117
119
  if (!summary)
118
120
  throw new HTTPException(404, {
@@ -1,13 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import {
3
- ActivateWorkspaceLearningPolicyRevisionRequest,
4
- CreateWorkspaceLearningPolicyRevisionRequest,
5
- RollbackWorkspaceLearningPolicyRevisionRequest,
6
3
  UndoGovernedLearningActivationHttpRequest,
7
4
  WorkspaceLearningHistoryResponse,
8
5
  WorkspaceLearningPolicyHistoryQuery,
9
- WorkspaceLearningPolicyMutationResponse,
10
- WorkspaceLearningPolicyRevision,
11
6
  } from "@opengeni/contracts";
12
7
  import {
13
8
  publishGovernedLearningEventToSlack,
@@ -16,8 +11,6 @@ import {
16
11
  type ApiRouteDeps,
17
12
  } from "@opengeni/core";
18
13
  import {
19
- activateWorkspaceLearningPolicyRevision,
20
- createWorkspaceLearningPolicyRevision,
21
14
  GovernedLearningActivationAuthorityError,
22
15
  GovernedLearningActivationConflictError,
23
16
  GovernedLearningActivationInvalidOperationError,
@@ -25,7 +18,6 @@ import {
25
18
  listGovernedLearningActivationHistory,
26
19
  listGovernedLearningDecisionReceipts,
27
20
  listWorkspaceLearningPolicyHistory,
28
- rollbackWorkspaceLearningPolicyRevision,
29
21
  undoGovernedLearningActivation,
30
22
  WorkspaceLearningPolicyAuthorityError,
31
23
  WorkspaceLearningPolicyConflictError,
@@ -121,82 +113,20 @@ export function registerWorkspaceLearningRoutes(app: Hono, deps: ApiRouteDeps):
121
113
  }
122
114
  });
123
115
 
124
- app.post(`${base}/revisions`, async (context) => {
125
- const workspaceId = context.req.param("workspaceId");
126
- const grant = await requireWorkspaceSettingsGrant(context, deps, workspaceId);
127
- const request = await parseBody(context, CreateWorkspaceLearningPolicyRevisionRequest);
128
- try {
129
- return context.json(
130
- WorkspaceLearningPolicyRevision.parse(
131
- await createWorkspaceLearningPolicyRevision(deps.db, {
132
- operationId: request.operationId ?? randomUUID(),
133
- accountId: grant.accountId,
134
- workspaceId,
135
- workspaceMode: request.workspaceMode,
136
- sourceOverrides: request.sourceOverrides,
137
- supersedesRevisionId: request.supersedesRevisionId,
138
- actorSubjectId: grant.subjectId,
139
- principalKind: grant.principalKind,
140
- }),
141
- ),
142
- 201,
143
- );
144
- } catch (error) {
145
- learningError(error);
146
- }
147
- });
148
-
149
- app.post(`${base}/revisions/:revisionId/activate`, async (context) => {
150
- const workspaceId = context.req.param("workspaceId");
151
- const revisionId = z.string().uuid().safeParse(context.req.param("revisionId"));
152
- if (!revisionId.success) throw new HTTPException(422, { message: "Invalid revision id" });
153
- const grant = await requireWorkspaceSettingsGrant(context, deps, workspaceId);
154
- const request = await parseBody(context, ActivateWorkspaceLearningPolicyRevisionRequest);
155
- try {
156
- return context.json(
157
- WorkspaceLearningPolicyMutationResponse.parse(
158
- await activateWorkspaceLearningPolicyRevision(deps.db, {
159
- operationId: request.operationId ?? randomUUID(),
160
- accountId: grant.accountId,
161
- workspaceId,
162
- revisionId: revisionId.data,
163
- expectedCurrentRevisionId: request.expectedCurrentRevisionId,
164
- expectedActivationVersion: request.expectedActivationVersion,
165
- actorSubjectId: grant.subjectId,
166
- principalKind: grant.principalKind,
167
- reason: request.reason,
168
- }),
169
- ),
170
- );
171
- } catch (error) {
172
- learningError(error);
173
- }
174
- });
175
-
176
- app.post(`${base}/rollback`, async (context) => {
177
- const workspaceId = context.req.param("workspaceId");
178
- const grant = await requireWorkspaceSettingsGrant(context, deps, workspaceId);
179
- const request = await parseBody(context, RollbackWorkspaceLearningPolicyRevisionRequest);
180
- try {
116
+ for (const path of ["/revisions", "/revisions/:revisionId/activate", "/rollback"]) {
117
+ app.post(`${base}${path}`, async (context) => {
118
+ await requireWorkspaceSettingsGrant(context, deps, context.req.param("workspaceId")!);
181
119
  return context.json(
182
- WorkspaceLearningPolicyMutationResponse.parse(
183
- await rollbackWorkspaceLearningPolicyRevision(deps.db, {
184
- operationId: request.operationId ?? randomUUID(),
185
- accountId: grant.accountId,
186
- workspaceId,
187
- targetRevisionId: request.targetRevisionId,
188
- expectedCurrentRevisionId: request.expectedCurrentRevisionId,
189
- expectedActivationVersion: request.expectedActivationVersion,
190
- actorSubjectId: grant.subjectId,
191
- principalKind: grant.principalKind,
192
- reason: request.reason,
193
- }),
194
- ),
120
+ {
121
+ error: {
122
+ code: "learning_settings_replaced",
123
+ message: "Configure Knowledge, instructions and Skills in Agent learning settings.",
124
+ },
125
+ },
126
+ 410,
195
127
  );
196
- } catch (error) {
197
- learningError(error);
198
- }
199
- });
128
+ });
129
+ }
200
130
 
201
131
  app.post(`${base}/activations/:activationReceiptId/undo`, async (context) => {
202
132
  const workspaceId = context.req.param("workspaceId");