@opengeni/api-router 0.12.12 → 0.14.4
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.d.ts +18 -38
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth.d.ts +35 -0
- package/dist/{chunk-5TULDPWX.js → chunk-36PW33ND.js} +4111 -1277
- package/dist/chunk-36PW33ND.js.map +1 -0
- package/dist/codex-redemption-security.d.ts +15 -0
- package/dist/github-access.d.ts +12 -0
- package/dist/github-browser-flow.d.ts +14 -0
- package/dist/http/auth.d.ts +3 -0
- package/dist/http/common.d.ts +3 -0
- package/dist/http/sse.d.ts +70 -0
- package/dist/index.d.ts +11 -13
- package/dist/index.js +1 -1
- package/dist/integrations/google-drive.d.ts +76 -0
- package/dist/integrations/oauth-client.d.ts +42 -0
- package/dist/integrations/provider-domain.d.ts +7 -0
- package/dist/integrations/slack-bot.d.ts +343 -0
- package/dist/mcp/documents.d.ts +8 -0
- package/dist/mcp/files.d.ts +5 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/session-view.d.ts +191 -0
- package/dist/mcp/toolspace.d.ts +61 -0
- package/dist/model-catalog.d.ts +39 -0
- package/dist/observability.d.ts +3 -0
- package/dist/routes/api-keys.d.ts +3 -0
- package/dist/routes/billing.d.ts +16 -0
- package/dist/routes/capabilities.d.ts +3 -0
- package/dist/routes/catalog-assets.d.ts +5 -0
- package/dist/routes/codex.d.ts +10 -0
- package/dist/routes/connections.d.ts +3 -0
- package/dist/routes/documents.d.ts +3 -0
- package/dist/routes/enrollments.d.ts +3 -0
- package/dist/routes/environments.d.ts +5 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/github.d.ts +3 -0
- package/dist/routes/insights.d.ts +3 -0
- package/dist/routes/install.d.ts +5 -0
- package/dist/routes/machines.d.ts +3 -0
- package/dist/routes/packs.d.ts +3 -0
- package/dist/routes/preference-registry.d.ts +5 -0
- package/dist/routes/rigs.d.ts +3 -0
- package/dist/routes/scheduled-tasks.d.ts +3 -0
- package/dist/routes/sessions.d.ts +15 -0
- package/dist/routes/social.d.ts +3 -0
- package/dist/routes/transcriptions.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +59 -0
- package/dist/routes/workspace-instruction-policies.d.ts +3 -0
- package/dist/routes/workspace-state.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +5 -0
- package/dist/sandbox/access.d.ts +21 -0
- package/dist/sandbox/auth-callout.d.ts +30 -0
- package/dist/sandbox/channel-a.d.ts +37 -0
- package/dist/sandbox/enrollment.d.ts +120 -0
- package/dist/sandbox/machines.d.ts +29 -0
- package/dist/sandbox/metrics-ingestion.d.ts +128 -0
- package/dist/sandbox/rematerialize.d.ts +24 -0
- package/dist/sandbox/viewer.d.ts +251 -0
- package/dist/transcription/providers/azure-openai.d.ts +9 -0
- package/dist/transcription/providers/codex-subscription.d.ts +9 -0
- package/dist/transcription/providers/openai.d.ts +9 -0
- package/dist/transcription/service.d.ts +10 -0
- package/dist/workspace-state-projection.d.ts +16 -0
- package/package.json +13 -13
- package/src/app.ts +67 -5
- package/src/integrations/google-drive.ts +869 -0
- package/src/integrations/oauth-client.ts +41 -6
- package/src/integrations/slack-bot.ts +756 -26
- package/src/mcp/server.ts +200 -3
- package/src/mcp/session-view.ts +1 -0
- package/src/mcp/toolspace.ts +17 -9
- package/src/routes/connections.ts +137 -7
- package/src/routes/documents.ts +13 -2
- package/src/routes/github.ts +7 -5
- package/src/routes/insights.ts +30 -0
- package/src/routes/preference-registry.ts +482 -0
- package/src/routes/sessions.ts +10 -0
- package/src/routes/transcriptions.ts +155 -0
- package/src/routes/workspace-state.ts +61 -0
- package/src/sandbox/channel-a.ts +2 -0
- package/src/sandbox/rematerialize.ts +111 -4
- package/src/sandbox/viewer.ts +29 -12
- package/src/transcription/providers/azure-openai.ts +45 -0
- package/src/transcription/providers/codex-subscription.ts +100 -0
- package/src/transcription/providers/openai.ts +93 -0
- package/src/transcription/service.ts +121 -0
- package/src/workspace-state-projection.ts +244 -0
- package/dist/chunk-5TULDPWX.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
getSessionGoal,
|
|
37
37
|
getSessionQueueSnapshot,
|
|
38
38
|
getSessionTurn,
|
|
39
|
+
getOrCreatePreferenceRegistrySnapshot,
|
|
40
|
+
getPreferenceRegistryFullContent,
|
|
39
41
|
getVariableSet,
|
|
40
42
|
getVariableSetByName,
|
|
41
43
|
areGitHubRepositoriesAllowedForWorkspace,
|
|
@@ -186,6 +188,8 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
186
188
|
memory_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
187
189
|
memory_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
188
190
|
memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
|
|
191
|
+
preference_registry_summary: { sessionRequired: true, allOf: ["workspace:read"] },
|
|
192
|
+
preference_registry_get: { sessionRequired: true, allOf: ["workspace:read"] },
|
|
189
193
|
sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
190
194
|
sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
191
195
|
sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
@@ -226,8 +230,13 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
226
230
|
scheduled_task_runs_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
|
|
227
231
|
slack_bot_list_channels: { allOf: ["connections:read"] },
|
|
228
232
|
slack_bot_channel_history: { allOf: ["connections:read"] },
|
|
233
|
+
slack_bot_thread_replies: { allOf: ["connections:read"] },
|
|
229
234
|
slack_bot_list_users: { allOf: ["connections:read"] },
|
|
235
|
+
slack_bot_list_files: { allOf: ["connections:read"] },
|
|
236
|
+
slack_bot_file_info: { allOf: ["connections:read"] },
|
|
237
|
+
slack_bot_file_content: { allOf: ["connections:read"] },
|
|
230
238
|
slack_bot_post_message: { allOf: ["connections:read"] },
|
|
239
|
+
slack_bot_delete_message: { allOf: ["connections:read"] },
|
|
231
240
|
} satisfies Record<FirstPartyMcpToolName, FirstPartyToolAuthorization>;
|
|
232
241
|
|
|
233
242
|
const FIRST_PARTY_MCP_TOOL_NAME_SET = new Set<string>(FIRST_PARTY_MCP_TOOL_NAMES);
|
|
@@ -366,6 +375,9 @@ export function buildOpenGeniMcpServer(
|
|
|
366
375
|
if (!toolspaceMode && sessionId !== null && options.workspaceMemoryEnabled === true) {
|
|
367
376
|
registerMemoryTools(server, deps, grant, sessionId, json);
|
|
368
377
|
}
|
|
378
|
+
if (!toolspaceMode && sessionId !== null && preferenceAttemptClaims(grant) !== null) {
|
|
379
|
+
registerPreferenceRegistryTools(server, deps, grant, json);
|
|
380
|
+
}
|
|
369
381
|
|
|
370
382
|
// Fleet tools (M7 bring-your-own-compute): list / attach / swap / run_on /
|
|
371
383
|
// provision over the session's Modal box + the workspace's enrolled machines.
|
|
@@ -838,6 +850,32 @@ function registerSlackBotTools(
|
|
|
838
850
|
),
|
|
839
851
|
);
|
|
840
852
|
|
|
853
|
+
server.registerTool(
|
|
854
|
+
"slack_bot_thread_replies",
|
|
855
|
+
{
|
|
856
|
+
description:
|
|
857
|
+
"Read a Slack thread as the workspace-shared OpenGeni bot. Pass the channel ID and the parent message timestamp returned by channel history. The result includes the parent followed by its replies.",
|
|
858
|
+
inputSchema: {
|
|
859
|
+
connectionId: z4.string().uuid().optional(),
|
|
860
|
+
channelId: z4.string().min(1).max(64),
|
|
861
|
+
threadTimestamp: z4.string().min(1).max(64),
|
|
862
|
+
cursor: z4.string().max(1024).optional(),
|
|
863
|
+
limit: z4.number().int().min(1).max(100).optional(),
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
async ({ connectionId, channelId, threadTimestamp, cursor, limit }) =>
|
|
867
|
+
json(
|
|
868
|
+
await (
|
|
869
|
+
await clientFor(connectionId)
|
|
870
|
+
).threadReplies({
|
|
871
|
+
channelId,
|
|
872
|
+
threadTimestamp,
|
|
873
|
+
...(cursor ? { cursor } : {}),
|
|
874
|
+
...(limit !== undefined ? { limit } : {}),
|
|
875
|
+
}),
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
|
|
841
879
|
server.registerTool(
|
|
842
880
|
"slack_bot_list_users",
|
|
843
881
|
{
|
|
@@ -859,20 +897,95 @@ function registerSlackBotTools(
|
|
|
859
897
|
),
|
|
860
898
|
);
|
|
861
899
|
|
|
900
|
+
server.registerTool(
|
|
901
|
+
"slack_bot_list_files",
|
|
902
|
+
{
|
|
903
|
+
description:
|
|
904
|
+
"List Slack files and canvases shared with a channel where the workspace-shared OpenGeni bot is already a member.",
|
|
905
|
+
inputSchema: {
|
|
906
|
+
connectionId: z4.string().uuid().optional(),
|
|
907
|
+
channelId: z4.string().min(1).max(64),
|
|
908
|
+
cursor: z4.string().max(1024).optional(),
|
|
909
|
+
limit: z4.number().int().min(1).max(200).optional(),
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
async ({ connectionId, channelId, cursor, limit }) =>
|
|
913
|
+
json(
|
|
914
|
+
await (
|
|
915
|
+
await clientFor(connectionId)
|
|
916
|
+
).listFiles({
|
|
917
|
+
channelId,
|
|
918
|
+
...(cursor ? { cursor } : {}),
|
|
919
|
+
...(limit !== undefined ? { limit } : {}),
|
|
920
|
+
}),
|
|
921
|
+
),
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
server.registerTool(
|
|
925
|
+
"slack_bot_file_info",
|
|
926
|
+
{
|
|
927
|
+
description:
|
|
928
|
+
"Read safe metadata for a Slack file or canvas shared with a channel where the workspace-shared OpenGeni bot is already a member. For an embedded huddle transcript, also pass the shared canvas file ID as parentFileId so OpenGeni can verify the indirect share.",
|
|
929
|
+
inputSchema: {
|
|
930
|
+
connectionId: z4.string().uuid().optional(),
|
|
931
|
+
channelId: z4.string().min(1).max(64),
|
|
932
|
+
fileId: z4.string().min(1).max(64),
|
|
933
|
+
parentFileId: z4.string().min(1).max(64).optional(),
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
async ({ connectionId, channelId, fileId, parentFileId }) =>
|
|
937
|
+
json(
|
|
938
|
+
await (
|
|
939
|
+
await clientFor(connectionId)
|
|
940
|
+
).fileInfo({
|
|
941
|
+
channelId,
|
|
942
|
+
fileId,
|
|
943
|
+
...(parentFileId ? { parentFileId } : {}),
|
|
944
|
+
}),
|
|
945
|
+
),
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
server.registerTool(
|
|
949
|
+
"slack_bot_file_content",
|
|
950
|
+
{
|
|
951
|
+
description:
|
|
952
|
+
"Read a bounded page of UTF-8 text from a Slack file or canvas shared with a channel where the workspace-shared OpenGeni bot is already a member. For an embedded huddle transcript, also pass the shared canvas file ID as parentFileId so OpenGeni can verify the channel-to-canvas-to-transcript chain. Slack may still restrict a huddle transcript body to participants; that returns huddle_transcript_requires_participant_access. Private Slack URLs and credentials are never returned. Continue with nextOffset when truncated is true.",
|
|
953
|
+
inputSchema: {
|
|
954
|
+
connectionId: z4.string().uuid().optional(),
|
|
955
|
+
channelId: z4.string().min(1).max(64),
|
|
956
|
+
fileId: z4.string().min(1).max(64),
|
|
957
|
+
parentFileId: z4.string().min(1).max(64).optional(),
|
|
958
|
+
offset: z4.number().int().min(0).max(4_000_000).optional(),
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
async ({ connectionId, channelId, fileId, parentFileId, offset }) =>
|
|
962
|
+
json(
|
|
963
|
+
await (
|
|
964
|
+
await clientFor(connectionId)
|
|
965
|
+
).fileContent({
|
|
966
|
+
channelId,
|
|
967
|
+
fileId,
|
|
968
|
+
...(parentFileId ? { parentFileId } : {}),
|
|
969
|
+
...(offset !== undefined ? { offset } : {}),
|
|
970
|
+
}),
|
|
971
|
+
),
|
|
972
|
+
);
|
|
973
|
+
|
|
862
974
|
server.registerTool(
|
|
863
975
|
"slack_bot_post_message",
|
|
864
976
|
{
|
|
865
977
|
description:
|
|
866
|
-
"Post as the workspace-shared OpenGeni bot. Pass channelId for a channel where the bot is already a member, or userId to open/post a DM; pass exactly one. Generate one operationId UUID per intended message and reuse that same UUID on every retry.",
|
|
978
|
+
"Post as the workspace-shared OpenGeni bot. Pass channelId for a channel where the bot is already a member, or userId to open/post a DM; pass exactly one. To reply in a thread, also pass its parent message timestamp as threadTimestamp. Generate one operationId UUID per intended message and reuse that same UUID on every retry.",
|
|
867
979
|
inputSchema: {
|
|
868
980
|
connectionId: z4.string().uuid().optional(),
|
|
869
981
|
operationId: z4.string().uuid(),
|
|
870
982
|
channelId: z4.string().min(1).max(64).optional(),
|
|
871
983
|
userId: z4.string().min(1).max(64).optional(),
|
|
984
|
+
threadTimestamp: z4.string().min(1).max(64).optional(),
|
|
872
985
|
text: z4.string().min(1).max(40_000),
|
|
873
986
|
},
|
|
874
987
|
},
|
|
875
|
-
async ({ connectionId, operationId, channelId, userId, text }) => {
|
|
988
|
+
async ({ connectionId, operationId, channelId, userId, threadTimestamp, text }) => {
|
|
876
989
|
if (Boolean(channelId) === Boolean(userId)) {
|
|
877
990
|
throw new Error("exactly one of channelId or userId is required");
|
|
878
991
|
}
|
|
@@ -883,11 +996,30 @@ function registerSlackBotTools(
|
|
|
883
996
|
operationId,
|
|
884
997
|
...(channelId ? { channelId } : {}),
|
|
885
998
|
...(userId ? { userId } : {}),
|
|
999
|
+
...(threadTimestamp ? { threadTimestamp } : {}),
|
|
886
1000
|
text,
|
|
887
1001
|
}),
|
|
888
1002
|
);
|
|
889
1003
|
},
|
|
890
1004
|
);
|
|
1005
|
+
|
|
1006
|
+
server.registerTool(
|
|
1007
|
+
"slack_bot_delete_message",
|
|
1008
|
+
{
|
|
1009
|
+
description:
|
|
1010
|
+
"Delete a message authored by the workspace-shared OpenGeni bot. Pass the channel ID and exact message timestamp returned by a prior post or channel/thread read. Generate one operationId UUID per intended deletion and reuse it on every retry, including after an unknown outcome. Slack refuses deletion of messages not authored by this bot.",
|
|
1011
|
+
inputSchema: {
|
|
1012
|
+
connectionId: z4.string().uuid().optional(),
|
|
1013
|
+
operationId: z4.string().uuid(),
|
|
1014
|
+
channelId: z4.string().min(1).max(64),
|
|
1015
|
+
timestamp: z4.string().min(1).max(64),
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
async ({ connectionId, operationId, channelId, timestamp }) =>
|
|
1019
|
+
json(
|
|
1020
|
+
await (await clientFor(connectionId)).deleteMessage({ operationId, channelId, timestamp }),
|
|
1021
|
+
),
|
|
1022
|
+
);
|
|
891
1023
|
}
|
|
892
1024
|
|
|
893
1025
|
function registerToolspaceProxyTools(server: McpServer, surface: ToolspaceMcpSurface | null): void {
|
|
@@ -1150,6 +1282,70 @@ async function authorizeFirstPartySession(
|
|
|
1150
1282
|
});
|
|
1151
1283
|
}
|
|
1152
1284
|
|
|
1285
|
+
function preferenceAttemptClaims(grant: AccessGrant): {
|
|
1286
|
+
sessionId: string;
|
|
1287
|
+
turnId: string;
|
|
1288
|
+
attemptId: string;
|
|
1289
|
+
executionGeneration: number;
|
|
1290
|
+
} | null {
|
|
1291
|
+
const metadata = grant.metadata ?? {};
|
|
1292
|
+
if (
|
|
1293
|
+
typeof metadata["sessionId"] !== "string" ||
|
|
1294
|
+
typeof metadata["turnId"] !== "string" ||
|
|
1295
|
+
typeof metadata["attemptId"] !== "string" ||
|
|
1296
|
+
typeof metadata["executionGeneration"] !== "number" ||
|
|
1297
|
+
!Number.isSafeInteger(metadata["executionGeneration"]) ||
|
|
1298
|
+
metadata["executionGeneration"] < 1 ||
|
|
1299
|
+
metadata["executionGeneration"] > 2_147_483_647
|
|
1300
|
+
) {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
sessionId: metadata["sessionId"],
|
|
1305
|
+
turnId: metadata["turnId"],
|
|
1306
|
+
attemptId: metadata["attemptId"],
|
|
1307
|
+
executionGeneration: metadata["executionGeneration"],
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function registerPreferenceRegistryTools(
|
|
1312
|
+
server: McpServer,
|
|
1313
|
+
deps: ApiRouteDeps,
|
|
1314
|
+
grant: AccessGrant,
|
|
1315
|
+
json: JsonResult,
|
|
1316
|
+
): void {
|
|
1317
|
+
const attemptClaims = () => {
|
|
1318
|
+
const resolved = preferenceAttemptClaims(grant);
|
|
1319
|
+
if (!resolved) throw new Error("Exact signed preference attempt authority is required.");
|
|
1320
|
+
return {
|
|
1321
|
+
accountId: grant.accountId,
|
|
1322
|
+
workspaceId: grant.workspaceId,
|
|
1323
|
+
...resolved,
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
server.registerTool(
|
|
1328
|
+
"preference_registry_summary",
|
|
1329
|
+
{
|
|
1330
|
+
description:
|
|
1331
|
+
"List bounded deterministic descriptors for organization, workspace, and immutable initiating-human preferences frozen to this exact attempt. Full content is omitted; retrieve only a relevant returned handle.",
|
|
1332
|
+
inputSchema: {},
|
|
1333
|
+
},
|
|
1334
|
+
async () => json(await getOrCreatePreferenceRegistrySnapshot(deps.db, attemptClaims())),
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
server.registerTool(
|
|
1338
|
+
"preference_registry_get",
|
|
1339
|
+
{
|
|
1340
|
+
description:
|
|
1341
|
+
"Retrieve full content for one preference in this exact attempt snapshot. Handles from another account, workspace, human, or attempt are rejected.",
|
|
1342
|
+
inputSchema: { retrievalHandle: z4.string().min(1).max(512) },
|
|
1343
|
+
},
|
|
1344
|
+
async ({ retrievalHandle }) =>
|
|
1345
|
+
json(await getPreferenceRegistryFullContent(deps.db, attemptClaims(), retrievalHandle)),
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1153
1349
|
const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
|
|
1154
1350
|
|
|
1155
1351
|
function memoryPreview(text: string): string {
|
|
@@ -1349,7 +1545,7 @@ function registerFleetTools(
|
|
|
1349
1545
|
"run_on",
|
|
1350
1546
|
{
|
|
1351
1547
|
description:
|
|
1352
|
-
"Run a ONE-OFF op on a SPECIFIC enrolled selfhosted machine WITHOUT changing this session's active sandbox (a side-channel to another machine). Ops: exec (run a command), read (read a file), write (write a file). `target` = a selfhosted sandboxes_list `id`. To make a machine the active sandbox instead, use sandbox_swap.",
|
|
1548
|
+
"Run a ONE-OFF op on a SPECIFIC enrolled selfhosted machine WITHOUT changing this session's active sandbox (a side-channel to another machine). Ops: exec (run a command), read (read a file), write (write a file). Exec returns exact exitCode plus typed timedOut and the effective deadlineMs; a deadline kill or missing exit proof is never ok:true, and an ambiguous transport loss is not replayed. `target` = a selfhosted sandboxes_list `id`. To make a machine the active sandbox instead, use sandbox_swap.",
|
|
1353
1549
|
inputSchema: {
|
|
1354
1550
|
target: z4.string().min(1),
|
|
1355
1551
|
op: z4.discriminatedUnion("kind", [
|
|
@@ -1825,6 +2021,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1825
2021
|
rigId: z4.string().uuid().optional(),
|
|
1826
2022
|
model: z4.string().min(1).optional(),
|
|
1827
2023
|
reasoningEffort: z4.string().optional(),
|
|
2024
|
+
latencyMode: z4.enum(["standard", "priority", "fast"]).optional(),
|
|
1828
2025
|
sandboxBackend: z4.string().optional(),
|
|
1829
2026
|
// Create-time machine targeting: an enrolled sandbox id (from
|
|
1830
2027
|
// sandboxes_list) to run the spawned session on. Seeds the active-sandbox
|
package/src/mcp/session-view.ts
CHANGED
|
@@ -564,6 +564,7 @@ export function boundSessionDetailMcp(
|
|
|
564
564
|
lastSequence: session.lastSequence,
|
|
565
565
|
codexPinnedCredentialId: session.codexPinnedCredentialId,
|
|
566
566
|
codexLastCredentialId: session.codexLastCredentialId,
|
|
567
|
+
codexCompactionMode: session.codexCompactionMode,
|
|
567
568
|
pinned: session.pinned,
|
|
568
569
|
pinnedAt: session.pinnedAt,
|
|
569
570
|
pinVersion: session.pinVersion,
|
package/src/mcp/toolspace.ts
CHANGED
|
@@ -76,9 +76,12 @@ type McpTool = {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
const APPROVAL_REQUIRED_MESSAGE = "requires approval - invoke via the agent";
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const TOOLSPACE_AUTH_NEEDED_ERROR = {
|
|
80
|
+
// OpenGeni application-defined JSON-RPC code. Keep this positive so it cannot
|
|
81
|
+
// collide with MCP SDK transport errors such as RequestTimeout (-32001).
|
|
82
|
+
code: 40_101,
|
|
83
|
+
message: "Authentication required - a connection link was posted to the session.",
|
|
84
|
+
} as const;
|
|
82
85
|
const TOOLSPACE_NO_ACTIVE_TURN_MESSAGE =
|
|
83
86
|
"no active turn - toolspace calls require an in-flight turn";
|
|
84
87
|
// First-party OpenGeni MCP proxies (files/docs) route back through the same
|
|
@@ -721,7 +724,7 @@ async function callRemoteTool(
|
|
|
721
724
|
return mcpError("upstream tool result exceeded the safety limit");
|
|
722
725
|
}
|
|
723
726
|
if (isToolspaceAuthNeededError(error)) {
|
|
724
|
-
return mcpError(
|
|
727
|
+
return mcpError(TOOLSPACE_AUTH_NEEDED_ERROR.message);
|
|
725
728
|
}
|
|
726
729
|
// The raw upstream error can carry provider-specific detail; log it
|
|
727
730
|
// server-side and return only a generic result to the sandbox so no header
|
|
@@ -1037,8 +1040,8 @@ async function authNeededFetchResponse(
|
|
|
1037
1040
|
jsonrpc: "2.0",
|
|
1038
1041
|
id: request.id ?? null,
|
|
1039
1042
|
error: {
|
|
1040
|
-
code:
|
|
1041
|
-
message:
|
|
1043
|
+
code: TOOLSPACE_AUTH_NEEDED_ERROR.code,
|
|
1044
|
+
message: TOOLSPACE_AUTH_NEEDED_ERROR.message,
|
|
1042
1045
|
},
|
|
1043
1046
|
}),
|
|
1044
1047
|
{
|
|
@@ -1110,9 +1113,14 @@ function fetchInputForAttempt(input: string | URL | Request): string | URL | Req
|
|
|
1110
1113
|
}
|
|
1111
1114
|
|
|
1112
1115
|
function isToolspaceAuthNeededError(error: unknown): boolean {
|
|
1116
|
+
if (!(error instanceof Error)) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
const code = (error as { code?: unknown }).code;
|
|
1113
1120
|
return (
|
|
1114
|
-
|
|
1115
|
-
(
|
|
1116
|
-
error.message
|
|
1121
|
+
code === TOOLSPACE_AUTH_NEEDED_ERROR.code &&
|
|
1122
|
+
(error.message === TOOLSPACE_AUTH_NEEDED_ERROR.message ||
|
|
1123
|
+
error.message ===
|
|
1124
|
+
`MCP error ${TOOLSPACE_AUTH_NEEDED_ERROR.code}: ${TOOLSPACE_AUTH_NEEDED_ERROR.message}`)
|
|
1117
1125
|
);
|
|
1118
1126
|
}
|
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
OAuthStartResponse,
|
|
11
11
|
UpdateConnectionRequest,
|
|
12
12
|
} from "@opengeni/contracts";
|
|
13
|
+
import {
|
|
14
|
+
GOOGLE_DRIVE_PROVIDER_DOMAIN,
|
|
15
|
+
GoogleDriveConnectionMetadata,
|
|
16
|
+
GoogleDriveOAuthStartRequest,
|
|
17
|
+
GoogleDriveOAuthStartResponse,
|
|
18
|
+
} from "@opengeni/contracts/google-drive";
|
|
13
19
|
import {
|
|
14
20
|
hasPermission,
|
|
15
21
|
hasReservedOpenGeniSlackBotMetadata,
|
|
@@ -38,6 +44,12 @@ import {
|
|
|
38
44
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
39
45
|
import type { Hono } from "hono";
|
|
40
46
|
import { HTTPException } from "hono/http-exception";
|
|
47
|
+
import {
|
|
48
|
+
browseGoogleDrive,
|
|
49
|
+
completeGoogleDriveOAuthCallback,
|
|
50
|
+
saveGoogleDriveSource,
|
|
51
|
+
startGoogleDriveOAuth,
|
|
52
|
+
} from "../integrations/google-drive";
|
|
41
53
|
import {
|
|
42
54
|
completeMcpOAuthCallback,
|
|
43
55
|
integrationBaseUrl,
|
|
@@ -96,6 +108,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
96
108
|
const subjectId = writableSubjectId(payload.subjectId, grant.subjectId);
|
|
97
109
|
const providerDomain = canonicalProviderDomain(payload.providerDomain);
|
|
98
110
|
assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
|
|
111
|
+
assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
|
|
99
112
|
const connection = await createConnection(db, {
|
|
100
113
|
accountId: grant.accountId,
|
|
101
114
|
workspaceId,
|
|
@@ -243,6 +256,72 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
243
256
|
}
|
|
244
257
|
});
|
|
245
258
|
|
|
259
|
+
app.post("/v1/workspaces/:workspaceId/connections/google-drive/install", async (c) => {
|
|
260
|
+
assertIntegrationsEnabled();
|
|
261
|
+
const workspaceId = c.req.param("workspaceId");
|
|
262
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
263
|
+
const parsed = GoogleDriveOAuthStartRequest.safeParse(await c.req.json());
|
|
264
|
+
if (!parsed.success) {
|
|
265
|
+
throw new HTTPException(400, { message: "invalid Google Drive install request" });
|
|
266
|
+
}
|
|
267
|
+
return c.json(
|
|
268
|
+
GoogleDriveOAuthStartResponse.parse(
|
|
269
|
+
await startGoogleDriveOAuth(deps, {
|
|
270
|
+
accountId: grant.accountId,
|
|
271
|
+
workspaceId,
|
|
272
|
+
subjectId: grant.subjectId,
|
|
273
|
+
requestUrl: c.req.url,
|
|
274
|
+
payload: parsed.data,
|
|
275
|
+
}),
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
app.get("/v1/integrations/google-drive/callback", async (c) => {
|
|
281
|
+
assertIntegrationsEnabled();
|
|
282
|
+
const result = await completeGoogleDriveOAuthCallback(deps, {
|
|
283
|
+
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
284
|
+
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
285
|
+
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
286
|
+
requestUrl: c.req.url,
|
|
287
|
+
});
|
|
288
|
+
return c.redirect(result.redirectTo, 302);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
app.get(
|
|
292
|
+
"/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/browse",
|
|
293
|
+
async (c) => {
|
|
294
|
+
assertIntegrationsEnabled();
|
|
295
|
+
const workspaceId = c.req.param("workspaceId");
|
|
296
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
297
|
+
return c.json(
|
|
298
|
+
await browseGoogleDrive(deps, {
|
|
299
|
+
workspaceId,
|
|
300
|
+
subjectId: grant.subjectId,
|
|
301
|
+
connectionId: c.req.param("connectionId"),
|
|
302
|
+
parentId: c.req.query("parentId") ?? "root",
|
|
303
|
+
...(c.req.query("pageToken") ? { pageToken: c.req.query("pageToken") } : {}),
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
app.post(
|
|
310
|
+
"/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/source",
|
|
311
|
+
async (c) => {
|
|
312
|
+
assertIntegrationsEnabled();
|
|
313
|
+
const workspaceId = c.req.param("workspaceId");
|
|
314
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
315
|
+
const connection = await saveGoogleDriveSource(deps, {
|
|
316
|
+
workspaceId,
|
|
317
|
+
subjectId: grant.subjectId,
|
|
318
|
+
connectionId: c.req.param("connectionId"),
|
|
319
|
+
payload: await c.req.json(),
|
|
320
|
+
});
|
|
321
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
246
325
|
app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
247
326
|
const workspaceId = c.req.param("workspaceId");
|
|
248
327
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
@@ -274,11 +353,18 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
274
353
|
message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection",
|
|
275
354
|
});
|
|
276
355
|
}
|
|
356
|
+
if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
|
|
357
|
+
throw new HTTPException(422, {
|
|
358
|
+
message: "use the dedicated Google Drive reconnect or source-selection flow",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
277
361
|
if (existing) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
payload.kind ?? existing.kind,
|
|
362
|
+
const providerDomain = canonicalProviderDomain(
|
|
363
|
+
payload.providerDomain ?? existing.providerDomain,
|
|
281
364
|
);
|
|
365
|
+
const kind = payload.kind ?? existing.kind;
|
|
366
|
+
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
367
|
+
assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
282
368
|
}
|
|
283
369
|
// Status is not a free-form field: revocation goes through DELETE, and the
|
|
284
370
|
// broker owns needs_reauth/error. Reactivating a connection is only
|
|
@@ -415,7 +501,7 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
415
501
|
const { db, settings } = input.deps;
|
|
416
502
|
const key = requireEnvironmentEncryption(settings);
|
|
417
503
|
const credentialEncrypted = encryptCredentialBundle(key, slackBotCredentialBundle(input.token));
|
|
418
|
-
const
|
|
504
|
+
const requestedExisting = input.state.connectionId
|
|
419
505
|
? await getConnectionMetadata(
|
|
420
506
|
db,
|
|
421
507
|
input.state.workspaceId,
|
|
@@ -423,7 +509,7 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
423
509
|
input.state.subjectId,
|
|
424
510
|
)
|
|
425
511
|
: null;
|
|
426
|
-
if (input.state.connectionId && !
|
|
512
|
+
if (input.state.connectionId && !requestedExisting) {
|
|
427
513
|
throw new SlackInstallCallbackError(
|
|
428
514
|
404,
|
|
429
515
|
"connection_conflict",
|
|
@@ -431,7 +517,7 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
431
517
|
"principal_validation",
|
|
432
518
|
);
|
|
433
519
|
}
|
|
434
|
-
if (
|
|
520
|
+
if (requestedExisting && !isOpenGeniSlackBotConnection(requestedExisting)) {
|
|
435
521
|
throw new SlackInstallCallbackError(
|
|
436
522
|
422,
|
|
437
523
|
"connection_conflict",
|
|
@@ -439,7 +525,7 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
439
525
|
"principal_validation",
|
|
440
526
|
);
|
|
441
527
|
}
|
|
442
|
-
if (
|
|
528
|
+
if (requestedExisting && requestedExisting.version !== input.state.connectionVersion) {
|
|
443
529
|
throw new SlackInstallCallbackError(
|
|
444
530
|
409,
|
|
445
531
|
"connection_conflict",
|
|
@@ -447,6 +533,13 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
447
533
|
"principal_validation",
|
|
448
534
|
);
|
|
449
535
|
}
|
|
536
|
+
const existing =
|
|
537
|
+
requestedExisting ??
|
|
538
|
+
(await findMatchingOpenGeniSlackBotConnection(
|
|
539
|
+
db,
|
|
540
|
+
input.state.workspaceId,
|
|
541
|
+
input.verified.metadata,
|
|
542
|
+
));
|
|
450
543
|
const existingMetadata = existing ? openGeniSlackBotMetadata(existing.metadata) : null;
|
|
451
544
|
if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
|
|
452
545
|
throw new SlackInstallCallbackError(
|
|
@@ -526,6 +619,28 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
526
619
|
return connection;
|
|
527
620
|
}
|
|
528
621
|
|
|
622
|
+
async function findMatchingOpenGeniSlackBotConnection(
|
|
623
|
+
db: ApiRouteDeps["db"],
|
|
624
|
+
workspaceId: string,
|
|
625
|
+
verified: Awaited<ReturnType<typeof verifyOpenGeniSlackBotCredential>>["metadata"],
|
|
626
|
+
) {
|
|
627
|
+
const connections = await listConnectionsMetadata(db, workspaceId, null);
|
|
628
|
+
return (
|
|
629
|
+
connections.find((connection) => {
|
|
630
|
+
const metadata = openGeniSlackBotMetadata(connection.metadata);
|
|
631
|
+
return (
|
|
632
|
+
connection.subjectId === null &&
|
|
633
|
+
connection.providerDomain === "slack.com" &&
|
|
634
|
+
connection.kind === "app_install" &&
|
|
635
|
+
metadata?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE &&
|
|
636
|
+
metadata.slackTeamId === verified.slackTeamId &&
|
|
637
|
+
metadata.botId === verified.botId &&
|
|
638
|
+
metadata.botUserId === verified.botUserId
|
|
639
|
+
);
|
|
640
|
+
}) ?? null
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
529
644
|
function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
|
|
530
645
|
clientId: string;
|
|
531
646
|
clientSecret: string;
|
|
@@ -685,6 +800,21 @@ function assertNotDirectPersonalSlackOAuth(providerDomain: string, kind: string)
|
|
|
685
800
|
}
|
|
686
801
|
}
|
|
687
802
|
|
|
803
|
+
function assertNotDirectGoogleDriveOAuth(
|
|
804
|
+
providerDomain: string,
|
|
805
|
+
kind: string,
|
|
806
|
+
metadata: Record<string, unknown> | undefined,
|
|
807
|
+
): void {
|
|
808
|
+
if (
|
|
809
|
+
(providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN && kind === "oauth2") ||
|
|
810
|
+
GoogleDriveConnectionMetadata.safeParse(metadata).success
|
|
811
|
+
) {
|
|
812
|
+
throw new HTTPException(422, {
|
|
813
|
+
message: "Google Drive credentials must use the dedicated OAuth connection flow",
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
688
818
|
function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
|
|
689
819
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
690
820
|
throw new HTTPException(422, {
|
package/src/routes/documents.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
searchDocuments,
|
|
38
38
|
} from "@opengeni/documents";
|
|
39
39
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
40
|
-
import type { Hono } from "hono";
|
|
40
|
+
import type { Context, Hono } from "hono";
|
|
41
41
|
import { HTTPException } from "hono/http-exception";
|
|
42
42
|
import { requireAccessGrant } from "@opengeni/core";
|
|
43
43
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
@@ -252,7 +252,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
252
252
|
app.post("/v1/workspaces/:workspaceId/knowledge/search", async (c) => {
|
|
253
253
|
const workspaceId = c.req.param("workspaceId");
|
|
254
254
|
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
255
|
-
const payload =
|
|
255
|
+
const payload = await parseDocumentSearchRequest(c, "invalid knowledge search request");
|
|
256
256
|
return c.json({
|
|
257
257
|
results: await searchDocuments(
|
|
258
258
|
db,
|
|
@@ -567,6 +567,17 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
567
567
|
});
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
async function parseDocumentSearchRequest(
|
|
571
|
+
context: Context,
|
|
572
|
+
message: string,
|
|
573
|
+
): Promise<DocumentSearchRequest> {
|
|
574
|
+
const parsed = DocumentSearchRequest.safeParse(await context.req.json().catch(() => null));
|
|
575
|
+
if (!parsed.success) {
|
|
576
|
+
throw new HTTPException(422, { message });
|
|
577
|
+
}
|
|
578
|
+
return parsed.data;
|
|
579
|
+
}
|
|
580
|
+
|
|
570
581
|
/** Derive a .txt filename for a raw-text drop from its optional title/filename. */
|
|
571
582
|
function dropFilename(preferred: string | undefined): string {
|
|
572
583
|
const stem = (preferred ?? "").trim() || `note-${new Date().toISOString().slice(0, 10)}`;
|
package/src/routes/github.ts
CHANGED
|
@@ -522,10 +522,12 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
522
522
|
);
|
|
523
523
|
});
|
|
524
524
|
|
|
525
|
-
|
|
525
|
+
// This is browser navigation, like /github/connect and the OAuth callbacks.
|
|
526
|
+
// Keep it GET: a native HTML form cannot attach the API contract header that
|
|
527
|
+
// protects product mutations, and no durable binding is written here.
|
|
528
|
+
app.get("/v1/workspaces/:workspaceId/github/installations/select", async (c) => {
|
|
526
529
|
const workspaceId = c.req.param("workspaceId");
|
|
527
|
-
const
|
|
528
|
-
const state = form.get("state");
|
|
530
|
+
const state = c.req.query("state");
|
|
529
531
|
if (!state) {
|
|
530
532
|
throw new HTTPException(400, { message: "missing GitHub installation selection state" });
|
|
531
533
|
}
|
|
@@ -549,7 +551,7 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
549
551
|
message: "GitHub installation state does not match this workspace",
|
|
550
552
|
});
|
|
551
553
|
}
|
|
552
|
-
const selected =
|
|
554
|
+
const selected = c.req.query("installation_id");
|
|
553
555
|
if (selected === "new") {
|
|
554
556
|
return redirectToGitHubInstallation(c, deps, state);
|
|
555
557
|
}
|
|
@@ -781,7 +783,7 @@ function githubInstallationChooserHtml(
|
|
|
781
783
|
return `<label class="option"><input type="radio" name="installation_id" value="${installation.installationId}" required><span><strong>${account}</strong><small>${label}</small></span></label>`;
|
|
782
784
|
})
|
|
783
785
|
.join("");
|
|
784
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Choose GitHub installation</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:12px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px}p{margin:0 0 18px;color:#d4d4d8}.options{display:grid;gap:8px;margin-bottom:18px}.option{display:flex;align-items:center;gap:12px;border:1px solid #3f3f46;border-radius:8px;padding:12px;cursor:pointer}.option span{display:grid;gap:2px}.option small{color:#a1a1aa}button{min-height:38px;border-radius:7px;border:1px solid #3f3f46;padding:0 14px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;cursor:pointer}.secondary{margin-left:8px;background:transparent;color:#f4f4f5}</style></head><body><main><h1>Choose a GitHub account</h1><p>Only installations where GitHub proved you are the personal owner or an active organization owner are shown.</p><form method="
|
|
786
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Choose GitHub installation</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:12px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px}p{margin:0 0 18px;color:#d4d4d8}.options{display:grid;gap:8px;margin-bottom:18px}.option{display:flex;align-items:center;gap:12px;border:1px solid #3f3f46;border-radius:8px;padding:12px;cursor:pointer}.option span{display:grid;gap:2px}.option small{color:#a1a1aa}button{min-height:38px;border-radius:7px;border:1px solid #3f3f46;padding:0 14px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;cursor:pointer}.secondary{margin-left:8px;background:transparent;color:#f4f4f5}</style></head><body><main><h1>Choose a GitHub account</h1><p>Only installations where GitHub proved you are the personal owner or an active organization owner are shown.</p><form method="get" action="${escapeHtml(action)}"><input type="hidden" name="state" value="${escapeHtml(state)}"><div class="options">${options}</div><button type="submit">Connect selected</button><button class="secondary" type="submit" name="installation_id" value="new" formnovalidate>Install on another account</button></form></main></body></html>`;
|
|
785
787
|
}
|
|
786
788
|
|
|
787
789
|
function githubSetupPendingHtml(): string {
|