@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
@@ -1,7 +1,8 @@
1
+ import { fileOwnerContextForAccess } from "@opengeni/core";
2
+ import { withSessionRlsActorContext } from "@opengeni/db";
1
3
  import {
2
4
  AddDocumentRequest,
3
5
  CreateKnowledgeDropRequest,
4
- CreateKnowledgeMemoryRequest,
5
6
  CreateDocumentBaseRequest,
6
7
  Document,
7
8
  DocumentAuthorityReclassification,
@@ -9,12 +10,8 @@ import {
9
10
  DocumentDefaultCollectionBackfill,
10
11
  DocumentDefaultCollectionBackfillAudit,
11
12
  GetDocumentDefaultCollectionBackfillAuditQuery,
12
- DocumentSearchRequest,
13
- DocumentSearchResponse,
14
13
  FileAsset,
15
14
  FileDownloadUrlResponse,
16
- KnowledgeMemory,
17
- KnowledgeMemorySearchRequest,
18
15
  ListDocumentAuthorityReclassificationsQuery,
19
16
  ListDocumentAuthorityReclassificationsResponse,
20
17
  ListDocumentDefaultCollectionBackfillRunsResponse,
@@ -23,21 +20,8 @@ import {
23
20
  MoveDocumentRequest,
24
21
  ReclassifyDocumentAuthorityRequest,
25
22
  RunDocumentDefaultCollectionBackfillRequest,
26
- UpdateKnowledgeMemoryRequest,
27
- WorkspaceMemorySearchRequest,
28
- WorkspaceMemorySearchResponse,
29
23
  } from "@opengeni/contracts";
30
- import {
31
- recordAuditEvent,
32
- completeFileUpload,
33
- createFileUpload,
34
- createKnowledgeMemory,
35
- getKnowledgeMemory,
36
- listKnowledgeMemories,
37
- resolveSessionMemoryAgentScope,
38
- updateKnowledgeMemory,
39
- searchWorkspaceMemories,
40
- } from "@opengeni/db";
24
+ import { recordAuditEvent, completeFileUpload, createFileUpload } from "@opengeni/db";
41
25
  import {
42
26
  addDocumentToBase,
43
27
  createDocumentBase,
@@ -57,16 +41,15 @@ import {
57
41
  queueDocumentForReindex,
58
42
  reclassifyDocumentAuthority,
59
43
  runDocumentDefaultCollectionBackfill,
60
- searchEffectiveDocuments,
61
44
  } from "@opengeni/documents";
62
45
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
63
- import type { Context, Hono } from "hono";
46
+ import type { Hono } from "hono";
64
47
  import { HTTPException } from "hono/http-exception";
65
48
  import {
66
49
  requireAccessGrant,
67
50
  requireAccountAdminAuthorizationStamp,
68
51
  requireAccessGrantAuthorization,
69
- saveWorkspaceMemoryWithSlackPublication,
52
+ knowledgeContextForAccess,
70
53
  } from "@opengeni/core";
71
54
  import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
72
55
  import type { ApiRouteDeps } from "@opengeni/core";
@@ -81,6 +64,31 @@ import { sanitizeFilename } from "./files";
81
64
 
82
65
  export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
83
66
  const { db, objectStorage, documentIndexer, getDocumentServices } = deps;
67
+ // Document compatibility uploads use the same verified personal file owner as
68
+ // Files and chat uploads. A delegated subject label never becomes an owner.
69
+ for (const path of [
70
+ "/v1/workspaces/:workspaceId/document-bases/*",
71
+ "/v1/workspaces/:workspaceId/documents/*",
72
+ "/v1/workspaces/:workspaceId/knowledge/drops",
73
+ ]) {
74
+ app.use(path, async (c, next) => {
75
+ const permission =
76
+ (c.req.method === "GET" && !c.req.path.includes("/authority-reclassifications")) ||
77
+ c.req.path.endsWith("/search")
78
+ ? "documents:search"
79
+ : "documents:manage";
80
+ const access = await requireAccessGrantAuthorization(
81
+ c,
82
+ deps,
83
+ c.req.param("workspaceId") ?? "",
84
+ permission,
85
+ );
86
+ return withSessionRlsActorContext(
87
+ await fileOwnerContextForAccess(deps, access, permission),
88
+ next,
89
+ );
90
+ });
91
+ }
84
92
 
85
93
  app.post("/v1/workspaces/:workspaceId/document-bases", async (c) => {
86
94
  const workspaceId = c.req.param("workspaceId");
@@ -290,19 +298,6 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
290
298
  authorityWorkspaceId: document.authorityWorkspaceId,
291
299
  authoritySubjectId: document.authoritySubjectId,
292
300
  })) ?? document);
293
- if (indexed.status === "ready") {
294
- await recordWorkspaceUsage(deps, {
295
- accountId: grant.accountId,
296
- workspaceId,
297
- subjectId: grant.subjectId,
298
- eventType: "document.indexed",
299
- quantity: indexed.chunkCount,
300
- unit: "chunk",
301
- sourceResourceType: "document",
302
- sourceResourceId: indexed.id,
303
- idempotencyKey: `document.indexed:${workspaceId}:${indexed.id}:${indexed.updatedAt}`,
304
- });
305
- }
306
301
  return c.json(Document.parse(indexed), wasCreated ? 201 : 200);
307
302
  } catch (error) {
308
303
  throw documentHttpException(error);
@@ -558,19 +553,6 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
558
553
  authorityWorkspaceId: document.authorityWorkspaceId,
559
554
  authoritySubjectId: document.authoritySubjectId,
560
555
  })) ?? queued;
561
- if (indexed.status === "ready") {
562
- await recordWorkspaceUsage(deps, {
563
- accountId: grant.accountId,
564
- workspaceId,
565
- subjectId: grant.subjectId,
566
- eventType: "document.indexed",
567
- quantity: indexed.chunkCount,
568
- unit: "chunk",
569
- sourceResourceType: "document",
570
- sourceResourceId: indexed.id,
571
- idempotencyKey: `document.indexed:${workspaceId}:${indexed.id}:${indexed.updatedAt}`,
572
- });
573
- }
574
556
  return c.json(Document.parse(indexed));
575
557
  } catch (error) {
576
558
  if (error instanceof HTTPException) {
@@ -581,68 +563,21 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
581
563
  },
582
564
  );
583
565
 
584
- app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/search", async (c) => {
585
- const workspaceId = c.req.param("workspaceId");
586
- const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
587
- const payload = DocumentSearchRequest.parse(await c.req.json());
588
- const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
589
- if (!base) {
590
- throw new HTTPException(404, { message: "document base not found" });
591
- }
592
- return c.json(
593
- DocumentSearchResponse.parse({
594
- results: await searchEffectiveDocuments(
595
- db,
596
- {
597
- accountId: grant.accountId,
598
- workspaceId,
599
- baseIds: [base.id],
600
- query: payload.query,
601
- limit: payload.limit,
602
- mode: payload.mode,
603
- sourceKinds: payload.sourceKinds,
604
- authorityKinds: payload.authorityKinds,
605
- aclTags: payload.aclTags,
606
- initiatingSubjectId: grant.subjectId,
607
- surface: "human",
608
- },
609
- getDocumentServices(),
610
- ),
611
- }),
612
- );
613
- });
614
-
615
- app.post("/v1/workspaces/:workspaceId/knowledge/search", async (c) => {
616
- const workspaceId = c.req.param("workspaceId");
617
- const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
618
- const payload = await parseDocumentSearchRequest(c, "invalid knowledge search request");
619
- return c.json(
620
- DocumentSearchResponse.parse({
621
- results: await searchEffectiveDocuments(
622
- db,
623
- {
624
- accountId: grant.accountId,
625
- workspaceId,
626
- query: payload.query,
627
- baseIds: payload.baseIds,
628
- limit: payload.limit,
629
- mode: payload.mode,
630
- sourceKinds: payload.sourceKinds,
631
- authorityKinds: payload.authorityKinds,
632
- aclTags: payload.aclTags,
633
- initiatingSubjectId: grant.subjectId,
634
- surface: "human",
635
- },
636
- getDocumentServices(),
637
- ),
638
- }),
639
- );
640
- });
566
+ for (const path of [
567
+ "/v1/workspaces/:workspaceId/document-bases/:baseId/search",
568
+ "/v1/workspaces/:workspaceId/knowledge/search",
569
+ ]) {
570
+ app.post(path, async (c) => {
571
+ await requireAccessGrant(c, deps, c.req.param("workspaceId")!, "documents:search");
572
+ throw new HTTPException(410, {
573
+ message:
574
+ "Document search moved to /knowledge/entries/search. Pending Knowledge is available through its review view.",
575
+ });
576
+ });
577
+ }
641
578
 
642
- // Knowledge drop: raw text or an uploaded file, no metadata required. Lands
643
- // in the workspace Default base with curationStatus 'pending'; indexing then
644
- // applies the configured curation provider, or leaves it as 'none' when
645
- // curation is disabled.
579
+ // Compatibility upload endpoint. Retained originals and extracted text now
580
+ // enter canonical Knowledge. Agent findings use ordinary accepted turns.
646
581
  app.post("/v1/workspaces/:workspaceId/knowledge/drops", async (c) => {
647
582
  const workspaceId = c.req.param("workspaceId");
648
583
  const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "documents:manage");
@@ -657,6 +592,15 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
657
592
  quantity: 0,
658
593
  });
659
594
  const payload = CreateKnowledgeDropRequest.parse(await c.req.json());
595
+ const personalFileOwner =
596
+ payload.authorityKind === "personal"
597
+ ? (await fileOwnerContextForAccess(deps, access, "documents:manage"))
598
+ .privateFileOwnerSubjectId
599
+ : null;
600
+ if (payload.authorityKind === "personal" && !personalFileOwner)
601
+ throw new HTTPException(403, {
602
+ message: "Personal source retention requires verified ownership",
603
+ });
660
604
  const organizationAuthorityGranted =
661
605
  access.accountGrant?.permissions.includes("account:admin") === true;
662
606
  if (payload.authorityKind === "organization" && !organizationAuthorityGranted) {
@@ -685,6 +629,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
685
629
  accountId: grant.accountId,
686
630
  workspaceId,
687
631
  fileId: newFileId,
632
+ privateOwnerSubjectId: personalFileOwner ?? null,
688
633
  filename,
689
634
  safeFilename,
690
635
  contentType: "text/plain; charset=utf-8",
@@ -731,7 +676,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
731
676
  createdBy: grant.subjectId,
732
677
  initiatingSubjectId: grant.subjectId,
733
678
  organizationAuthorityGranted,
734
- curationStatus: "pending",
679
+ curationStatus: "none",
735
680
  access: { viewerSubjectId: grant.subjectId },
736
681
  });
737
682
  const wasCreated =
@@ -747,19 +692,6 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
747
692
  authorityWorkspaceId: document.authorityWorkspaceId,
748
693
  authoritySubjectId: document.authoritySubjectId,
749
694
  })) ?? document);
750
- if (indexed.status === "ready") {
751
- await recordWorkspaceUsage(deps, {
752
- accountId: grant.accountId,
753
- workspaceId,
754
- subjectId: grant.subjectId,
755
- eventType: "document.indexed",
756
- quantity: indexed.chunkCount,
757
- unit: "chunk",
758
- sourceResourceType: "document",
759
- sourceResourceId: indexed.id,
760
- idempotencyKey: `document.indexed:${workspaceId}:${indexed.id}:${indexed.updatedAt}`,
761
- });
762
- }
763
695
  return c.json(Document.parse(indexed), wasCreated ? 201 : 200);
764
696
  } catch (error) {
765
697
  if (error instanceof HTTPException) {
@@ -809,150 +741,26 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
809
741
  }
810
742
  });
811
743
 
812
- app.get("/v1/workspaces/:workspaceId/knowledge/memories", async (c) => {
813
- const workspaceId = c.req.param("workspaceId");
814
- await requireAccessGrant(c, deps, workspaceId, "documents:search");
815
- const parsed = KnowledgeMemorySearchRequest.safeParse({
816
- query: c.req.query("query") || undefined,
817
- status: c.req.query("status") || undefined,
818
- kind: c.req.query("kind") || undefined,
819
- scope: c.req.query("scope") || undefined,
820
- limit: c.req.query("limit") ? Number(c.req.query("limit")) : undefined,
821
- });
822
- if (!parsed.success) {
823
- throw new HTTPException(400, { message: "invalid knowledge memory query parameters" });
824
- }
825
- return c.json(
826
- (await listKnowledgeMemories(db, workspaceId, parsed.data)).map((memory) =>
827
- KnowledgeMemory.parse(memory),
828
- ),
829
- );
830
- });
831
-
832
- app.get("/v1/workspaces/:workspaceId/knowledge/memories/:memoryId", async (c) => {
833
- const workspaceId = c.req.param("workspaceId");
834
- await requireAccessGrant(c, deps, workspaceId, "documents:search");
835
- const memory = await getKnowledgeMemory(db, workspaceId, c.req.param("memoryId"));
836
- if (!memory) {
837
- throw new HTTPException(404, { message: "knowledge memory not found" });
838
- }
839
- return c.json(KnowledgeMemory.parse(memory));
840
- });
841
-
842
- // Hybrid search over the workspace's agent-visible memory (active ∪ approved).
843
- // Available regardless of the workspace memory setting (human/audit lane).
844
- app.post("/v1/workspaces/:workspaceId/knowledge/memories/search", async (c) => {
845
- const workspaceId = c.req.param("workspaceId");
846
- await requireAccessGrant(c, deps, workspaceId, "documents:search");
847
- const parsed = WorkspaceMemorySearchRequest.safeParse(await c.req.json());
848
- if (!parsed.success) {
849
- throw new HTTPException(400, { message: "invalid workspace memory search request" });
850
- }
851
- const results = await searchWorkspaceMemories(
852
- db,
853
- workspaceId,
854
- parsed.data,
855
- getDocumentServices().embedder,
856
- );
857
- return c.json(
858
- WorkspaceMemorySearchResponse.parse({
859
- results: results.map((result) => ({
860
- ...result,
861
- memory: KnowledgeMemory.parse(result.memory),
862
- })),
863
- }),
864
- );
865
- });
866
-
867
- app.post("/v1/workspaces/:workspaceId/knowledge/memories", async (c) => {
868
- const workspaceId = c.req.param("workspaceId");
869
- const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
870
- const parsedBody = CreateKnowledgeMemoryRequest.safeParse(await c.req.json());
871
- if (!parsedBody.success) {
872
- throw new HTTPException(400, { message: "invalid knowledge memory request" });
873
- }
874
- const payload = parsedBody.data;
875
- if (payload.status !== "active" && payload.slackPublication) {
876
- throw new HTTPException(400, {
877
- message: "Slack publication is available only for active Workspace Memory writes",
878
- });
879
- }
880
- // status `active` (the default) is a memory write → route through the single
881
- // gate (sanitize + embed + dedup). Explicit proposed/approved/rejected keeps
882
- // the legacy curated create.
883
- if (payload.status === "active") {
884
- try {
885
- const result = await saveWorkspaceMemoryWithSlackPublication(
886
- db,
887
- {
888
- accountId: grant.accountId,
889
- workspaceId,
890
- text: payload.text,
891
- kind: payload.kind,
892
- confidence: payload.confidence,
893
- pinned: payload.pinned,
894
- replacesId: payload.replacesId ?? null,
895
- metadata: payload.metadata,
896
- origin: "human",
897
- },
898
- payload.slackPublication
899
- ? {
900
- distribution: payload.slackPublication,
901
- actor: {
902
- kind: "human",
903
- subjectId: grant.subjectId,
904
- initiatingHumanSubjectId: grant.subjectId,
905
- },
906
- ownerLabel: grant.subjectLabel ?? null,
907
- }
908
- : null,
909
- getDocumentServices().embedder,
910
- );
911
- return c.json(KnowledgeMemory.parse(result.memory), 201);
912
- } catch (error) {
913
- throw documentHttpException(error);
914
- }
915
- }
916
- return c.json(
917
- KnowledgeMemory.parse(
918
- await createKnowledgeMemory(db, {
919
- ...payload,
920
- accountId: grant.accountId,
921
- workspaceId,
922
- }),
923
- ),
924
- 201,
925
- );
926
- });
927
-
928
- app.patch("/v1/workspaces/:workspaceId/knowledge/memories/:memoryId", async (c) => {
929
- const workspaceId = c.req.param("workspaceId");
930
- const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
931
- const payload = UpdateKnowledgeMemoryRequest.parse(await c.req.json());
932
- const reviewedBy =
933
- payload.reviewedBy ??
934
- (payload.status === "approved" || payload.status === "rejected"
935
- ? (grant.subjectLabel ?? grant.subjectId)
936
- : undefined);
937
- try {
744
+ // Old clients get an explicit cutover response instead of writing to an
745
+ // invisible second Knowledge store. Historical Memory IDs are entry IDs now.
746
+ for (const path of [
747
+ "/v1/workspaces/:workspaceId/knowledge/memories",
748
+ "/v1/workspaces/:workspaceId/knowledge/memories/*",
749
+ ]) {
750
+ app.all(path, async (c) => {
751
+ await requireAccessGrant(c, deps, c.req.param("workspaceId")!, "documents:search");
938
752
  return c.json(
939
- KnowledgeMemory.parse(
940
- await updateKnowledgeMemory(
941
- db,
942
- workspaceId,
943
- c.req.param("memoryId"),
944
- {
945
- ...payload,
946
- ...(reviewedBy ? { reviewedBy } : {}),
947
- },
948
- getDocumentServices().embedder,
949
- ),
950
- ),
753
+ {
754
+ error: {
755
+ code: "memory_replaced",
756
+ message:
757
+ "Memory has been replaced by Knowledge entries. Use /knowledge/entries and the current SDK Knowledge methods.",
758
+ },
759
+ },
760
+ 410,
951
761
  );
952
- } catch (error) {
953
- throw documentHttpException(error);
954
- }
955
- });
762
+ });
763
+ }
956
764
 
957
765
  app.all("/v1/workspaces/:workspaceId/mcp/docs", async (c) => {
958
766
  const workspaceId = c.req.param("workspaceId");
@@ -998,20 +806,6 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
998
806
  throw error;
999
807
  }
1000
808
  return await withAccessGrantSessionRlsContext(deps, grant, async () => {
1001
- const sessionId =
1002
- typeof grant.metadata?.sessionId === "string" ? grant.metadata.sessionId : undefined;
1003
- const attemptId =
1004
- typeof grant.metadata?.attemptId === "string" ? grant.metadata.attemptId : undefined;
1005
- // A session-bound caller reads Memory through its frozen selector
1006
- // (migration 0427); a missing row resolves to no Memory tools.
1007
- const memory =
1008
- sessionId !== undefined
1009
- ? ((await resolveSessionMemoryAgentScope(db, workspaceId, sessionId, grant.metadata)) ?? {
1010
- mode: "off" as const,
1011
- userSubjectId: null,
1012
- rootSessionId: null,
1013
- })
1014
- : null;
1015
809
  const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
1016
810
  const server = buildDocumentsMcpServer(
1017
811
  db,
@@ -1019,10 +813,11 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
1019
813
  workspaceId,
1020
814
  getDocumentServices(),
1021
815
  {
1022
- createdBySessionId: sessionId,
1023
- attemptId,
1024
- initiatingSubjectId: grant.subjectId,
1025
- memory,
816
+ knowledge: await knowledgeContextForAccess(
817
+ deps,
818
+ await requireAccessGrantAuthorization(c, deps, workspaceId, "documents:search"),
819
+ "documents:search",
820
+ ),
1026
821
  },
1027
822
  );
1028
823
  await server.connect(transport);
@@ -1031,17 +826,6 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
1031
826
  });
1032
827
  }
1033
828
 
1034
- async function parseDocumentSearchRequest(
1035
- context: Context,
1036
- message: string,
1037
- ): Promise<DocumentSearchRequest> {
1038
- const parsed = DocumentSearchRequest.safeParse(await context.req.json().catch(() => null));
1039
- if (!parsed.success) {
1040
- throw new HTTPException(422, { message });
1041
- }
1042
- return parsed.data;
1043
- }
1044
-
1045
829
  /** Derive a .txt filename for a raw-text drop from its optional title/filename. */
1046
830
  function dropFilename(preferred: string | undefined): string {
1047
831
  const stem = (preferred ?? "").trim() || `note-${new Date().toISOString().slice(0, 10)}`;
@@ -1,8 +1,11 @@
1
+ import { ZodError } from "zod";
1
2
  import {
2
3
  CompleteFileUploadResponse,
3
4
  CreateFileUploadRequest,
4
5
  CreateFileUploadResponse,
5
6
  FileAsset,
7
+ FileListRequest,
8
+ FileListResponse,
6
9
  FileDownloadUrlResponse,
7
10
  RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
8
11
  RETAINED_OUTPUT_MAX_PAGE_BYTES,
@@ -17,6 +20,7 @@ import {
17
20
  type RetainedOutputUnavailableReason,
18
21
  } from "@opengeni/contracts";
19
22
  import {
23
+ withSessionRlsActorContext,
20
24
  claimFileUploadCleanup,
21
25
  completeFileUploadCleanup,
22
26
  completeFileUpload,
@@ -26,6 +30,7 @@ import {
26
30
  recordAuditEvent,
27
31
  getGeneratedVideoArtifact,
28
32
  getFilesForSubject,
33
+ listFilesForSubject,
29
34
  getRetainedFileArtifact,
30
35
  getRetainedScreenshotArtifact,
31
36
  requireFileForSubject,
@@ -37,6 +42,8 @@ import {
37
42
  import type { Context, Hono } from "hono";
38
43
  import { HTTPException } from "hono/http-exception";
39
44
  import {
45
+ fileOwnerContextForAccess,
46
+ requireAccessGrantAuthorization,
40
47
  grantHasAgentAttemptAuthority,
41
48
  requireAccessGrant,
42
49
  requireLiveAgentAttemptAuthorization,
@@ -59,6 +66,31 @@ import {
59
66
  export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
60
67
  const { db, objectStorage } = deps;
61
68
 
69
+ const fileRequestAuthority = new WeakMap<Request, { owner: string | null; agent: boolean }>();
70
+ // This scope covers metadata, original bytes, range reads and finalization.
71
+ // A service/agent subject string alone cannot unlock a personal attachment.
72
+ for (const path of [
73
+ "/v1/workspaces/:workspaceId/files",
74
+ "/v1/workspaces/:workspaceId/files/*",
75
+ "/v1/workspaces/:workspaceId/artifacts/*",
76
+ ]) {
77
+ app.use(path, async (c, next) => {
78
+ const permission = c.req.path.includes("/files/uploads") ? "files:upload" : "files:read";
79
+ const access = await requireAccessGrantAuthorization(
80
+ c,
81
+ deps,
82
+ c.req.param("workspaceId") ?? "",
83
+ permission,
84
+ );
85
+ const actor = await fileOwnerContextForAccess(deps, access, permission);
86
+ fileRequestAuthority.set(c.req.raw, {
87
+ owner: actor.privateFileOwnerSubjectId ?? null,
88
+ agent: access.grant.principalKind === "agent_attempt",
89
+ });
90
+ return withSessionRlsActorContext(actor, next);
91
+ });
92
+ }
93
+
62
94
  app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => {
63
95
  const workspaceId = c.req.param("workspaceId");
64
96
  let oauthAccess: Awaited<ReturnType<typeof resolveMcpOAuthRouteAccess>>;
@@ -93,20 +125,21 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
93
125
  }
94
126
  });
95
127
  }
96
- let grant: Awaited<ReturnType<typeof requireAccessGrant>>;
128
+ let access: Awaited<ReturnType<typeof requireAccessGrantAuthorization>>;
97
129
  try {
98
- grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
130
+ access = await requireAccessGrantAuthorization(c, deps, workspaceId, "files:read");
99
131
  } catch (error) {
100
132
  if (deps.settings.mcpOauthEnabled && error instanceof HTTPException && error.status === 401) {
101
133
  c.header("www-authenticate", mcpOAuthAuthenticateHeader(deps, new URL(c.req.url).pathname));
102
134
  }
103
135
  throw error;
104
136
  }
137
+ const { grant } = access;
105
138
  return await withAccessGrantSessionRlsContext(deps, grant, async () => {
106
139
  const transport = new WebStandardStreamableHTTPServerTransport({
107
140
  enableJsonResponse: true,
108
141
  });
109
- const server = buildFilesMcpServer(deps, grant);
142
+ const server = buildFilesMcpServer(deps, access);
110
143
  await server.connect(transport);
111
144
  return await transport.handleRequest(c.req.raw);
112
145
  });
@@ -121,6 +154,12 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
121
154
  });
122
155
  }
123
156
  const payload = CreateFileUploadRequest.parse(await c.req.json());
157
+ const privateOwner = fileRequestAuthority.get(c.req.raw)?.owner ?? null;
158
+ const personal =
159
+ payload.scope === "personal" ||
160
+ (fileRequestAuthority.get(c.req.raw)?.agent && privateOwner !== null);
161
+ if (personal && !privateOwner)
162
+ throw new HTTPException(403, { message: "Personal uploads require the authenticated owner" });
124
163
  await requireLimit(deps, {
125
164
  accountId: grant.accountId,
126
165
  workspaceId,
@@ -144,6 +183,7 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
144
183
  accountId: grant.accountId,
145
184
  workspaceId,
146
185
  fileId,
186
+ privateOwnerSubjectId: personal ? privateOwner : null,
147
187
  filename: payload.filename,
148
188
  safeFilename,
149
189
  contentType: payload.contentType,
@@ -340,6 +380,31 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
340
380
  return c.json(CompleteFileUploadResponse.parse({ file }));
341
381
  });
342
382
 
383
+ app.get("/v1/workspaces/:workspaceId/files", async (c) => {
384
+ const workspaceId = c.req.param("workspaceId");
385
+ const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
386
+ const parsed = FileListRequest.safeParse({
387
+ ...c.req.query(),
388
+ ...(c.req.query("limit") !== undefined ? { limit: Number(c.req.query("limit")) } : {}),
389
+ });
390
+ if (!parsed.success) throw new HTTPException(422, { message: "Invalid file list request" });
391
+ c.header("cache-control", "private, no-store");
392
+ return c.json(
393
+ FileListResponse.parse(
394
+ await listFilesForSubject(db, {
395
+ accountId: grant.accountId,
396
+ workspaceId,
397
+ subjectId: grant.subjectId,
398
+ ...parsed.data,
399
+ }).catch((error: unknown) => {
400
+ if (error instanceof ZodError)
401
+ throw new HTTPException(422, { message: "Invalid file cursor" });
402
+ throw error;
403
+ }),
404
+ ),
405
+ );
406
+ });
407
+
343
408
  app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => {
344
409
  const workspaceId = c.req.param("workspaceId");
345
410
  const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
@@ -0,0 +1,41 @@
1
+ import type { Hono } from "hono";
2
+ import { hostMcpResolverForRequest, type ApiRouteDeps } from "@opengeni/core";
3
+
4
+ export function registerHostMcpResolverRoutes(app: Hono, deps: ApiRouteDeps): void {
5
+ const path = "/v1/organizations/:organizationId/mcp-credential-resolvers/:externalSource";
6
+ app.get(path, async (context) =>
7
+ context.json(
8
+ await hostMcpResolverForRequest(
9
+ context,
10
+ deps,
11
+ context.req.param("organizationId"),
12
+ context.req.param("externalSource"),
13
+ "get",
14
+ ),
15
+ ),
16
+ );
17
+ app.put(path, async (context) =>
18
+ context.json(
19
+ await hostMcpResolverForRequest(
20
+ context,
21
+ deps,
22
+ context.req.param("organizationId"),
23
+ context.req.param("externalSource"),
24
+ "put",
25
+ await context.req.json().catch(() => null),
26
+ ),
27
+ ),
28
+ );
29
+ app.post(`${path}/revoke`, async (context) =>
30
+ context.json(
31
+ await hostMcpResolverForRequest(
32
+ context,
33
+ deps,
34
+ context.req.param("organizationId"),
35
+ context.req.param("externalSource"),
36
+ "revoke",
37
+ await context.req.json().catch(() => null),
38
+ ),
39
+ ),
40
+ );
41
+ }