@opengeni/api-router 3.0.1 → 4.0.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.
Files changed (96) hide show
  1. package/dist/admission-trace.d.ts +8 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  4. package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
  5. package/dist/auth/managed-auth.d.ts +1 -0
  6. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  7. package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
  8. package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
  9. package/dist/connection-ownership.d.ts +1 -2
  10. package/dist/fatal-process-boundary.d.ts +1 -1
  11. package/dist/index.js +24 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/atlassian.d.ts +8 -3
  14. package/dist/integrations/fiken.d.ts +3 -1
  15. package/dist/integrations/github-lens-connect.d.ts +5 -65
  16. package/dist/integrations/google-drive.d.ts +2 -0
  17. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  18. package/dist/integrations/oauth-client.d.ts +119 -11
  19. package/dist/integrations/oauth-profiles.d.ts +8 -13
  20. package/dist/integrations/slack-interactions.d.ts +1 -1
  21. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  22. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  23. package/dist/mcp-oauth.d.ts +22 -1
  24. package/dist/routes/api-integrations.d.ts +1 -0
  25. package/dist/routes/codex.d.ts +28 -0
  26. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  27. package/dist/routes/organization-integration-policy.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +24 -2
  29. package/dist/scheduled-task-deletion.d.ts +3 -4
  30. package/dist/workspace-tool-gateway.d.ts +3 -1
  31. package/package.json +15 -15
  32. package/src/admission-trace.ts +14 -0
  33. package/src/app.ts +141 -94
  34. package/src/auth/managed-auth-attempt-context.ts +9 -0
  35. package/src/auth/managed-auth-session-adapter.ts +11 -0
  36. package/src/auth/managed-auth.ts +39 -2
  37. package/src/auth/managed-sign-in-notifications.ts +81 -0
  38. package/src/connection-ownership.ts +2 -9
  39. package/src/editable-artifact-production.ts +6 -0
  40. package/src/fatal-process-boundary.ts +17 -2
  41. package/src/index.ts +3 -4
  42. package/src/integrations/atlassian.ts +184 -76
  43. package/src/integrations/fiken.ts +57 -35
  44. package/src/integrations/github-app-connect.ts +51 -15
  45. package/src/integrations/github-lens-connect.ts +7 -28
  46. package/src/integrations/google-drive.ts +296 -125
  47. package/src/integrations/integration-commit-authority.ts +69 -0
  48. package/src/integrations/oauth-client.ts +231 -132
  49. package/src/integrations/oauth-profiles.ts +10 -45
  50. package/src/integrations/personal-github.ts +55 -31
  51. package/src/integrations/provider-oauth.ts +57 -82
  52. package/src/integrations/social-oauth.ts +83 -56
  53. package/src/mcp/capability-account-readiness.ts +17 -0
  54. package/src/mcp/editable-artifacts.ts +7 -2
  55. package/src/mcp/first-party-tool-permissions.ts +1 -0
  56. package/src/mcp/knowledge-entries.ts +25 -8
  57. package/src/mcp/server.ts +135 -43
  58. package/src/mcp-oauth.ts +336 -34
  59. package/src/routes/api-integrations.ts +116 -4
  60. package/src/routes/automations.ts +5 -13
  61. package/src/routes/billing.ts +52 -0
  62. package/src/routes/canonical-human-identities.ts +61 -0
  63. package/src/routes/capabilities.ts +51 -1
  64. package/src/routes/codex.ts +4 -2
  65. package/src/routes/connect.ts +90 -31
  66. package/src/routes/connections.ts +487 -307
  67. package/src/routes/files.ts +21 -12
  68. package/src/routes/github.ts +48 -19
  69. package/src/routes/insights.ts +17 -7
  70. package/src/routes/integration-facets.ts +66 -25
  71. package/src/routes/knowledge.ts +48 -1
  72. package/src/routes/managed-sign-in-methods.ts +433 -0
  73. package/src/routes/organization-integration-policy.ts +113 -0
  74. package/src/routes/organization-memberships.ts +22 -2
  75. package/src/routes/plugins.ts +42 -4
  76. package/src/routes/pr-review-github.ts +67 -57
  77. package/src/routes/pr-review.ts +74 -71
  78. package/src/routes/rigs.ts +32 -13
  79. package/src/routes/scheduled-tasks.ts +6 -6
  80. package/src/routes/sessions.ts +233 -11
  81. package/src/routes/skill-content.ts +19 -3
  82. package/src/routes/social.ts +24 -13
  83. package/src/sandbox/channel-a.ts +7 -1
  84. package/src/sandbox/rematerialize.ts +2 -1
  85. package/src/scheduled-task-deletion.ts +13 -3
  86. package/src/workspace-tool-gateway.ts +31 -31
  87. package/dist/connection-authority-owner.d.ts +0 -59
  88. package/dist/routes/connection-authorities.d.ts +0 -3
  89. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  90. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  91. package/dist/routes/packs.d.ts +0 -3
  92. package/src/connection-authority-owner.ts +0 -61
  93. package/src/routes/connection-authorities.ts +0 -139
  94. package/src/routes/host-mcp-bindings.ts +0 -122
  95. package/src/routes/host-mcp-resolvers.ts +0 -41
  96. package/src/routes/packs.ts +0 -784
@@ -1,3 +1,4 @@
1
+ import { readSessionAttachmentFiles } from "@opengeni/core";
1
2
  import { ZodError } from "zod";
2
3
  import {
3
4
  CompleteFileUploadResponse,
@@ -405,15 +406,28 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
405
406
  );
406
407
  });
407
408
 
408
- app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => {
409
- const workspaceId = c.req.param("workspaceId");
410
- const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
411
- const file = await requireFileForSubject(db, {
409
+ const readRequestedFile = async (
410
+ c: Context,
411
+ grant: Awaited<ReturnType<typeof requireAccessGrant>>,
412
+ ) => {
413
+ const fileId = c.req.param("fileId") ?? "";
414
+ const sessionId = c.req.query("sessionId");
415
+ if (sessionId) {
416
+ if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/iu.test(sessionId)) return null;
417
+ return (await readSessionAttachmentFiles(deps, grant, sessionId, [fileId]))[0] ?? null;
418
+ }
419
+ return requireFileForSubject(db, {
412
420
  accountId: grant.accountId,
413
- workspaceId,
421
+ workspaceId: grant.workspaceId,
414
422
  subjectId: grant.subjectId,
415
- fileId: c.req.param("fileId"),
423
+ fileId,
416
424
  }).catch(() => null);
425
+ };
426
+
427
+ app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => {
428
+ const workspaceId = c.req.param("workspaceId");
429
+ const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
430
+ const file = await readRequestedFile(c, grant).catch(() => null);
417
431
  if (!file) {
418
432
  throw new HTTPException(404, { message: "file not found" });
419
433
  }
@@ -589,12 +603,7 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
589
603
  message: "object storage is not configured",
590
604
  });
591
605
  }
592
- const file = await requireFileForSubject(db, {
593
- accountId: grant.accountId,
594
- workspaceId,
595
- subjectId: grant.subjectId,
596
- fileId: c.req.param("fileId"),
597
- }).catch(() => null);
606
+ const file = await readRequestedFile(c, grant).catch(() => null);
598
607
  if (!file) {
599
608
  throw new HTTPException(404, { message: "file not found" });
600
609
  }
@@ -7,6 +7,7 @@ import {
7
7
  type GitHubInstallationBindingCandidate,
8
8
  } from "@opengeni/contracts";
9
9
  import { PersonalGitHubConnectionMetadata } from "@opengeni/contracts/personal-github";
10
+ import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
10
11
  import {
11
12
  ListGitHubRepositoryBranchesQuery,
12
13
  VerifyPublicGitHubRepositoryRefRequest,
@@ -72,6 +73,10 @@ import {
72
73
  requireLegacyOAuthActor,
73
74
  } from "../connection-ownership";
74
75
  import { listPersonalGitHubConnections } from "../integrations/personal-github";
76
+ import {
77
+ integrationCommitGrant,
78
+ type IntegrationCommitGrant,
79
+ } from "../integrations/integration-commit-authority";
75
80
  import {
76
81
  isConsistentGitHubBindingCandidates,
77
82
  isConsistentGitHubBindingProof,
@@ -598,6 +603,7 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
598
603
  message: "GitHub OAuth state does not match this workspace",
599
604
  });
600
605
  }
606
+ await withOrganizationIntegrationAcquisition(db, grant, ["github-app"], async () => {});
601
607
  let candidates: GitHubInstallationBindingCandidate[] | null;
602
608
  try {
603
609
  candidates = deps.githubAppApi?.discoverInstallationBindingCandidates
@@ -652,6 +658,7 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
652
658
  message: "GitHub OAuth state does not match this workspace",
653
659
  });
654
660
  }
661
+ await withOrganizationIntegrationAcquisition(db, grant, ["github-app"], async () => {});
655
662
  let proof;
656
663
  try {
657
664
  proof = deps.githubAppApi?.authorizeInstallationBinding
@@ -681,22 +688,30 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
681
688
  const expiresAt = new Date((statePayload.iat + githubBindingStateMaxAgeSeconds) * 1_000);
682
689
  let bound;
683
690
  try {
684
- bound = await bindAuthorizedGitHubInstallationRepositories(db, {
685
- accountId: grant.accountId,
686
- workspaceId: grant.workspaceId,
687
- installationId,
688
- githubAccountId: proof.installation.accountId,
689
- accountLogin: proof.installation.accountLogin,
690
- accountType: proof.installation.accountType,
691
- linkedBySubjectId: grant.subjectId,
692
- githubActorId: proof.actorId,
693
- githubActorLogin: proof.actorLogin,
694
- authorityKind: proof.authorityKind,
695
- authorityCheckedAt,
696
- authorityExpiresAt: expiresAt,
697
- authorityNonce: statePayload.nonce,
698
- repositoryIds,
699
- });
691
+ bound = await withOrganizationIntegrationAcquisition(
692
+ db,
693
+ grant,
694
+ ["github-app"],
695
+ async (tx) => {
696
+ await grant.authorizeCommit(tx);
697
+ return bindAuthorizedGitHubInstallationRepositories(tx, {
698
+ accountId: grant.accountId,
699
+ workspaceId: grant.workspaceId,
700
+ installationId,
701
+ githubAccountId: proof.installation.accountId,
702
+ accountLogin: proof.installation.accountLogin,
703
+ accountType: proof.installation.accountType,
704
+ linkedBySubjectId: grant.subjectId,
705
+ githubActorId: proof.actorId,
706
+ githubActorLogin: proof.actorLogin,
707
+ authorityKind: proof.authorityKind,
708
+ authorityCheckedAt,
709
+ authorityExpiresAt: expiresAt,
710
+ authorityNonce: statePayload.nonce,
711
+ repositoryIds,
712
+ });
713
+ },
714
+ );
700
715
  } catch (error) {
701
716
  if (error instanceof GitHubInstallationAuthorityCommitError) {
702
717
  throw new HTTPException(409, { message: error.message });
@@ -913,18 +928,32 @@ async function requireGitHubManageGrant(
913
928
  deps: ApiRouteDeps,
914
929
  workspaceId: string,
915
930
  expectedState: GitHubSignedStatePayload,
916
- ): Promise<AccessGrant> {
931
+ ): Promise<IntegrationCommitGrant> {
917
932
  try {
918
933
  const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "github:manage");
919
934
  requireLegacyOAuthActor(access);
920
- return access.grant;
935
+ return integrationCommitGrant(access, ["github:manage"], {
936
+ settings: deps.settings,
937
+ authorizationHeader: c.req.header("authorization"),
938
+ });
921
939
  } catch (error) {
922
940
  if (!(error instanceof HTTPException) || error.status !== 401) {
923
941
  throw error;
924
942
  }
925
943
  const grant = githubBrowserGrantFromState(deps.settings, expectedState, workspaceId);
926
944
  if (grant) {
927
- return grant;
945
+ return {
946
+ ...grant,
947
+ authorizeCommit: async () => {
948
+ const current = githubBrowserGrantFromState(deps.settings, expectedState, workspaceId);
949
+ if (
950
+ !current ||
951
+ current.accountId !== grant.accountId ||
952
+ current.subjectId !== grant.subjectId
953
+ )
954
+ throw new HTTPException(403, { message: "GitHub browser handoff expired or changed" });
955
+ },
956
+ };
928
957
  }
929
958
  throw error;
930
959
  }
@@ -1,6 +1,8 @@
1
1
  import { InsightsRange, WorkspaceInsightsResponse } from "@opengeni/contracts";
2
2
  import {
3
3
  getWorkspaceInsights,
4
+ measureInsightsPhase,
5
+ workspaceInsightsPhaseMetricObserver,
4
6
  normalizeWorkspaceInsightsFilter,
5
7
  requireAccessGrant,
6
8
  WorkspaceInsightsFilterValidationError,
@@ -27,6 +29,7 @@ export function normalizeWorkspaceInsightsQueryFilter(
27
29
 
28
30
  export function registerInsightsRoutes(app: Hono, deps: ApiRouteDeps): void {
29
31
  const observeRequest = workspaceInsightsMetricObserver(deps.observability);
32
+ const observePhase = workspaceInsightsPhaseMetricObserver(deps.observability);
30
33
  app.get("/v1/workspaces/:workspaceId/insights", async (c) => {
31
34
  const startedAtMs = performance.now();
32
35
  const rangeRaw = c.req.query("range") ?? "week";
@@ -38,7 +41,9 @@ export function registerInsightsRoutes(app: Hono, deps: ApiRouteDeps): void {
38
41
 
39
42
  try {
40
43
  const workspaceId = c.req.param("workspaceId");
41
- await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
44
+ await measureInsightsPhase(observePhase, "auth", () =>
45
+ requireAccessGrant(c, deps, workspaceId, "workspace:admin"),
46
+ );
42
47
 
43
48
  const rangeParsed = InsightsRange.safeParse(rangeRaw);
44
49
  if (!rangeParsed.success) {
@@ -49,12 +54,17 @@ export function registerInsightsRoutes(app: Hono, deps: ApiRouteDeps): void {
49
54
  provider = normalizeWorkspaceInsightsQueryFilter(providerRaw, "provider");
50
55
  model = normalizeWorkspaceInsightsQueryFilter(modelRaw, "model");
51
56
 
52
- const response = await getWorkspaceInsights(deps.db, deps.settings, {
53
- workspaceId,
54
- range: rangeParsed.data,
55
- provider,
56
- model,
57
- });
57
+ const response = await getWorkspaceInsights(
58
+ deps.db,
59
+ deps.settings,
60
+ {
61
+ workspaceId,
62
+ range: rangeParsed.data,
63
+ provider,
64
+ model,
65
+ },
66
+ observePhase,
67
+ );
58
68
  c.header("cache-control", "private, no-store");
59
69
  const result = c.json(WorkspaceInsightsResponse.parse(response));
60
70
  outcome = "completed";
@@ -1,10 +1,13 @@
1
1
  import {
2
+ assertOrganizationIntegrationAllowed,
2
3
  IntegrationFacetMutationResult,
3
4
  IntegrationFacetRemovalResult,
4
5
  IntegrationInstanceFacetsResponse,
5
6
  MutateIntegrationFacetRequest,
6
7
  UpsertIntegrationFacetRequest,
7
8
  } from "@opengeni/contracts";
9
+ import { withOrganizationIntegrationPolicyFence } from "@opengeni/db/organization-integration-policy";
10
+ import { CORE_INTEGRATION_DEFINITIONS } from "@opengeni/capabilities";
8
11
  import {
9
12
  hasPermission,
10
13
  requireAccessGrant,
@@ -25,6 +28,7 @@ import {
25
28
  removeIntegrationFacet,
26
29
  replayCompletedIntegrationFacetOperation,
27
30
  setIntegrationFacetLifecycle,
31
+ type IntegrationFacetAcquisitionAuthorizer,
28
32
  } from "@opengeni/db";
29
33
  import type { Hono } from "hono";
30
34
  import { HTTPException } from "hono/http-exception";
@@ -179,20 +183,30 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
179
183
  }
180
184
  return c.json(
181
185
  IntegrationFacetMutationResult.parse(
182
- await configureIntegrationFacet(deps.db, {
183
- accountId: grant.accountId,
184
- workspaceId,
185
- subjectId: grant.subjectId,
186
- capabilityId,
187
- instanceKey,
188
- facetKey,
189
- displayName: payload.displayName,
190
- config: payload.config,
191
- ...(payload.expectedVersion !== undefined
192
- ? { expectedVersion: payload.expectedVersion }
193
- : {}),
194
- idempotencyKey: payload.idempotencyKey,
195
- }),
186
+ await withOrganizationIntegrationPolicyFence(
187
+ deps.db,
188
+ { accountId: grant.accountId, workspaceId },
189
+ (tx, policy) =>
190
+ configureIntegrationFacet(tx, {
191
+ accountId: grant.accountId,
192
+ workspaceId,
193
+ subjectId: grant.subjectId,
194
+ capabilityId,
195
+ instanceKey,
196
+ facetKey,
197
+ displayName: payload.displayName,
198
+ config: payload.config,
199
+ ...(payload.expectedVersion !== undefined
200
+ ? { expectedVersion: payload.expectedVersion }
201
+ : {}),
202
+ idempotencyKey: payload.idempotencyKey,
203
+ beforeAcquire: async (_db, context) =>
204
+ assertOrganizationIntegrationAllowed(
205
+ policy,
206
+ installedFacetIntegrationKey(context),
207
+ ),
208
+ }),
209
+ ),
196
210
  ),
197
211
  payload.expectedVersion === undefined ? 201 : 200,
198
212
  );
@@ -212,17 +226,27 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
212
226
  try {
213
227
  return c.json(
214
228
  IntegrationFacetMutationResult.parse(
215
- await setIntegrationFacetLifecycle(deps.db, {
216
- accountId: grant.accountId,
217
- workspaceId,
218
- subjectId: grant.subjectId,
219
- capabilityId: decoded(c.req.param("capabilityId")),
220
- instanceKey: decoded(c.req.param("instanceKey")),
221
- facetKey: decoded(c.req.param("facetKey")),
222
- action,
223
- expectedVersion: payload.expectedVersion,
224
- idempotencyKey: payload.idempotencyKey,
225
- }),
229
+ await withOrganizationIntegrationPolicyFence(
230
+ deps.db,
231
+ { accountId: grant.accountId, workspaceId },
232
+ (tx, policy) =>
233
+ setIntegrationFacetLifecycle(tx, {
234
+ accountId: grant.accountId,
235
+ workspaceId,
236
+ subjectId: grant.subjectId,
237
+ capabilityId: decoded(c.req.param("capabilityId")),
238
+ instanceKey: decoded(c.req.param("instanceKey")),
239
+ facetKey: decoded(c.req.param("facetKey")),
240
+ action,
241
+ expectedVersion: payload.expectedVersion,
242
+ idempotencyKey: payload.idempotencyKey,
243
+ beforeAcquire: async (_db, context) =>
244
+ assertOrganizationIntegrationAllowed(
245
+ policy,
246
+ installedFacetIntegrationKey(context),
247
+ ),
248
+ }),
249
+ ),
226
250
  ),
227
251
  );
228
252
  } catch (error) {
@@ -264,6 +288,23 @@ function decoded(value: string): string {
264
288
  return decodeURIComponent(value);
265
289
  }
266
290
 
291
+ /** The DB context comes from the matching installed API facet and immutable
292
+ * server-authored version manifest, never request metadata or a provider domain.
293
+ */
294
+ function installedFacetIntegrationKey(
295
+ context: Parameters<IntegrationFacetAcquisitionAuthorizer>[1],
296
+ ): string | null {
297
+ if (context.apiProtocol !== "openapi" && context.apiProtocol !== "graphql") return null;
298
+ if (context.definitionProvenance === "workspace") return `custom:${context.apiProtocol}`;
299
+ if (
300
+ context.definitionProvenance === "curated" &&
301
+ CORE_INTEGRATION_DEFINITIONS.some((definition) => definition.id === context.definitionId)
302
+ ) {
303
+ return context.definitionId;
304
+ }
305
+ return null;
306
+ }
307
+
267
308
  function facetHttpError(error: unknown): HTTPException {
268
309
  if (error instanceof HTTPException) return error;
269
310
  if (error instanceof IntegrationFacetNotFoundError) {
@@ -5,6 +5,7 @@ import {
5
5
  AgentLearningContext,
6
6
  AgentLearningOverridePatch,
7
7
  KnowledgeEntryListRequest,
8
+ KnowledgeSavePreparationRequest,
8
9
  KnowledgeReviewBatchListRequest,
9
10
  KnowledgeEntryBatchReviewRequest,
10
11
  AgentInstructionReviewRequest,
@@ -15,6 +16,7 @@ import {
15
16
  } from "@opengeni/contracts";
16
17
  import {
17
18
  prepareKnowledgeFile,
19
+ prepareKnowledgeSave,
18
20
  searchKnowledgeEntries,
19
21
  hasPermission,
20
22
  knowledgeContextForAccess,
@@ -125,7 +127,20 @@ export function registerKnowledgeRoutes(app: Hono, deps: ApiRouteDeps) {
125
127
  throw new HTTPException(403, {
126
128
  message: "Source preparation is unavailable for this task",
127
129
  });
128
- return c.json(await prepareKnowledgeFile(deps, context, Id.parse(c.req.param("fileId"))));
130
+ const body = await c.req.text();
131
+ let input: unknown = {};
132
+ try {
133
+ input = body ? JSON.parse(body) : {};
134
+ } catch {
135
+ throw new HTTPException(400, { message: "Invalid source preparation JSON" });
136
+ }
137
+ const { purpose } = z
138
+ .object({ purpose: z.enum(["evidence", "reference"]).default("evidence") })
139
+ .strict()
140
+ .parse(input);
141
+ return c.json(
142
+ await prepareKnowledgeFile(deps, context, Id.parse(c.req.param("fileId")), purpose),
143
+ );
129
144
  }),
130
145
  );
131
146
  app.post(`${base}/:entryId/file/download-url`, (c) =>
@@ -173,6 +188,38 @@ export function registerKnowledgeRoutes(app: Hono, deps: ApiRouteDeps) {
173
188
  );
174
189
  }),
175
190
  );
191
+ app.post(`${base}/prepare-save`, (c) =>
192
+ run(c, false, async (context) => {
193
+ if (context.actor.kind === "agent") {
194
+ const access = await requireAccessGrantAuthorization(
195
+ c,
196
+ deps,
197
+ context.workspaceId,
198
+ "documents:search",
199
+ );
200
+ const selected = access.grant.metadata?.firstPartyMcpTools as
201
+ | FirstPartyMcpToolName[]
202
+ | undefined;
203
+ if (
204
+ !selected ||
205
+ !allowedFirstPartyMcpToolsForSession(deps.settings, selected).includes(
206
+ "knowledge_prepare_save",
207
+ )
208
+ )
209
+ throw new HTTPException(403, {
210
+ message: "Knowledge preparation is unavailable for this task",
211
+ });
212
+ }
213
+ return c.json(
214
+ await prepareKnowledgeSave(
215
+ deps.db,
216
+ context,
217
+ KnowledgeSavePreparationRequest.parse(await c.req.json()),
218
+ () => deps.getDocumentServices().embedder,
219
+ ),
220
+ );
221
+ }),
222
+ );
176
223
  app.post(`${base}/search`, (c) =>
177
224
  run(c, false, async (context) =>
178
225
  c.json(