@opengeni/api-router 2.12.3 → 2.12.4-canary.1
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-552H26YK.js → chunk-VQCBJV4P.js} +30190 -925
- package/dist/chunk-VQCBJV4P.js.map +1 -0
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/marketplace-plugin.d.ts +2691 -0
- package/dist/integrations/oauth-client.d.ts +5 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/sandbox/metrics-ingestion.d.ts +1 -0
- package/dist/sandbox/rematerialize.d.ts +1 -1
- package/package.json +19 -19
- package/src/integrations/marketplace-plugin.ts +48 -0
- package/src/integrations/oauth-client.ts +131 -3
- package/src/routes/browser-sessions.ts +1 -0
- package/src/routes/capabilities.ts +50 -0
- package/src/routes/computer-sessions.ts +1 -0
- package/src/routes/plugins.ts +158 -11
- package/src/routes/skills.ts +19 -2
- package/src/sandbox/channel-a.ts +1 -0
- package/src/sandbox/machines.ts +1 -0
- package/src/sandbox/metrics-ingestion.ts +2 -1
- package/src/sandbox/rematerialize.ts +33 -1
- package/src/transcription/segmenter.ts +8 -2
- package/dist/chunk-552H26YK.js.map +0 -1
|
@@ -60,6 +60,11 @@ export declare function requireIntegrationsStateSecret(settings: Settings): stri
|
|
|
60
60
|
export declare function assertSlackAuthorizationServer(as: AuthorizationServerMetadata): void;
|
|
61
61
|
/** The official-Gmail profile's origin pins, kept exported for its tests. */
|
|
62
62
|
export declare function assertGoogleAuthorizationServer(as: AuthorizationServerMetadata): void;
|
|
63
|
+
/** Inspect without credentials, registration, or running tools. */
|
|
64
|
+
export declare function inspectMcpAuthentication(resource: string, settings: Settings): Promise<{
|
|
65
|
+
kind: "oauth2" | "none" | "unknown";
|
|
66
|
+
message?: string;
|
|
67
|
+
}>;
|
|
63
68
|
export declare function preferredOAuthSelfRegistration(as: Pick<AuthorizationServerMetadata, "clientIdMetadataDocumentSupported" | "registrationEndpoint">, clientSource: OAuthProviderProfile["clientSource"]): "cimd" | "dcr" | null;
|
|
64
69
|
export declare function buildAuthorizationUrl(input: {
|
|
65
70
|
endpoint: string;
|
|
@@ -82,8 +82,8 @@ export declare function scheduledTaskMcpSummary(task: ScheduledTask): {
|
|
|
82
82
|
runMode: "existing_session" | "new_session_per_run" | "reusable_session";
|
|
83
83
|
overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
|
|
84
84
|
action: {
|
|
85
|
+
readonly sourceId?: never;
|
|
85
86
|
kind: "agent_turn";
|
|
86
|
-
sourceId?: never;
|
|
87
87
|
sourceGeneration?: never;
|
|
88
88
|
sourceLifecycleGeneration?: never;
|
|
89
89
|
destination?: never;
|
|
@@ -205,8 +205,8 @@ export declare function boundScheduledTaskDetailMcp(task: ScheduledTask): {
|
|
|
205
205
|
runMode: "existing_session" | "new_session_per_run" | "reusable_session";
|
|
206
206
|
overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
|
|
207
207
|
action: {
|
|
208
|
+
readonly sourceId?: never;
|
|
208
209
|
kind: "agent_turn";
|
|
209
|
-
sourceId?: never;
|
|
210
210
|
sourceGeneration?: never;
|
|
211
211
|
sourceLifecycleGeneration?: never;
|
|
212
212
|
destination?: never;
|
|
@@ -389,8 +389,8 @@ export declare function boundScheduledTaskMcpPage(input: {
|
|
|
389
389
|
runMode: "existing_session" | "new_session_per_run" | "reusable_session";
|
|
390
390
|
overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
|
|
391
391
|
action: {
|
|
392
|
+
readonly sourceId?: never;
|
|
392
393
|
kind: "agent_turn";
|
|
393
|
-
sourceId?: never;
|
|
394
394
|
sourceGeneration?: never;
|
|
395
395
|
sourceLifecycleGeneration?: never;
|
|
396
396
|
destination?: never;
|
|
@@ -86,6 +86,7 @@ export declare function helloReportsDisplay(hello: Hello): boolean;
|
|
|
86
86
|
export declare function helloDesktopUnavailableReason(hello: Hello): string | null;
|
|
87
87
|
/** Whether the runner's current Hello advertises the op-stream engine. */
|
|
88
88
|
export declare function helloReportsOpStream(hello: Hello): boolean;
|
|
89
|
+
export declare function helloRuntimeCapabilities(hello: Hello): Record<string, boolean>;
|
|
89
90
|
/**
|
|
90
91
|
* Reconcile `enrollments.has_display` (+ the capture-blocked reason) to what a Hello
|
|
91
92
|
* reports. Resolves the enrollment (the accountId is the RLS principal + the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
2
|
import { type Database, type LeaseSnapshot } from "@opengeni/db";
|
|
3
3
|
import { type EstablishedSandboxSession } from "@opengeni/runtime/sandbox";
|
|
4
|
-
import type
|
|
4
|
+
import { type ObjectStorage } from "@opengeni/storage";
|
|
5
5
|
/** The sole API-direct cold->warming owner path used by Channel A and viewer
|
|
6
6
|
* attach. It never publishes warm until archive identity, hydrated tree, command
|
|
7
7
|
* routing, provider identity, and the selected rematerialization attempt all
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.4-canary.1",
|
|
4
4
|
"description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,24 +43,24 @@
|
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@llamaindex/liteparse": "2.14.2",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
-
"@opengeni/agent-proto": "^0.5.1",
|
|
47
|
-
"@opengeni/artifact-tool": "^0.3.
|
|
48
|
-
"@opengeni/capabilities": "^0.3.3",
|
|
49
|
-
"@opengeni/codemode": "^0.5.
|
|
50
|
-
"@opengeni/codex": "^0.2.22",
|
|
51
|
-
"@opengeni/config": "^1.1.1",
|
|
52
|
-
"@opengeni/contracts": "^3.0.1",
|
|
53
|
-
"@opengeni/core": "^2.9.
|
|
54
|
-
"@opengeni/db": "^4.3.
|
|
55
|
-
"@opengeni/documents": "^0.8.
|
|
56
|
-
"@opengeni/events": "^0.4.
|
|
57
|
-
"@opengeni/github": "^0.7.
|
|
58
|
-
"@opengeni/network": "^0.3.1",
|
|
59
|
-
"@opengeni/observability": "^0.8.
|
|
60
|
-
"@opengeni/runtime": "^2.5.
|
|
61
|
-
"@opengeni/storage": "^0.2.
|
|
62
|
-
"@opengeni/tool-gateway": "^0.1.
|
|
63
|
-
"@opengeni/xai-subscription": "^0.1.4",
|
|
46
|
+
"@opengeni/agent-proto": "^0.5.1-canary.27",
|
|
47
|
+
"@opengeni/artifact-tool": "^0.3.26-canary.1",
|
|
48
|
+
"@opengeni/capabilities": "^0.3.3-canary.9",
|
|
49
|
+
"@opengeni/codemode": "^0.5.6-canary.1",
|
|
50
|
+
"@opengeni/codex": "^0.2.22-canary.9",
|
|
51
|
+
"@opengeni/config": "^1.1.2-canary.1",
|
|
52
|
+
"@opengeni/contracts": "^3.0.2-canary.1",
|
|
53
|
+
"@opengeni/core": "^2.9.4-canary.1",
|
|
54
|
+
"@opengeni/db": "^4.3.3-canary.1",
|
|
55
|
+
"@opengeni/documents": "^0.8.27-canary.1",
|
|
56
|
+
"@opengeni/events": "^0.4.25-canary.1",
|
|
57
|
+
"@opengeni/github": "^0.7.10-canary.1",
|
|
58
|
+
"@opengeni/network": "^0.3.1-canary.9",
|
|
59
|
+
"@opengeni/observability": "^0.8.26-canary.1",
|
|
60
|
+
"@opengeni/runtime": "^2.5.3-canary.1",
|
|
61
|
+
"@opengeni/storage": "^0.2.127-canary.1",
|
|
62
|
+
"@opengeni/tool-gateway": "^0.1.7-canary.1",
|
|
63
|
+
"@opengeni/xai-subscription": "^0.1.4-canary.9",
|
|
64
64
|
"@temporalio/client": "^1.17.0",
|
|
65
65
|
"better-auth": "^1.6.14",
|
|
66
66
|
"hono": "^4.12.18",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import snapshot from "../../../../data/catalog/plugins-snapshot.json";
|
|
2
|
+
import { PluginManifest, pluginMcpUnavailableReason } from "@opengeni/contracts";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
|
|
5
|
+
/** Adapt only indexed portable components. Never silently drop required components. */
|
|
6
|
+
export function marketplacePlugin(url: string) {
|
|
7
|
+
const source = snapshot.sources.find((candidate) =>
|
|
8
|
+
candidate.entries.some((item) => item.sourceUrl === url),
|
|
9
|
+
);
|
|
10
|
+
const item = source?.entries.find((candidate) => candidate.sourceUrl === url);
|
|
11
|
+
if (!item || !source) return null;
|
|
12
|
+
if (!item.skills || !item.mcpServers)
|
|
13
|
+
throw new HTTPException(422, {
|
|
14
|
+
message: "This external plugin must be indexed before it can be installed.",
|
|
15
|
+
});
|
|
16
|
+
const servers = item.mcpServers
|
|
17
|
+
.filter((server) => pluginMcpUnavailableReason(server) === null)
|
|
18
|
+
.map((server, index) => ({
|
|
19
|
+
id: "marketplace-" + source.provider + "-" + item.name + "-" + index,
|
|
20
|
+
name: server.name,
|
|
21
|
+
url: server.endpoint!,
|
|
22
|
+
cacheToolsList: true,
|
|
23
|
+
}));
|
|
24
|
+
if (!item.skills.length && !servers.length)
|
|
25
|
+
throw new HTTPException(422, {
|
|
26
|
+
message: "This plugin has no skills or remote MCP servers that OpenGeni can install.",
|
|
27
|
+
});
|
|
28
|
+
const manifest = PluginManifest.parse({
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
pluginKey: "marketplace/" + source.provider + "/" + item.name,
|
|
31
|
+
name: item.displayName,
|
|
32
|
+
description: item.description.slice(0, 4000),
|
|
33
|
+
version: item.version || source.revision,
|
|
34
|
+
components: [
|
|
35
|
+
...item.skills.map((skill, index) => ({
|
|
36
|
+
key: "skill-" + index,
|
|
37
|
+
kind: "skill",
|
|
38
|
+
url: skill.sourceUrl,
|
|
39
|
+
})),
|
|
40
|
+
...servers.map((server, index) => ({
|
|
41
|
+
key: "mcp-" + index,
|
|
42
|
+
kind: "mcp",
|
|
43
|
+
serverId: server.id,
|
|
44
|
+
})),
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
return { manifest, servers, discovery: { ...item, provider: source.provider } };
|
|
48
|
+
}
|
|
@@ -961,10 +961,136 @@ export function assertGoogleAuthorizationServer(as: AuthorizationServerMetadata)
|
|
|
961
961
|
}
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
+
/** Inspect without credentials, registration, or running tools. */
|
|
965
|
+
export async function inspectMcpAuthentication(
|
|
966
|
+
resource: string,
|
|
967
|
+
settings: Settings,
|
|
968
|
+
): Promise<{
|
|
969
|
+
kind: "oauth2" | "none" | "unknown";
|
|
970
|
+
message?: string;
|
|
971
|
+
}> {
|
|
972
|
+
const deadline = new OAuthStartDeadline(12_000);
|
|
973
|
+
try {
|
|
974
|
+
const response = await deadline.run("mcp_challenge", (signal) =>
|
|
975
|
+
fetchOAuth(resource, settings, {
|
|
976
|
+
method: "POST",
|
|
977
|
+
headers: {
|
|
978
|
+
accept: "application/json, text/event-stream",
|
|
979
|
+
"content-type": "application/json",
|
|
980
|
+
},
|
|
981
|
+
body: JSON.stringify({
|
|
982
|
+
jsonrpc: "2.0",
|
|
983
|
+
id: "auth-discovery",
|
|
984
|
+
method: "initialize",
|
|
985
|
+
params: {
|
|
986
|
+
protocolVersion: "2025-03-26",
|
|
987
|
+
capabilities: {},
|
|
988
|
+
clientInfo: { name: "OpenGeni", version: "1.0" },
|
|
989
|
+
},
|
|
990
|
+
}),
|
|
991
|
+
signal,
|
|
992
|
+
}),
|
|
993
|
+
);
|
|
994
|
+
const challenge = parseMcpOAuthChallenge(response.headers.get("www-authenticate"));
|
|
995
|
+
let initialized = false;
|
|
996
|
+
if (response.ok) {
|
|
997
|
+
initialized = await deadline.run("mcp_challenge", async () => {
|
|
998
|
+
const reader = response.body?.getReader();
|
|
999
|
+
if (!reader) return false;
|
|
1000
|
+
const decoder = new TextDecoder();
|
|
1001
|
+
let text = "";
|
|
1002
|
+
let bytes = 0;
|
|
1003
|
+
const sse = response.headers.get("content-type")?.includes("text/event-stream");
|
|
1004
|
+
try {
|
|
1005
|
+
while (true) {
|
|
1006
|
+
const chunk = await reader.read();
|
|
1007
|
+
if (chunk.done) return false;
|
|
1008
|
+
bytes += chunk.value.byteLength;
|
|
1009
|
+
if (bytes > 262144) return false;
|
|
1010
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
1011
|
+
const messages = sse
|
|
1012
|
+
? text
|
|
1013
|
+
.split(/\r?\n/)
|
|
1014
|
+
.filter((line) => line.startsWith("data:"))
|
|
1015
|
+
.map((line) => line.slice(5).trim())
|
|
1016
|
+
: [text];
|
|
1017
|
+
for (const message of messages) {
|
|
1018
|
+
try {
|
|
1019
|
+
const value = JSON.parse(message);
|
|
1020
|
+
if (
|
|
1021
|
+
value.id === "auth-discovery" &&
|
|
1022
|
+
typeof value.result?.protocolVersion === "string" &&
|
|
1023
|
+
value.result?.serverInfo &&
|
|
1024
|
+
value.result?.capabilities
|
|
1025
|
+
)
|
|
1026
|
+
return true;
|
|
1027
|
+
} catch {
|
|
1028
|
+
/* Wait for a complete JSON message. */
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} finally {
|
|
1033
|
+
await reader.cancel().catch(() => undefined);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
} else {
|
|
1037
|
+
await cancelResponseBody(response);
|
|
1038
|
+
}
|
|
1039
|
+
let metadataRead = false;
|
|
1040
|
+
let metadataAbsent = true;
|
|
1041
|
+
try {
|
|
1042
|
+
await resolveMcpOAuthDiscovery({
|
|
1043
|
+
resourceUrl: resource,
|
|
1044
|
+
challenge,
|
|
1045
|
+
fetchMetadata: async ({ url }) => {
|
|
1046
|
+
try {
|
|
1047
|
+
const result = await deadline.run("protected_resource_metadata", (signal) =>
|
|
1048
|
+
fetchOAuthMetadata(url, settings, signal),
|
|
1049
|
+
);
|
|
1050
|
+
metadataRead = true;
|
|
1051
|
+
if (result.status !== "absent") metadataAbsent = false;
|
|
1052
|
+
return result;
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
metadataAbsent = false;
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
validateEndpoint: (url, label) => oauthEndpointUrl(url, settings, label),
|
|
1059
|
+
canonicalizeResource: canonicalOAuthResource,
|
|
1060
|
+
});
|
|
1061
|
+
return { kind: "oauth2" };
|
|
1062
|
+
} catch {
|
|
1063
|
+
// Public initialization does not exclude optional or tool-level OAuth.
|
|
1064
|
+
// Only explicit absence of metadata permits the unauthenticated path.
|
|
1065
|
+
if (
|
|
1066
|
+
initialized &&
|
|
1067
|
+
!challenge.scheme &&
|
|
1068
|
+
!challenge.resourceMetadata &&
|
|
1069
|
+
metadataRead &&
|
|
1070
|
+
metadataAbsent
|
|
1071
|
+
)
|
|
1072
|
+
return { kind: "none" };
|
|
1073
|
+
return {
|
|
1074
|
+
kind: "unknown",
|
|
1075
|
+
message:
|
|
1076
|
+
"This server's sign-in requirements could not be determined. Check its setup instructions.",
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
} catch {
|
|
1080
|
+
return {
|
|
1081
|
+
kind: "unknown",
|
|
1082
|
+
message: "Could not check this server. Retry or consult its setup instructions.",
|
|
1083
|
+
};
|
|
1084
|
+
} finally {
|
|
1085
|
+
deadline.dispose();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
964
1089
|
async function discoverMcpOAuth(
|
|
965
1090
|
resource: string,
|
|
966
1091
|
settings: Settings,
|
|
967
1092
|
deadline: OAuthStartDeadline,
|
|
1093
|
+
knownChallenge?: WwwAuthenticateChallenge,
|
|
968
1094
|
): Promise<{
|
|
969
1095
|
challenge: WwwAuthenticateChallenge;
|
|
970
1096
|
prm: ProtectedResourceMetadata;
|
|
@@ -977,9 +1103,11 @@ async function discoverMcpOAuth(
|
|
|
977
1103
|
metadataSha256: string;
|
|
978
1104
|
};
|
|
979
1105
|
}> {
|
|
980
|
-
const challenge =
|
|
981
|
-
|
|
982
|
-
|
|
1106
|
+
const challenge =
|
|
1107
|
+
knownChallenge ??
|
|
1108
|
+
(await deadline.run("mcp_challenge", (signal) =>
|
|
1109
|
+
probeMcpChallenge(resource, settings, signal),
|
|
1110
|
+
));
|
|
983
1111
|
try {
|
|
984
1112
|
const discovery = await resolveMcpOAuthDiscovery({
|
|
985
1113
|
resourceUrl: resource,
|
|
@@ -2703,6 +2703,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2703
2703
|
operationResourcePolicySupported:
|
|
2704
2704
|
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
2705
2705
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
2706
|
+
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
2706
2707
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
2707
2708
|
enrollment.opStream === true &&
|
|
2708
2709
|
deps.bus.getOpStreamConnection
|
|
@@ -16,9 +16,59 @@ import {
|
|
|
16
16
|
officialMcpRegistryUrl,
|
|
17
17
|
} from "@opengeni/core";
|
|
18
18
|
import { boundedLimit } from "../http/common";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import pluginSnapshot from "../../../../data/catalog/plugins-snapshot.json";
|
|
21
|
+
const discoverablePlugins = pluginSnapshot.sources
|
|
22
|
+
.flatMap((source) => source.entries.map((entry) => ({ ...entry, provider: source.provider })))
|
|
23
|
+
.sort((a, b) => a.displayName.localeCompare(b.displayName) || a.id.localeCompare(b.id));
|
|
24
|
+
import { inspectMcpAuthentication } from "../integrations/oauth-client";
|
|
19
25
|
|
|
20
26
|
export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
21
27
|
const { db, settings } = deps;
|
|
28
|
+
app.get("/v1/workspaces/:workspaceId/capabilities/discovery/plugins", async (c) => {
|
|
29
|
+
await requireAccessGrant(c, deps, c.req.param("workspaceId"), "workspace:read");
|
|
30
|
+
const query = (c.req.query("query") ?? "").trim().toLowerCase();
|
|
31
|
+
const provider = c.req.query("provider");
|
|
32
|
+
const id = c.req.query("id");
|
|
33
|
+
const offset = z.coerce
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.min(0)
|
|
37
|
+
.max(10000)
|
|
38
|
+
.parse(c.req.query("offset") ?? 0);
|
|
39
|
+
const matches = discoverablePlugins.filter(
|
|
40
|
+
(item) =>
|
|
41
|
+
(!id || item.id === id) &&
|
|
42
|
+
(!provider || item.provider === provider) &&
|
|
43
|
+
query
|
|
44
|
+
.split(/\s+/)
|
|
45
|
+
.every((term) =>
|
|
46
|
+
[
|
|
47
|
+
item.displayName,
|
|
48
|
+
item.name,
|
|
49
|
+
item.description,
|
|
50
|
+
item.category,
|
|
51
|
+
item.author?.name,
|
|
52
|
+
...item.keywords,
|
|
53
|
+
...(item.components ?? []),
|
|
54
|
+
]
|
|
55
|
+
.join(" ")
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.includes(term),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
const end = offset + 40;
|
|
61
|
+
return c.json({
|
|
62
|
+
items: matches.slice(offset, end),
|
|
63
|
+
total: matches.length,
|
|
64
|
+
nextOffset: end < matches.length ? end : null,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
app.post("/v1/workspaces/:workspaceId/capabilities/discovery/mcp-auth", async (c) => {
|
|
68
|
+
await requireAccessGrant(c, deps, c.req.param("workspaceId"), "workspace:read");
|
|
69
|
+
const { url } = z.object({ url: z.string().url().max(2048) }).parse(await c.req.json());
|
|
70
|
+
return c.json(await inspectMcpAuthentication(url, settings));
|
|
71
|
+
});
|
|
22
72
|
|
|
23
73
|
app.get("/v1/workspaces/:workspaceId/capabilities", async (c) => {
|
|
24
74
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -979,6 +979,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
979
979
|
operationResourcePolicySupported:
|
|
980
980
|
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
981
981
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
982
|
+
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
982
983
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
983
984
|
enrollment.opStream === true &&
|
|
984
985
|
deps.bus.getOpStreamConnection
|
package/src/routes/plugins.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import pluginSnapshot from "../../../../data/catalog/plugins-snapshot.json";
|
|
2
|
+
import { marketplacePlugin } from "../integrations/marketplace-plugin";
|
|
3
|
+
import { mcpEndpointIdentity } from "@opengeni/contracts";
|
|
4
|
+
import { buildCapabilityCatalog, createCatalogItem } from "@opengeni/core";
|
|
1
5
|
import { createHash } from "node:crypto";
|
|
2
6
|
|
|
3
7
|
import {
|
|
@@ -101,11 +105,80 @@ export function registerPluginRoutes(
|
|
|
101
105
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
102
106
|
return c.json(
|
|
103
107
|
ListInstalledPluginsResponse.parse({
|
|
104
|
-
plugins: await listInstalledPluginPackages(deps.db, workspaceId)
|
|
108
|
+
plugins: (await listInstalledPluginPackages(deps.db, workspaceId)).map((plugin) => {
|
|
109
|
+
const source = pluginSnapshot.sources.find((candidate) =>
|
|
110
|
+
plugin.pluginKey.startsWith("marketplace/" + candidate.provider + "/"),
|
|
111
|
+
);
|
|
112
|
+
const entry = source?.entries.find(
|
|
113
|
+
(candidate) =>
|
|
114
|
+
plugin.pluginKey === "marketplace/" + source!.provider + "/" + candidate.name,
|
|
115
|
+
);
|
|
116
|
+
return { ...plugin, logoUrl: plugin.logoUrl ?? entry?.logoUrl ?? null };
|
|
117
|
+
}),
|
|
105
118
|
}),
|
|
106
119
|
);
|
|
107
120
|
});
|
|
108
121
|
|
|
122
|
+
app.get("/v1/workspaces/:workspaceId/plugins/details", async (c) => {
|
|
123
|
+
const workspaceId = c.req.param("workspaceId");
|
|
124
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
125
|
+
const plugin = await getInstalledPluginPackage(
|
|
126
|
+
deps.db,
|
|
127
|
+
workspaceId,
|
|
128
|
+
c.req.query("pluginKey") ?? "",
|
|
129
|
+
);
|
|
130
|
+
if (!plugin || plugin.status === "disabled")
|
|
131
|
+
throw new HTTPException(404, { message: "Plugin is not installed" });
|
|
132
|
+
const stored = plugin.manifest as Record<string, any>;
|
|
133
|
+
if (stored.discovery) return c.json(stored.discovery);
|
|
134
|
+
const catalog = await buildCapabilityCatalog({
|
|
135
|
+
db: deps.db,
|
|
136
|
+
workspaceId,
|
|
137
|
+
settings: deps.settings,
|
|
138
|
+
subjectId: grant.subjectId,
|
|
139
|
+
});
|
|
140
|
+
const members = Array.isArray(stored.components) ? stored.components : [];
|
|
141
|
+
return c.json({
|
|
142
|
+
id: String(stored.pluginKey ?? "installed")
|
|
143
|
+
.replace("marketplace/", "")
|
|
144
|
+
.replace("/", ":"),
|
|
145
|
+
name: stored.name,
|
|
146
|
+
displayName: stored.name,
|
|
147
|
+
description: stored.description ?? "",
|
|
148
|
+
longDescription: stored.description ?? "",
|
|
149
|
+
provider: "custom",
|
|
150
|
+
category: stored.category ?? null,
|
|
151
|
+
logoUrl: null,
|
|
152
|
+
darkLogoUrl: null,
|
|
153
|
+
sourceUrl: stored.sourceUrl ?? null,
|
|
154
|
+
author: null,
|
|
155
|
+
version: stored.version,
|
|
156
|
+
skills: members
|
|
157
|
+
.filter((member) => member.kind === "skill")
|
|
158
|
+
.map((member) => ({
|
|
159
|
+
name: decodeURIComponent(member.url.split("/").filter(Boolean).at(-2) ?? member.key),
|
|
160
|
+
sourceUrl: member.url,
|
|
161
|
+
})),
|
|
162
|
+
mcpServers: members
|
|
163
|
+
.filter((member) => member.kind === "mcp")
|
|
164
|
+
.map((member) => {
|
|
165
|
+
const bom = Array.isArray(stored.bom)
|
|
166
|
+
? stored.bom.find((entry: any) => entry.key === member.key)
|
|
167
|
+
: null;
|
|
168
|
+
const item = catalog.items.find((candidate) => candidate.id === bom?.capabilityId);
|
|
169
|
+
return {
|
|
170
|
+
name: item?.name ?? member.serverId,
|
|
171
|
+
transport: item?.transport ?? "http",
|
|
172
|
+
endpoint: item?.endpointUrl ?? null,
|
|
173
|
+
};
|
|
174
|
+
}),
|
|
175
|
+
components: [
|
|
176
|
+
...new Set(members.map((member) => (member.kind === "skill" ? "skills" : member.kind))),
|
|
177
|
+
],
|
|
178
|
+
installation: "installed",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
109
182
|
app.post("/v1/workspaces/:workspaceId/plugins/preview", async (c) => {
|
|
110
183
|
const workspaceId = c.req.param("workspaceId");
|
|
111
184
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
@@ -329,15 +402,20 @@ async function resolvePluginPackage(input: {
|
|
|
329
402
|
>;
|
|
330
403
|
}): Promise<ResolvedPluginPackage> {
|
|
331
404
|
let manifest;
|
|
405
|
+
const marketplace = marketplacePlugin(input.url);
|
|
332
406
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
407
|
+
if (marketplace) {
|
|
408
|
+
manifest = marketplace.manifest;
|
|
409
|
+
} else {
|
|
410
|
+
const bytes = await fetchIntegrationSourceDocument(
|
|
411
|
+
input.transport,
|
|
412
|
+
input.url,
|
|
413
|
+
MAX_PLUGIN_MANIFEST_BYTES,
|
|
414
|
+
);
|
|
415
|
+
manifest = PluginManifest.parse(
|
|
416
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
341
419
|
} catch (error) {
|
|
342
420
|
if (error instanceof HTTPException) throw error;
|
|
343
421
|
throw new HTTPException(422, {
|
|
@@ -346,6 +424,14 @@ async function resolvePluginPackage(input: {
|
|
|
346
424
|
}
|
|
347
425
|
const manifestDigest = sha256(stableJson(manifest));
|
|
348
426
|
const components: ResolvedPluginComponent[] = [];
|
|
427
|
+
const catalog = marketplace
|
|
428
|
+
? await buildCapabilityCatalog({
|
|
429
|
+
db: input.deps.db,
|
|
430
|
+
workspaceId: input.workspaceId,
|
|
431
|
+
settings: input.deps.settings,
|
|
432
|
+
subjectId: input.subjectId,
|
|
433
|
+
})
|
|
434
|
+
: null;
|
|
349
435
|
for (const component of manifest.components) {
|
|
350
436
|
if (component.kind === "skill") {
|
|
351
437
|
const resolved = await resolveSkillImport(component.url, input.github);
|
|
@@ -511,7 +597,62 @@ async function resolvePluginPackage(input: {
|
|
|
511
597
|
});
|
|
512
598
|
continue;
|
|
513
599
|
}
|
|
514
|
-
const server = configuredMcpServer(
|
|
600
|
+
const server = configuredMcpServer(
|
|
601
|
+
[...(marketplace?.servers ?? []), ...input.deps.settings.mcpServers],
|
|
602
|
+
component.serverId,
|
|
603
|
+
);
|
|
604
|
+
if (marketplace) {
|
|
605
|
+
const endpoint = mcpEndpointIdentity(server.url)!;
|
|
606
|
+
const matches = (catalog?.items ?? []).filter(
|
|
607
|
+
(item) => item.kind === "mcp" && mcpEndpointIdentity(item.endpointUrl) === endpoint,
|
|
608
|
+
);
|
|
609
|
+
// Prefer the original catalogue connection over legacy plugin-created duplicates.
|
|
610
|
+
matches.sort(
|
|
611
|
+
(a, b) =>
|
|
612
|
+
Number(a.id.startsWith("mcp:configured:marketplace-")) -
|
|
613
|
+
Number(b.id.startsWith("mcp:configured:marketplace-")) ||
|
|
614
|
+
Number(b.enabled) - Number(a.enabled) ||
|
|
615
|
+
a.id.localeCompare(b.id),
|
|
616
|
+
);
|
|
617
|
+
const existing = matches[0];
|
|
618
|
+
const capabilityId = existing?.id ?? "mcp:endpoint:" + sha256(endpoint).slice(0, 24);
|
|
619
|
+
const digest = sha256(stableJson({ endpoint, capabilityId }));
|
|
620
|
+
components.push({
|
|
621
|
+
preview: {
|
|
622
|
+
key: component.key,
|
|
623
|
+
kind: "mcp",
|
|
624
|
+
name: existing?.name ?? server.name ?? server.id,
|
|
625
|
+
capabilityId,
|
|
626
|
+
digest,
|
|
627
|
+
connectionRequired: !existing?.enabled,
|
|
628
|
+
connectionId: null,
|
|
629
|
+
instanceKey: null,
|
|
630
|
+
displayName: null,
|
|
631
|
+
facts: { endpointUrl: endpoint, connectionManagedSeparately: true },
|
|
632
|
+
},
|
|
633
|
+
install: async () => {
|
|
634
|
+
if (!existing)
|
|
635
|
+
await createCatalogItem({
|
|
636
|
+
db: input.deps.db,
|
|
637
|
+
accountId: input.accountId,
|
|
638
|
+
workspaceId: input.workspaceId,
|
|
639
|
+
payload: {
|
|
640
|
+
id: capabilityId,
|
|
641
|
+
kind: "mcp",
|
|
642
|
+
source: "manual",
|
|
643
|
+
name: server.name ?? server.id,
|
|
644
|
+
endpointUrl: endpoint,
|
|
645
|
+
category: "integrations",
|
|
646
|
+
tags: ["mcp"],
|
|
647
|
+
metadata: { authDiscovery: "unknown" },
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
// Connections are independently owned: installing/removing a plugin never signs in or disconnects an account.
|
|
651
|
+
return { facetInstallationIds: [] };
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
515
656
|
const digest = sha256(stableJson(safeMcpFacts(server)));
|
|
516
657
|
components.push({
|
|
517
658
|
preview: {
|
|
@@ -584,7 +725,13 @@ async function resolvePluginPackage(input: {
|
|
|
584
725
|
components: components.map((component) => component.preview),
|
|
585
726
|
diff,
|
|
586
727
|
}),
|
|
587
|
-
storedManifest: {
|
|
728
|
+
storedManifest: {
|
|
729
|
+
...manifest,
|
|
730
|
+
...(marketplace ? { discovery: marketplace.discovery } : {}),
|
|
731
|
+
sourceUrl: input.url,
|
|
732
|
+
manifestDigest,
|
|
733
|
+
bom,
|
|
734
|
+
},
|
|
588
735
|
components,
|
|
589
736
|
};
|
|
590
737
|
}
|
package/src/routes/skills.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from "@opengeni/contracts";
|
|
12
12
|
import {
|
|
13
13
|
portableSkillCapabilityId,
|
|
14
|
+
createPublicSkillSearchClient,
|
|
15
|
+
PublicSkillSearchError,
|
|
14
16
|
portableSkillPluginKey,
|
|
15
17
|
requireAccessGrant,
|
|
16
18
|
requireAccessGrantAuthorization,
|
|
@@ -48,6 +50,22 @@ export function registerSkillRoutes(
|
|
|
48
50
|
): void {
|
|
49
51
|
registerSkillContentRoutes(app, deps);
|
|
50
52
|
const github = overrides.github ?? createGitHubSkillSourceClient(deps.settings);
|
|
53
|
+
const discovery = createPublicSkillSearchClient(deps.settings);
|
|
54
|
+
app.get("/v1/workspaces/:workspaceId/skills/search", async (c) => {
|
|
55
|
+
await requireAccessGrant(c, deps, c.req.param("workspaceId"), "workspace:read");
|
|
56
|
+
try {
|
|
57
|
+
return c.json(await discovery.search({ query: c.req.query("q") ?? "", limit: 20 }));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!(error instanceof PublicSkillSearchError)) throw error;
|
|
60
|
+
if (error.retryAfterSeconds) c.header("Retry-After", String(error.retryAfterSeconds));
|
|
61
|
+
throw new HTTPException(
|
|
62
|
+
error.code === "invalid_query" ? 422 : error.code === "rate_limited" ? 429 : 503,
|
|
63
|
+
{
|
|
64
|
+
message: "Skill search is unavailable. Please try again.",
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
51
69
|
|
|
52
70
|
app.get("/v1/workspaces/:workspaceId/skills", async (c) => {
|
|
53
71
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -165,8 +183,7 @@ export function registerSkillRoutes(
|
|
|
165
183
|
resolved.preview.contentSha256 !== payload.expectedContentSha256
|
|
166
184
|
) {
|
|
167
185
|
throw new HTTPException(409, {
|
|
168
|
-
message:
|
|
169
|
-
"The Skill source changed after preview. Review the new commit and contents before installing.",
|
|
186
|
+
message: "The skill changed after preview. Review its contents again before installing.",
|
|
170
187
|
});
|
|
171
188
|
}
|
|
172
189
|
const fileSummaryByPath = new Map(
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -727,6 +727,7 @@ async function withChannelAOperation<T>(
|
|
|
727
727
|
operationResourcePolicySupported:
|
|
728
728
|
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
729
729
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
730
|
+
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
730
731
|
...(settings.agentOpStreamEnabled === true &&
|
|
731
732
|
enrollment?.opStream === true &&
|
|
732
733
|
bus.getOpStreamConnection
|
package/src/sandbox/machines.ts
CHANGED
|
@@ -112,6 +112,7 @@ function runtimeFor(settings: Settings, enrollment: EnrollmentRecord): MachineVi
|
|
|
112
112
|
browserBridge: capability("browserBridge"),
|
|
113
113
|
operationResourcePolicy: capability("operationResourcePolicy"),
|
|
114
114
|
operationCpuQuota: capability("operationCpuQuota"),
|
|
115
|
+
transactionalFsWrite: capability("transactionalFsWrite"),
|
|
115
116
|
},
|
|
116
117
|
update,
|
|
117
118
|
};
|