@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.
- package/dist/app.js +1 -1
- package/dist/{chunk-BJWHVMOL.js → chunk-K2NU4KRK.js} +3165 -3662
- package/dist/{chunk-BJWHVMOL.js.map → chunk-K2NU4KRK.js.map} +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/integrations/github-lens-connect.d.ts +2 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/documents.d.ts +5 -13
- package/dist/mcp/files.d.ts +2 -2
- package/dist/mcp/first-party-tool-permissions.d.ts +36 -0
- package/dist/mcp/knowledge-entries.d.ts +5 -0
- package/dist/mcp/server.d.ts +4 -4
- package/dist/routes/host-mcp-resolvers.d.ts +3 -0
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/sandbox-file-artifacts.d.ts +6 -2
- package/dist/workspace-state-projection.d.ts +3 -5
- package/dist/workspace-tool-gateway.d.ts +2 -1
- package/package.json +19 -19
- package/src/access-grant-rls.ts +13 -1
- package/src/app.ts +4 -11
- package/src/codemode.ts +4 -8
- package/src/index.ts +4 -3
- package/src/integrations/atlassian.ts +31 -28
- package/src/integrations/google-drive.ts +33 -30
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/provider-oauth.ts +11 -2
- package/src/mcp/documents.ts +57 -428
- package/src/mcp/files.ts +76 -58
- package/src/mcp/first-party-tool-permissions.ts +9 -0
- package/src/mcp/knowledge-entries.ts +276 -0
- package/src/mcp/server.ts +7 -361
- package/src/routes/company-brain.ts +58 -107
- package/src/routes/documents.ts +78 -294
- package/src/routes/files.ts +68 -3
- package/src/routes/host-mcp-resolvers.ts +41 -0
- package/src/routes/knowledge.ts +386 -0
- package/src/routes/organization-memberships.ts +33 -0
- package/src/routes/scheduled-tasks.ts +9 -1
- package/src/routes/sessions.ts +20 -3
- package/src/routes/skill-content.ts +8 -2
- package/src/routes/video-generation.ts +7 -5
- package/src/routes/workspace-learning.ts +12 -82
- package/src/routes/workspace-state.ts +34 -25
- package/src/routes/workspaces.ts +20 -10
- package/src/sandbox-file-artifacts.ts +54 -2
- package/src/scheduled-task-deletion.ts +3 -2
- package/src/workspace-state-projection.ts +18 -148
- package/src/workspace-tool-gateway.ts +14 -25
- package/dist/mcp/company-brain-governed-writes.d.ts +0 -26
- package/dist/mcp/remember.d.ts +0 -28
- package/src/mcp/company-brain-governed-writes.ts +0 -271
- package/src/mcp/remember.ts +0 -191
package/src/routes/documents.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
throw new HTTPException(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
//
|
|
643
|
-
//
|
|
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: "
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
}
|
|
953
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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)}`;
|
package/src/routes/files.ts
CHANGED
|
@@ -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
|
|
128
|
+
let access: Awaited<ReturnType<typeof requireAccessGrantAuthorization>>;
|
|
97
129
|
try {
|
|
98
|
-
|
|
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,
|
|
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
|
+
}
|