@opengeni/api-router 0.17.0 → 0.21.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.
@@ -7,25 +7,33 @@ import {
7
7
  GOOGLE_DRIVE_READONLY_SCOPE,
8
8
  GoogleDriveBrowseItem,
9
9
  GoogleDriveBrowseResponse,
10
+ GoogleDriveConnectionLifecycle,
10
11
  GoogleDriveConnectionMetadata,
11
12
  GoogleDriveOAuthStartResponse,
12
13
  SaveGoogleDriveSourceRequest,
13
14
  googleDriveOAuthScopeDecision,
14
15
  googleDriveScopesAllowCapability,
16
+ type GoogleDriveDisconnectRequest,
17
+ type GoogleDriveLifecycleActionRequest,
15
18
  type GoogleDriveOAuthStartRequest,
16
19
  } from "@opengeni/contracts/google-drive";
17
20
  import { hasPermission, requireEnvironmentEncryption } from "@opengeni/core";
18
21
  import type { ApiRouteDeps } from "@opengeni/core";
19
22
  import {
20
23
  buildConnectionTokenResolver,
24
+ ConnectionDisconnectGenerationError,
25
+ ConnectionDisconnectIdempotencyError,
21
26
  consumeIntegrationOAuthStateNonce,
22
27
  createConnection,
23
28
  decryptEnvironmentValue,
29
+ disconnectConnectionIdempotently,
24
30
  encryptEnvironmentValue,
25
31
  getConnectionMetadata,
26
32
  getWorkspaceGrant,
27
33
  loadConnectionCredentialForBroker,
34
+ transitionConnectionState,
28
35
  updateConnection,
36
+ type PermanentConnectionRefreshFailure,
29
37
  } from "@opengeni/db";
30
38
  import { createSignedState, readSignedState } from "@opengeni/github";
31
39
  import { readResponseJsonBounded, type FetchLike } from "@opengeni/network";
@@ -44,6 +52,12 @@ const GOOGLE_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
44
52
  const GOOGLE_REQUEST_TIMEOUT_MS = 10_000;
45
53
  const GOOGLE_DRIVE_PAGE_SIZE = 100;
46
54
  const GOOGLE_DRIVE_RETURN_PATH = (workspaceId: string) => `/workspaces/${workspaceId}/capabilities`;
55
+ const GOOGLE_DRIVE_RECONSENT_ERROR_CODES = new Set([
56
+ "appNotAuthorizedToFile",
57
+ "authError",
58
+ "insufficientFilePermissions",
59
+ "insufficientPermissions",
60
+ ]);
47
61
 
48
62
  type GoogleDriveOAuthState = {
49
63
  accountId: string;
@@ -65,6 +79,8 @@ type GoogleTokenResponse = {
65
79
  scopes: string[];
66
80
  };
67
81
 
82
+ type GoogleDriveConnectionRecord = NonNullable<Awaited<ReturnType<typeof getConnectionMetadata>>>;
83
+
68
84
  export async function startGoogleDriveOAuth(
69
85
  deps: ApiRouteDeps,
70
86
  input: {
@@ -236,6 +252,7 @@ export async function completeGoogleDriveOAuthCallback(
236
252
  googleDisplayName: identity.displayName,
237
253
  verifiedAt: new Date().toISOString(),
238
254
  accessMode: scopeDecision.accessMode,
255
+ lifecycle: googleDriveLifecycle("active"),
239
256
  ...(previousMetadata?.selectedSources
240
257
  ? { selectedSources: previousMetadata.selectedSources }
241
258
  : previousMetadata?.selectedSource
@@ -288,6 +305,123 @@ export async function completeGoogleDriveOAuthCallback(
288
305
  }
289
306
  }
290
307
 
308
+ export async function transitionGoogleDriveLifecycle(
309
+ deps: ApiRouteDeps,
310
+ input: {
311
+ workspaceId: string;
312
+ subjectId: string;
313
+ connectionId: string;
314
+ payload: GoogleDriveLifecycleActionRequest;
315
+ },
316
+ ) {
317
+ const existing = await getConnectionMetadata(
318
+ deps.db,
319
+ input.workspaceId,
320
+ input.connectionId,
321
+ input.subjectId,
322
+ );
323
+ if (!existing) {
324
+ throw new HTTPException(404, { message: "Google Drive connection not found" });
325
+ }
326
+ const metadata = requireGoogleDriveConnection(existing, input.subjectId);
327
+ const lifecycle = effectiveGoogleDriveLifecycle(existing, metadata);
328
+ const targetState = input.payload.action === "pause" ? "paused" : "active";
329
+
330
+ // Natural convergence makes retried pause/resume requests idempotent even if
331
+ // the caller still carries the pre-transition version.
332
+ if (existing.status === "active" && lifecycle.state === targetState) {
333
+ return existing;
334
+ }
335
+ if (existing.status === "revoked") {
336
+ throw new HTTPException(409, {
337
+ message: "Google Drive is disconnected; connect it again instead",
338
+ });
339
+ }
340
+ if (input.payload.action === "pause" && lifecycle.state !== "active") {
341
+ throw new HTTPException(409, {
342
+ message: "Google Drive must be reconnected before it can be paused",
343
+ });
344
+ }
345
+ if (input.payload.action === "resume" && lifecycle.state !== "paused") {
346
+ throw new HTTPException(409, {
347
+ message: "Google Drive must be reconnected or re-consented before it can resume",
348
+ });
349
+ }
350
+ if (existing.status !== "active" || existing.version !== input.payload.expectedVersion) {
351
+ throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
352
+ }
353
+
354
+ const updated = await transitionConnectionState(deps.db, {
355
+ workspaceId: input.workspaceId,
356
+ connectionId: existing.id,
357
+ visibleToSubjectId: input.subjectId,
358
+ expectedVersion: existing.version,
359
+ status: "active",
360
+ metadata: GoogleDriveConnectionMetadata.parse({
361
+ ...metadata,
362
+ lifecycle: googleDriveLifecycle(targetState),
363
+ }),
364
+ lastError: null,
365
+ updatedBySubjectId: input.subjectId,
366
+ });
367
+ if (!updated) {
368
+ const converged = await getConnectionMetadata(
369
+ deps.db,
370
+ input.workspaceId,
371
+ input.connectionId,
372
+ input.subjectId,
373
+ );
374
+ if (converged?.status === "active") {
375
+ const convergedMetadata = requireGoogleDriveConnection(converged, input.subjectId);
376
+ if (effectiveGoogleDriveLifecycle(converged, convergedMetadata).state === targetState) {
377
+ return converged;
378
+ }
379
+ }
380
+ throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
381
+ }
382
+ return updated;
383
+ }
384
+
385
+ export async function disconnectGoogleDrive(
386
+ deps: ApiRouteDeps,
387
+ input: {
388
+ workspaceId: string;
389
+ subjectId: string;
390
+ connection: GoogleDriveConnectionRecord;
391
+ payload: GoogleDriveDisconnectRequest;
392
+ },
393
+ ) {
394
+ const metadata = requireGoogleDriveConnection(input.connection, input.subjectId);
395
+ try {
396
+ return await disconnectConnectionIdempotently(deps.db, {
397
+ accountId: input.connection.accountId,
398
+ workspaceId: input.workspaceId,
399
+ subjectId: input.subjectId,
400
+ connectionId: input.connection.id,
401
+ expectedVersion: input.payload.expectedVersion,
402
+ idempotencyKey: input.payload.idempotencyKey,
403
+ metadata: GoogleDriveConnectionMetadata.parse({
404
+ ...metadata,
405
+ lifecycle: googleDriveLifecycle("disconnected"),
406
+ }),
407
+ lastError: null,
408
+ updatedBySubjectId: input.subjectId,
409
+ });
410
+ } catch (error) {
411
+ if (error instanceof ConnectionDisconnectIdempotencyError) {
412
+ throw new HTTPException(409, {
413
+ message: "Google Drive disconnect key was already used for another operation",
414
+ });
415
+ }
416
+ if (error instanceof ConnectionDisconnectGenerationError) {
417
+ throw new HTTPException(409, {
418
+ message: "Google Drive connection changed; refresh before disconnecting",
419
+ });
420
+ }
421
+ throw error;
422
+ }
423
+ }
424
+
291
425
  export async function browseGoogleDrive(
292
426
  deps: ApiRouteDeps,
293
427
  input: {
@@ -307,7 +441,7 @@ export async function browseGoogleDrive(
307
441
  if (!connection) {
308
442
  throw new HTTPException(404, { message: "Google Drive connection not found" });
309
443
  }
310
- requireGoogleDriveSourceConnection(connection, input.subjectId);
444
+ await requireGoogleDriveSourceConnection(deps, connection, input.subjectId);
311
445
  const parentId = validDriveId(input.parentId, "parentId");
312
446
  const currentItem = await resolveGoogleDriveBoundaryItem(deps, {
313
447
  workspaceId: input.workspaceId,
@@ -382,7 +516,7 @@ export async function saveGoogleDriveSource(
382
516
  if (!existing) {
383
517
  throw new HTTPException(404, { message: "Google Drive connection not found" });
384
518
  }
385
- requireGoogleDriveSourceConnection(existing, input.subjectId);
519
+ await requireGoogleDriveSourceConnection(deps, existing, input.subjectId);
386
520
  const verifiedSources = [];
387
521
  for (const source of payload.sources) {
388
522
  const sourceId = validDriveId(source.id, "source.id");
@@ -410,8 +544,8 @@ export async function saveGoogleDriveSource(
410
544
  input.connectionId,
411
545
  input.subjectId,
412
546
  )) ?? existing;
413
- const latestMetadata = requireGoogleDriveSourceConnection(latest, input.subjectId);
414
- const updated = await updateConnection(deps.db, {
547
+ const latestMetadata = await requireGoogleDriveSourceConnection(deps, latest, input.subjectId);
548
+ const updated = await transitionConnectionState(deps.db, {
415
549
  workspaceId: input.workspaceId,
416
550
  connectionId: latest.id,
417
551
  visibleToSubjectId: input.subjectId,
@@ -532,16 +666,7 @@ function requireGoogleDriveSettings(settings: Settings): {
532
666
  return { clientId, clientSecret };
533
667
  }
534
668
 
535
- function requireGoogleDriveConnection(
536
- connection: {
537
- subjectId: string | null;
538
- providerDomain: string;
539
- kind: string;
540
- grantedScopes: string[];
541
- metadata: Record<string, unknown>;
542
- },
543
- subjectId: string,
544
- ) {
669
+ function requireGoogleDriveConnection(connection: GoogleDriveConnectionRecord, subjectId: string) {
545
670
  const parsed = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
546
671
  if (
547
672
  connection.subjectId !== subjectId ||
@@ -554,14 +679,88 @@ function requireGoogleDriveConnection(
554
679
  return parsed.data;
555
680
  }
556
681
 
557
- function requireGoogleDriveSourceConnection(
558
- connection: Parameters<typeof requireGoogleDriveConnection>[0],
682
+ function googleDriveLifecycle(
683
+ state: GoogleDriveConnectionLifecycle["state"],
684
+ ): GoogleDriveConnectionLifecycle {
685
+ return GoogleDriveConnectionLifecycle.parse({
686
+ state,
687
+ recoverable: state !== "app_removed",
688
+ observedAt: new Date().toISOString(),
689
+ });
690
+ }
691
+
692
+ function effectiveGoogleDriveLifecycle(
693
+ connection: GoogleDriveConnectionRecord,
694
+ metadata: ReturnType<typeof requireGoogleDriveConnection>,
695
+ ): GoogleDriveConnectionLifecycle {
696
+ if (metadata.lifecycle) return metadata.lifecycle;
697
+ if (connection.status === "revoked") return googleDriveLifecycle("disconnected");
698
+ if (connection.status === "active") return googleDriveLifecycle("active");
699
+ return googleDriveLifecycle("reconnect_required");
700
+ }
701
+
702
+ async function transitionGoogleDriveConnectionLifecycle(
703
+ deps: ApiRouteDeps,
704
+ connection: GoogleDriveConnectionRecord,
559
705
  subjectId: string,
706
+ lifecycle: GoogleDriveConnectionLifecycle,
707
+ status: "active" | "needs_reauth" | "error",
708
+ lastError: string | null,
560
709
  ) {
561
710
  const metadata = requireGoogleDriveConnection(connection, subjectId);
711
+ if (
712
+ connection.status === status &&
713
+ metadata.lifecycle?.state === lifecycle.state &&
714
+ metadata.lifecycle.recoverable === lifecycle.recoverable
715
+ ) {
716
+ return connection;
717
+ }
718
+ return await transitionConnectionState(deps.db, {
719
+ workspaceId: connection.workspaceId,
720
+ connectionId: connection.id,
721
+ visibleToSubjectId: subjectId,
722
+ expectedVersion: connection.version,
723
+ status,
724
+ metadata: GoogleDriveConnectionMetadata.parse({ ...metadata, lifecycle }),
725
+ lastError,
726
+ updatedBySubjectId: subjectId,
727
+ });
728
+ }
729
+
730
+ async function requireGoogleDriveSourceConnection(
731
+ deps: ApiRouteDeps,
732
+ connection: GoogleDriveConnectionRecord,
733
+ subjectId: string,
734
+ ) {
735
+ const metadata = requireGoogleDriveConnection(connection, subjectId);
736
+ const lifecycle = effectiveGoogleDriveLifecycle(connection, metadata);
737
+ if (connection.status === "revoked") {
738
+ throw new HTTPException(409, { message: "Google Drive is disconnected" });
739
+ }
740
+ if (lifecycle.state === "paused") {
741
+ throw new HTTPException(409, { message: "Google Drive is paused" });
742
+ }
743
+ if (connection.status !== "active" || lifecycle.state !== "active") {
744
+ throw new HTTPException(401, {
745
+ message:
746
+ lifecycle.state === "reconsent_required"
747
+ ? "Google Drive needs permission re-consent"
748
+ : lifecycle.state === "app_removed"
749
+ ? "Google Drive app access is unavailable"
750
+ : "Google Drive needs to be reconnected",
751
+ });
752
+ }
562
753
  if (!googleDriveScopesAllowCapability(connection.grantedScopes, "recursive_source_sync")) {
754
+ await transitionGoogleDriveConnectionLifecycle(
755
+ deps,
756
+ connection,
757
+ subjectId,
758
+ googleDriveLifecycle("reconsent_required"),
759
+ "needs_reauth",
760
+ "google_drive_reconsent_required",
761
+ );
563
762
  throw new HTTPException(401, {
564
- message: "Google Drive needs to be reconnected with selected-source read access",
763
+ message: "Google Drive needs permission re-consent for selected-source read access",
565
764
  });
566
765
  }
567
766
  return metadata;
@@ -705,6 +904,99 @@ async function verifyGoogleDriveIdentity(accessToken: string, fetchImpl: FetchLi
705
904
  };
706
905
  }
707
906
 
907
+ function googleDriveRefreshFailureLifecycle(failure: PermanentConnectionRefreshFailure): {
908
+ lifecycle: GoogleDriveConnectionLifecycle;
909
+ status: "needs_reauth" | "error";
910
+ lastError: string;
911
+ } {
912
+ const code = failure.oauthErrorCode?.toLowerCase() ?? null;
913
+ if (code === "invalid_client" || code === "unauthorized_client") {
914
+ return {
915
+ lifecycle: googleDriveLifecycle("app_removed"),
916
+ status: "error",
917
+ lastError: "google_drive_app_removed",
918
+ };
919
+ }
920
+ if (code === "invalid_scope" || code === "insufficient_scope") {
921
+ return {
922
+ lifecycle: googleDriveLifecycle("reconsent_required"),
923
+ status: "needs_reauth",
924
+ lastError: "google_drive_reconsent_required",
925
+ };
926
+ }
927
+ if (code === "invalid_grant") {
928
+ return {
929
+ lifecycle: googleDriveLifecycle("token_revoked"),
930
+ status: "needs_reauth",
931
+ lastError: "google_drive_token_revoked",
932
+ };
933
+ }
934
+ return {
935
+ lifecycle: googleDriveLifecycle("reconnect_required"),
936
+ status: "needs_reauth",
937
+ lastError: "google_drive_reconnect_required",
938
+ };
939
+ }
940
+
941
+ async function transitionGoogleDrivePermanentRefreshFailure(
942
+ deps: ApiRouteDeps,
943
+ failure: PermanentConnectionRefreshFailure,
944
+ ): Promise<boolean> {
945
+ if (failure.providerDomain !== GOOGLE_DRIVE_PROVIDER_DOMAIN || !failure.subjectId) {
946
+ return false;
947
+ }
948
+ const connection = await getConnectionMetadata(
949
+ deps.db,
950
+ failure.workspaceId,
951
+ failure.connectionId,
952
+ failure.subjectId,
953
+ );
954
+ if (!connection || connection.version !== failure.connectionVersion) {
955
+ return true;
956
+ }
957
+ const transition = googleDriveRefreshFailureLifecycle(failure);
958
+ await transitionGoogleDriveConnectionLifecycle(
959
+ deps,
960
+ connection,
961
+ failure.subjectId,
962
+ transition.lifecycle,
963
+ transition.status,
964
+ transition.lastError,
965
+ );
966
+ return true;
967
+ }
968
+
969
+ async function transitionGoogleDriveProviderResponseFailure(
970
+ deps: ApiRouteDeps,
971
+ input: {
972
+ workspaceId: string;
973
+ subjectId: string;
974
+ connectionId: string;
975
+ connectionVersion: number;
976
+ lifecycle: GoogleDriveConnectionLifecycle;
977
+ status: "needs_reauth" | "error";
978
+ lastError: string;
979
+ },
980
+ ): Promise<void> {
981
+ const latest = await getConnectionMetadata(
982
+ deps.db,
983
+ input.workspaceId,
984
+ input.connectionId,
985
+ input.subjectId,
986
+ );
987
+ if (!latest || latest.version !== input.connectionVersion || latest.status !== "active") {
988
+ return;
989
+ }
990
+ await transitionGoogleDriveConnectionLifecycle(
991
+ deps,
992
+ latest,
993
+ input.subjectId,
994
+ input.lifecycle,
995
+ input.status,
996
+ input.lastError,
997
+ );
998
+ }
999
+
708
1000
  async function googleDriveApiRequest(
709
1001
  deps: ApiRouteDeps,
710
1002
  input: {
@@ -715,7 +1007,21 @@ async function googleDriveApiRequest(
715
1007
  label: string;
716
1008
  },
717
1009
  ): Promise<unknown> {
718
- const resolver = buildConnectionTokenResolver(deps.db, deps.settings);
1010
+ const current = await getConnectionMetadata(
1011
+ deps.db,
1012
+ input.workspaceId,
1013
+ input.connectionId,
1014
+ input.subjectId,
1015
+ );
1016
+ if (!current) {
1017
+ throw new HTTPException(404, { message: "Google Drive connection not found" });
1018
+ }
1019
+ await requireGoogleDriveSourceConnection(deps, current, input.subjectId);
1020
+ const resolver = buildConnectionTokenResolver(deps.db, deps.settings, undefined, {
1021
+ ...(deps.googleDriveFetch ? { refreshTransport: { fetchImpl: deps.googleDriveFetch } } : {}),
1022
+ transitionPermanentRefreshFailure: async (failure) =>
1023
+ await transitionGoogleDrivePermanentRefreshFailure(deps, failure),
1024
+ });
719
1025
  const resolve = async (forceRefresh: boolean) =>
720
1026
  await resolver({
721
1027
  workspaceId: input.workspaceId,
@@ -735,6 +1041,10 @@ async function googleDriveApiRequest(
735
1041
  if (credential.status !== "ok") {
736
1042
  throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
737
1043
  }
1044
+ let providerConnectionVersion = credential.connectionVersion;
1045
+ if (providerConnectionVersion === undefined) {
1046
+ throw new Error("Google Drive credential resolver omitted the connection version");
1047
+ }
738
1048
  const fetchImpl = deps.googleDriveFetch ?? fetch;
739
1049
  let response = await providerFetch(fetchImpl, input.url, {
740
1050
  headers: { ...credential.headers, accept: "application/json" },
@@ -745,16 +1055,52 @@ async function googleDriveApiRequest(
745
1055
  if (credential.status !== "ok") {
746
1056
  throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
747
1057
  }
1058
+ providerConnectionVersion = credential.connectionVersion;
1059
+ if (providerConnectionVersion === undefined) {
1060
+ throw new Error("Google Drive credential resolver omitted the connection version");
1061
+ }
748
1062
  response = await providerFetch(fetchImpl, input.url, {
749
1063
  headers: { ...credential.headers, accept: "application/json" },
750
1064
  });
751
1065
  }
752
1066
  if (!response.ok) {
753
- await response.body?.cancel().catch(() => undefined);
1067
+ if (response.status === 401) {
1068
+ await response.body?.cancel().catch(() => undefined);
1069
+ await transitionGoogleDriveProviderResponseFailure(deps, {
1070
+ workspaceId: input.workspaceId,
1071
+ subjectId: input.subjectId,
1072
+ connectionId: input.connectionId,
1073
+ connectionVersion: providerConnectionVersion,
1074
+ lifecycle: googleDriveLifecycle("reconnect_required"),
1075
+ status: "needs_reauth",
1076
+ lastError: "google_drive_reconnect_required",
1077
+ });
1078
+ throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
1079
+ }
1080
+ const providerErrorCode =
1081
+ response.status === 403 ? await readGoogleDriveProviderErrorCode(response) : null;
1082
+ if (response.status !== 403) {
1083
+ await response.body?.cancel().catch(() => undefined);
1084
+ }
1085
+ if (
1086
+ response.status === 403 &&
1087
+ providerErrorCode &&
1088
+ GOOGLE_DRIVE_RECONSENT_ERROR_CODES.has(providerErrorCode)
1089
+ ) {
1090
+ await transitionGoogleDriveProviderResponseFailure(deps, {
1091
+ workspaceId: input.workspaceId,
1092
+ subjectId: input.subjectId,
1093
+ connectionId: input.connectionId,
1094
+ connectionVersion: providerConnectionVersion,
1095
+ lifecycle: googleDriveLifecycle("reconsent_required"),
1096
+ status: "needs_reauth",
1097
+ lastError: "google_drive_reconsent_required",
1098
+ });
1099
+ }
754
1100
  throw new HTTPException(response.status === 403 ? 403 : 502, {
755
1101
  message:
756
1102
  response.status === 403
757
- ? "Google Drive denied metadata access; reconnect and approve the requested scope"
1103
+ ? "Google Drive denied metadata access; re-consent may be required"
758
1104
  : "Google Drive metadata request failed",
759
1105
  });
760
1106
  }
@@ -777,6 +1123,25 @@ async function providerFetch(
777
1123
  }
778
1124
  }
779
1125
 
1126
+ async function readGoogleDriveProviderErrorCode(response: Response): Promise<string | null> {
1127
+ try {
1128
+ const payload = objectRecord(
1129
+ await readResponseJsonBounded<unknown>(
1130
+ response,
1131
+ GOOGLE_RESPONSE_MAX_BYTES,
1132
+ "Google Drive error response",
1133
+ ),
1134
+ );
1135
+ const error = objectRecord(payload.error);
1136
+ const first = Array.isArray(error.errors) ? objectRecord(error.errors[0]) : {};
1137
+ const code = optionalString(first.reason) ?? optionalString(error.status);
1138
+ return code && /^[A-Za-z0-9_.-]{1,64}$/.test(code) ? code : null;
1139
+ } catch {
1140
+ await response.body?.cancel().catch(() => undefined);
1141
+ return null;
1142
+ }
1143
+ }
1144
+
780
1145
  function parseDriveItem(value: unknown): GoogleDriveBrowseItem | null {
781
1146
  const item = objectRecord(value);
782
1147
  const id = optionalString(item.id);
@@ -1,7 +1,8 @@
1
+ import { DocumentSearchResponse } from "@opengeni/contracts";
1
2
  import {
2
3
  getDocumentChunk,
3
4
  listDocumentBases,
4
- searchDocuments,
5
+ searchEffectiveDocuments,
5
6
  type DocumentAccessFilter,
6
7
  type DocumentServices,
7
8
  } from "@opengeni/documents";
@@ -47,9 +48,9 @@ export function buildDocumentsMcpServer(
47
48
  documentServices: DocumentServices,
48
49
  options: {
49
50
  createdBySessionId?: string | undefined;
50
- /** The human subject whose agent is making this retrieval request. */
51
- viewerSubjectId?: string | undefined;
52
- } = {},
51
+ /** Immutable human subject whose agent is making this retrieval request. */
52
+ initiatingSubjectId: string;
53
+ },
53
54
  ): McpServer {
54
55
  const server = new McpServer({
55
56
  name: "opengeni-documents",
@@ -60,7 +61,7 @@ export function buildDocumentsMcpServer(
60
61
  // documents are available only to the creating subject's agent.
61
62
  const agentAccess: DocumentAccessFilter = {
62
63
  agentOnly: true,
63
- ...(options.viewerSubjectId ? { viewerSubjectId: options.viewerSubjectId } : {}),
64
+ viewerSubjectId: options.initiatingSubjectId,
64
65
  };
65
66
 
66
67
  server.registerTool(
@@ -80,17 +81,35 @@ export function buildDocumentsMcpServer(
80
81
  description: "Search indexed documents with hybrid, vector, or keyword retrieval.",
81
82
  inputSchema: SearchInputSchema,
82
83
  },
83
- async (input) => searchContent(db, workspaceId, documentServices, input, agentAccess),
84
+ async (input) =>
85
+ searchContent(
86
+ db,
87
+ accountId,
88
+ workspaceId,
89
+ documentServices,
90
+ input,
91
+ options.initiatingSubjectId,
92
+ false,
93
+ ),
84
94
  );
85
95
 
86
96
  server.registerTool(
87
97
  "knowledge_search",
88
98
  {
89
99
  description:
90
- "Search company knowledge sources with optional base, source-kind, ACL, and retrieval-mode filters.",
100
+ "Search the effective authorized organization, current-workspace, and immutable initiating-user personal document scope. Authorization is applied before ranking and every result retains source and authority provenance.",
91
101
  inputSchema: SearchInputSchema,
92
102
  },
93
- async (input) => searchContent(db, workspaceId, documentServices, input, agentAccess),
103
+ async (input) =>
104
+ searchContent(
105
+ db,
106
+ accountId,
107
+ workspaceId,
108
+ documentServices,
109
+ input,
110
+ options.initiatingSubjectId,
111
+ true,
112
+ ),
94
113
  );
95
114
 
96
115
  server.registerTool(
@@ -102,7 +121,7 @@ export function buildDocumentsMcpServer(
102
121
  },
103
122
  },
104
123
  async ({ chunkId }) => {
105
- const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
124
+ const found = await getDocumentChunk(db, accountId, workspaceId, chunkId, agentAccess);
106
125
  return {
107
126
  content: [
108
127
  { type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
@@ -121,7 +140,7 @@ export function buildDocumentsMcpServer(
121
140
  },
122
141
  },
123
142
  async ({ chunkId }) => {
124
- const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
143
+ const found = await getDocumentChunk(db, accountId, workspaceId, chunkId, agentAccess);
125
144
  return {
126
145
  content: [
127
146
  { type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
@@ -205,6 +224,7 @@ export function buildDocumentsMcpServer(
205
224
 
206
225
  async function searchContent(
207
226
  db: Database,
227
+ accountId: string,
208
228
  workspaceId: string,
209
229
  documentServices: DocumentServices,
210
230
  input: {
@@ -226,28 +246,30 @@ async function searchContent(
226
246
  | undefined;
227
247
  aclTags?: string[] | undefined;
228
248
  },
229
- access: DocumentAccessFilter,
249
+ initiatingSubjectId: string,
250
+ wrapResponse: boolean,
230
251
  ) {
252
+ const results = await searchEffectiveDocuments(
253
+ db,
254
+ {
255
+ accountId,
256
+ workspaceId,
257
+ query: input.query,
258
+ ...(input.baseIds ? { baseIds: input.baseIds } : {}),
259
+ ...(input.limit ? { limit: input.limit } : {}),
260
+ ...(input.mode ? { mode: input.mode } : {}),
261
+ ...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
262
+ ...(input.aclTags ? { aclTags: input.aclTags } : {}),
263
+ initiatingSubjectId,
264
+ surface: "agent",
265
+ },
266
+ documentServices,
267
+ );
231
268
  return {
232
269
  content: [
233
270
  {
234
271
  type: "text" as const,
235
- text: JSON.stringify(
236
- await searchDocuments(
237
- db,
238
- {
239
- workspaceId,
240
- query: input.query,
241
- ...(input.baseIds ? { baseIds: input.baseIds } : {}),
242
- ...(input.limit ? { limit: input.limit } : {}),
243
- ...(input.mode ? { mode: input.mode } : {}),
244
- ...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
245
- ...(input.aclTags ? { aclTags: input.aclTags } : {}),
246
- access,
247
- },
248
- documentServices,
249
- ),
250
- ),
272
+ text: JSON.stringify(wrapResponse ? DocumentSearchResponse.parse({ results }) : results),
251
273
  },
252
274
  ],
253
275
  };