@agent-native/dispatch 0.40.40 → 0.40.41-beta-dispatch-embed-01a15dc9-20261027050558
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/CHANGELOG.md +42 -0
- package/dist/actions/create_embed_session.js +1 -0
- package/dist/actions/create_embed_session.js.map +1 -1
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/provider-api-register.d.ts +13 -13
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -11
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +23 -10
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +2 -2
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +35 -5
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +173 -48
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.js +1 -1
- package/dist/server/lib/community-apps.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +1 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +170 -38
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/provider-api.d.ts +2 -2
- package/dist/shared/workspace-app-route.d.ts +2 -0
- package/dist/shared/workspace-app-route.d.ts.map +1 -0
- package/dist/shared/workspace-app-route.js +4 -0
- package/dist/shared/workspace-app-route.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/create_embed_session.ts +1 -0
- package/src/actions/index.spec.ts +3 -0
- package/src/components/layout/Layout.spec.tsx +39 -9
- package/src/components/layout/Layout.tsx +19 -40
- package/src/components/workspace-app-host.spec.tsx +88 -1
- package/src/components/workspace-app-host.tsx +30 -9
- package/src/lib/workspace-apps.spec.ts +64 -5
- package/src/lib/workspace-apps.ts +36 -6
- package/src/routes/pages/apps.$appId.spec.tsx +21 -3
- package/src/server/lib/app-creation-store.spec.ts +502 -11
- package/src/server/lib/app-creation-store.ts +198 -50
- package/src/server/lib/community-apps.ts +1 -1
- package/src/server/lib/mcp-gateway.spec.ts +396 -9
- package/src/server/lib/mcp-gateway.ts +209 -45
- package/src/shared/workspace-app-route.ts +3 -0
|
@@ -3,12 +3,14 @@ import {
|
|
|
3
3
|
canonicalA2AAudience,
|
|
4
4
|
extractA2APersistedMutationReceipts,
|
|
5
5
|
getGlobalA2ASecret,
|
|
6
|
+
signA2AOrgMemberDelegationToken,
|
|
6
7
|
signA2AOrganizationToken,
|
|
7
8
|
signA2AToken,
|
|
8
9
|
stripA2APersistedArtifactMarkers,
|
|
9
10
|
type A2APersistedMutationReceipt,
|
|
10
11
|
type Task,
|
|
11
12
|
} from "@agent-native/core/a2a";
|
|
13
|
+
import { getAppConfig } from "@agent-native/core/app-config";
|
|
12
14
|
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
13
15
|
import {
|
|
14
16
|
buildMcpToolName,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
buildDeepLink,
|
|
20
22
|
buildEmbedStartPath,
|
|
21
23
|
createEmbedSessionTicket,
|
|
24
|
+
getConfiguredAppBasePath,
|
|
22
25
|
getRequestContext,
|
|
23
26
|
getRequestOrgId,
|
|
24
27
|
getRequestUserEmail,
|
|
@@ -28,6 +31,7 @@ import {
|
|
|
28
31
|
getBuiltinAgents,
|
|
29
32
|
type DiscoveredAgent,
|
|
30
33
|
} from "@agent-native/core/server/agent-discovery";
|
|
34
|
+
import { EMBED_START_PATH } from "@agent-native/core/shared";
|
|
31
35
|
|
|
32
36
|
import { DESKTOP_WORKSPACE_SSO_FLAG } from "../../shared/feature-flags.js";
|
|
33
37
|
import {
|
|
@@ -79,6 +83,7 @@ export interface DispatchMcpAccessibleApp {
|
|
|
79
83
|
description: string;
|
|
80
84
|
url: string;
|
|
81
85
|
homeUrl?: string;
|
|
86
|
+
mcpUrl?: string;
|
|
82
87
|
color: string;
|
|
83
88
|
granted: boolean;
|
|
84
89
|
}
|
|
@@ -581,14 +586,73 @@ function appHomeBaseUrl(app: DispatchMcpAccessibleApp): string {
|
|
|
581
586
|
return appBaseUrl(app);
|
|
582
587
|
}
|
|
583
588
|
|
|
589
|
+
function workspaceGatewayMcpUrl(appUrl: string): string | undefined {
|
|
590
|
+
const configuredGateway = getAppConfig().workspace.gatewayUrl?.trim();
|
|
591
|
+
if (!configuredGateway) return undefined;
|
|
592
|
+
|
|
593
|
+
let configuredUrl: URL;
|
|
594
|
+
try {
|
|
595
|
+
configuredUrl = new URL(configuredGateway);
|
|
596
|
+
} catch {
|
|
597
|
+
throw new Error(
|
|
598
|
+
"WORKSPACE_GATEWAY_URL must be a valid absolute HTTP(S) URL.",
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
if (
|
|
602
|
+
configuredUrl.protocol !== "http:" &&
|
|
603
|
+
configuredUrl.protocol !== "https:"
|
|
604
|
+
) {
|
|
605
|
+
throw new Error("WORKSPACE_GATEWAY_URL must use http or https.");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const gateway = new URL(projectEnvironmentUrl(configuredUrl.toString()));
|
|
609
|
+
if (gateway.protocol !== "http:" && gateway.protocol !== "https:") {
|
|
610
|
+
throw new Error("WORKSPACE_GATEWAY_URL must use http or https.");
|
|
611
|
+
}
|
|
612
|
+
const target = new URL(appUrl);
|
|
613
|
+
if (gateway.origin !== target.origin) return undefined;
|
|
614
|
+
const configuredBasePath = getConfiguredAppBasePath();
|
|
615
|
+
const gatewayPath = gateway.pathname.replace(/\/+$/, "");
|
|
616
|
+
const basePath =
|
|
617
|
+
configuredBasePath &&
|
|
618
|
+
gatewayPath !== configuredBasePath &&
|
|
619
|
+
!gatewayPath.startsWith(`${configuredBasePath}/`)
|
|
620
|
+
? `${configuredBasePath}${gatewayPath}`
|
|
621
|
+
: gatewayPath;
|
|
622
|
+
gateway.pathname = `${basePath}/mcp`;
|
|
623
|
+
gateway.search = "";
|
|
624
|
+
gateway.hash = "";
|
|
625
|
+
return gateway.toString().replace(/\/$/, "");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function appMcpUrl(app: DispatchMcpAccessibleApp): string {
|
|
629
|
+
const explicit = app.mcpUrl?.trim();
|
|
630
|
+
return (
|
|
631
|
+
explicit?.replace(/\/+$/, "") ??
|
|
632
|
+
workspaceGatewayMcpUrl(app.url) ??
|
|
633
|
+
`${appHomeBaseUrl(app)}/mcp`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function appMcpBaseUrl(app: DispatchMcpAccessibleApp): string {
|
|
638
|
+
const url = new URL(appMcpUrl(app));
|
|
639
|
+
url.pathname = url.pathname.replace(/\/mcp\/?$/, "") || "/";
|
|
640
|
+
url.search = "";
|
|
641
|
+
url.hash = "";
|
|
642
|
+
return url.toString().replace(/\/$/, "");
|
|
643
|
+
}
|
|
644
|
+
|
|
584
645
|
function resolveGrantedAppEmbedStartUrl(
|
|
585
646
|
app: DispatchMcpAccessibleApp,
|
|
586
647
|
startUrl: string,
|
|
587
648
|
): string {
|
|
588
649
|
try {
|
|
589
|
-
const baseUrl =
|
|
650
|
+
const baseUrl = appMcpBaseUrl(app);
|
|
590
651
|
const resolved = new URL(startUrl, `${baseUrl}/`);
|
|
591
|
-
|
|
652
|
+
const base = new URL(baseUrl);
|
|
653
|
+
const basePath = base.pathname.replace(/\/+$/, "");
|
|
654
|
+
const expectedPath = `${basePath}${EMBED_START_PATH}`;
|
|
655
|
+
if (resolved.origin !== base.origin || resolved.pathname !== expectedPath) {
|
|
592
656
|
throw new Error(
|
|
593
657
|
"Target app returned an embed start URL outside the granted app.",
|
|
594
658
|
);
|
|
@@ -763,11 +827,13 @@ async function listWorkspaceSsoApps(): Promise<DispatchMcpAccessibleApp[]> {
|
|
|
763
827
|
}
|
|
764
828
|
for (const app of mountedApps) {
|
|
765
829
|
if (app.isDispatch || !app.url) continue;
|
|
830
|
+
const mcpUrl = workspaceGatewayMcpUrl(app.url);
|
|
766
831
|
candidatesById.set(app.id, {
|
|
767
832
|
id: app.id,
|
|
768
833
|
name: app.name,
|
|
769
834
|
description: app.description,
|
|
770
835
|
url: app.url,
|
|
836
|
+
...(mcpUrl ? { mcpUrl } : {}),
|
|
771
837
|
color: DISPATCH_COLOR,
|
|
772
838
|
granted: true,
|
|
773
839
|
});
|
|
@@ -1104,14 +1170,16 @@ function isRetryableTargetMcpError(error: unknown): boolean {
|
|
|
1104
1170
|
}
|
|
1105
1171
|
|
|
1106
1172
|
function targetMcpErrorStatus(error: unknown): number | undefined {
|
|
1173
|
+
const status = httpStatusFromError(error);
|
|
1174
|
+
if (status !== undefined) return status;
|
|
1107
1175
|
const message =
|
|
1108
1176
|
error instanceof Error
|
|
1109
1177
|
? error.message
|
|
1110
1178
|
: typeof error === "string"
|
|
1111
1179
|
? error
|
|
1112
1180
|
: safeJson(error);
|
|
1113
|
-
const
|
|
1114
|
-
return
|
|
1181
|
+
const matchedStatus = message.match(/\b([45]\d{2})\b/)?.[1];
|
|
1182
|
+
return matchedStatus ? Number(matchedStatus) : undefined;
|
|
1115
1183
|
}
|
|
1116
1184
|
|
|
1117
1185
|
function targetMcpRequestDetails(input: {
|
|
@@ -1132,6 +1200,22 @@ function targetMcpConnectTimeout(deadline: number): number {
|
|
|
1132
1200
|
return Math.max(1000, budget);
|
|
1133
1201
|
}
|
|
1134
1202
|
|
|
1203
|
+
class TargetMcpAuthenticationRejectedError extends Error {
|
|
1204
|
+
readonly status?: number;
|
|
1205
|
+
|
|
1206
|
+
constructor(cause: unknown) {
|
|
1207
|
+
super("Target MCP authentication was rejected", { cause });
|
|
1208
|
+
this.name = "TargetMcpAuthenticationRejectedError";
|
|
1209
|
+
const status = httpStatusFromError(cause) ?? targetMcpErrorStatus(cause);
|
|
1210
|
+
if (status !== undefined) this.status = status;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function isTargetMcpAuthenticationRejected(error: unknown): boolean {
|
|
1215
|
+
const status = httpStatusFromError(error);
|
|
1216
|
+
return status === 401 || status === 403;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1135
1219
|
function targetMcpRetryDelay(attempt: number): number {
|
|
1136
1220
|
const base =
|
|
1137
1221
|
TARGET_EMBED_SESSION_RETRY_BASE_MS * Math.pow(2, Math.max(0, attempt - 1));
|
|
@@ -1152,7 +1236,7 @@ async function callTargetCreateEmbedSession(input: {
|
|
|
1152
1236
|
servers: {
|
|
1153
1237
|
[serverId]: {
|
|
1154
1238
|
type: "http",
|
|
1155
|
-
url:
|
|
1239
|
+
url: appMcpUrl(input.app),
|
|
1156
1240
|
headers: {
|
|
1157
1241
|
Authorization: `Bearer ${input.token}`,
|
|
1158
1242
|
},
|
|
@@ -1161,8 +1245,17 @@ async function callTargetCreateEmbedSession(input: {
|
|
|
1161
1245
|
},
|
|
1162
1246
|
{ connectTimeoutMs: targetMcpConnectTimeout(deadline) },
|
|
1163
1247
|
);
|
|
1248
|
+
let connected = false;
|
|
1164
1249
|
try {
|
|
1165
1250
|
await manager.start();
|
|
1251
|
+
const connectErrorStatus = manager.getConnectErrorStatus(serverId);
|
|
1252
|
+
if (connectErrorStatus === 401 || connectErrorStatus === 403) {
|
|
1253
|
+
const error = Object.assign(new Error(`HTTP ${connectErrorStatus}`), {
|
|
1254
|
+
status: connectErrorStatus,
|
|
1255
|
+
});
|
|
1256
|
+
throw new TargetMcpAuthenticationRejectedError(error);
|
|
1257
|
+
}
|
|
1258
|
+
connected = true;
|
|
1166
1259
|
return await manager.callTool(
|
|
1167
1260
|
buildMcpToolName(serverId, "create_embed_session"),
|
|
1168
1261
|
{
|
|
@@ -1171,6 +1264,13 @@ async function callTargetCreateEmbedSession(input: {
|
|
|
1171
1264
|
},
|
|
1172
1265
|
);
|
|
1173
1266
|
} catch (error) {
|
|
1267
|
+
if (
|
|
1268
|
+
!connected &&
|
|
1269
|
+
!(error instanceof TargetMcpAuthenticationRejectedError) &&
|
|
1270
|
+
isTargetMcpAuthenticationRejected(error)
|
|
1271
|
+
) {
|
|
1272
|
+
throw new TargetMcpAuthenticationRejectedError(error);
|
|
1273
|
+
}
|
|
1174
1274
|
if (
|
|
1175
1275
|
attempt >= TARGET_EMBED_SESSION_ATTEMPTS ||
|
|
1176
1276
|
!isRetryableTargetMcpError(error) ||
|
|
@@ -1187,23 +1287,60 @@ async function callTargetCreateEmbedSession(input: {
|
|
|
1187
1287
|
}
|
|
1188
1288
|
}
|
|
1189
1289
|
|
|
1190
|
-
|
|
1290
|
+
type TargetMcpCredential = {
|
|
1291
|
+
strategy: "deployment-user" | "org-member-delegation";
|
|
1292
|
+
token: string;
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
async function createTargetMcpUserCredentials(input: {
|
|
1191
1296
|
ownerEmail: string;
|
|
1192
1297
|
orgId?: string;
|
|
1193
1298
|
target: DispatchMcpAccessibleApp;
|
|
1194
|
-
}): Promise<
|
|
1195
|
-
|
|
1299
|
+
}): Promise<{
|
|
1300
|
+
credentials: TargetMcpCredential[];
|
|
1301
|
+
loadOrgMemberCredential: () => Promise<TargetMcpCredential | undefined>;
|
|
1302
|
+
}> {
|
|
1303
|
+
const audience = canonicalA2AAudience(appMcpUrl(input.target));
|
|
1304
|
+
const orgDomain = input.orgId
|
|
1305
|
+
? (await getOrgDomain(input.orgId))?.trim().toLowerCase() || undefined
|
|
1306
|
+
: undefined;
|
|
1307
|
+
const credentials: TargetMcpCredential[] = [];
|
|
1308
|
+
if (getGlobalA2ASecret()) {
|
|
1309
|
+
credentials.push({
|
|
1310
|
+
strategy: "deployment-user",
|
|
1311
|
+
token: await signA2AToken(input.ownerEmail, undefined, undefined, {
|
|
1312
|
+
expiresIn: "5m",
|
|
1313
|
+
audience,
|
|
1314
|
+
preferGlobalSecret: true,
|
|
1315
|
+
}),
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const loadOrgMemberCredential = async () => {
|
|
1320
|
+
if (!input.orgId || !orgDomain) return undefined;
|
|
1321
|
+
const orgSecret = await getOrgA2ASecret(input.orgId);
|
|
1322
|
+
if (!orgSecret) return undefined;
|
|
1323
|
+
return {
|
|
1324
|
+
strategy: "org-member-delegation" as const,
|
|
1325
|
+
token: await signA2AOrgMemberDelegationToken(
|
|
1326
|
+
input.ownerEmail,
|
|
1327
|
+
orgDomain,
|
|
1328
|
+
orgSecret,
|
|
1329
|
+
audience,
|
|
1330
|
+
),
|
|
1331
|
+
};
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
if (credentials.length === 0) {
|
|
1335
|
+
const orgMemberCredential = await loadOrgMemberCredential();
|
|
1336
|
+
if (orgMemberCredential) credentials.push(orgMemberCredential);
|
|
1337
|
+
}
|
|
1338
|
+
if (credentials.length === 0) {
|
|
1196
1339
|
throw new Error(
|
|
1197
|
-
"Cross-app embed sessions require
|
|
1340
|
+
"Cross-app embed sessions require a deployment A2A secret or an organization secret with member delegation enabled on the target.",
|
|
1198
1341
|
);
|
|
1199
1342
|
}
|
|
1200
|
-
|
|
1201
|
-
return signA2AToken(input.ownerEmail, undefined, undefined, {
|
|
1202
|
-
expiresIn: "5m",
|
|
1203
|
-
audience,
|
|
1204
|
-
preferGlobalSecret: true,
|
|
1205
|
-
...(input.orgId ? { extraClaims: { org_id: input.orgId } } : {}),
|
|
1206
|
-
});
|
|
1343
|
+
return { credentials, loadOrgMemberCredential };
|
|
1207
1344
|
}
|
|
1208
1345
|
|
|
1209
1346
|
async function resolveEmbedTarget(
|
|
@@ -1363,45 +1500,72 @@ async function createEmbedSessionForResolvedApp(input: {
|
|
|
1363
1500
|
});
|
|
1364
1501
|
}
|
|
1365
1502
|
|
|
1366
|
-
const
|
|
1503
|
+
const targetCredentials = await createTargetMcpUserCredentials({
|
|
1367
1504
|
ownerEmail,
|
|
1368
|
-
orgId,
|
|
1505
|
+
...(orgId !== undefined ? { orgId } : {}),
|
|
1369
1506
|
target: target.app,
|
|
1370
1507
|
});
|
|
1371
1508
|
const targetDetails = targetMcpRequestDetails({
|
|
1372
1509
|
app: target.app,
|
|
1373
1510
|
url: target.url,
|
|
1374
1511
|
});
|
|
1375
|
-
let parsed:
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const result = await callTargetCreateEmbedSession({
|
|
1386
|
-
app: target.app,
|
|
1387
|
-
token: targetToken,
|
|
1388
|
-
url: target.url,
|
|
1389
|
-
chrome,
|
|
1390
|
-
});
|
|
1391
|
-
parsed = parseMcpToolTextResult(result) as {
|
|
1392
|
-
startUrl?: string;
|
|
1393
|
-
targetPath?: string;
|
|
1394
|
-
expiresAt?: number;
|
|
1395
|
-
};
|
|
1396
|
-
} catch (error) {
|
|
1397
|
-
console.warn("[dispatch] workspace embed target response", {
|
|
1512
|
+
let parsed:
|
|
1513
|
+
| {
|
|
1514
|
+
startUrl?: string;
|
|
1515
|
+
targetPath?: string;
|
|
1516
|
+
expiresAt?: number;
|
|
1517
|
+
}
|
|
1518
|
+
| undefined;
|
|
1519
|
+
const credentials = [...targetCredentials.credentials];
|
|
1520
|
+
for (const [index, credential] of credentials.entries()) {
|
|
1521
|
+
console.info("[dispatch] workspace embed target request", {
|
|
1398
1522
|
...targetDetails,
|
|
1399
|
-
authStrategy:
|
|
1400
|
-
status: targetMcpErrorStatus(error),
|
|
1401
|
-
category: isRetryableTargetMcpError(error) ? "transient" : "permanent",
|
|
1523
|
+
authStrategy: credential.strategy,
|
|
1402
1524
|
});
|
|
1403
|
-
|
|
1525
|
+
try {
|
|
1526
|
+
const result = await callTargetCreateEmbedSession({
|
|
1527
|
+
app: target.app,
|
|
1528
|
+
token: credential.token,
|
|
1529
|
+
url: target.url,
|
|
1530
|
+
...(chrome !== undefined ? { chrome } : {}),
|
|
1531
|
+
});
|
|
1532
|
+
parsed = parseMcpToolTextResult(result) as {
|
|
1533
|
+
startUrl?: string;
|
|
1534
|
+
targetPath?: string;
|
|
1535
|
+
expiresAt?: number;
|
|
1536
|
+
};
|
|
1537
|
+
break;
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
if (
|
|
1540
|
+
error instanceof TargetMcpAuthenticationRejectedError &&
|
|
1541
|
+
credential.strategy === "deployment-user" &&
|
|
1542
|
+
index === credentials.length - 1
|
|
1543
|
+
) {
|
|
1544
|
+
const orgMemberCredential =
|
|
1545
|
+
await targetCredentials.loadOrgMemberCredential();
|
|
1546
|
+
if (orgMemberCredential) credentials.push(orgMemberCredential);
|
|
1547
|
+
}
|
|
1548
|
+
if (
|
|
1549
|
+
error instanceof TargetMcpAuthenticationRejectedError &&
|
|
1550
|
+
index < credentials.length - 1
|
|
1551
|
+
) {
|
|
1552
|
+
console.warn("[dispatch] workspace embed auth candidate rejected", {
|
|
1553
|
+
...targetDetails,
|
|
1554
|
+
authStrategy: credential.strategy,
|
|
1555
|
+
status: targetMcpErrorStatus(error),
|
|
1556
|
+
});
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
console.warn("[dispatch] workspace embed target response", {
|
|
1560
|
+
...targetDetails,
|
|
1561
|
+
authStrategy: credential.strategy,
|
|
1562
|
+
status: targetMcpErrorStatus(error),
|
|
1563
|
+
category: isRetryableTargetMcpError(error) ? "transient" : "permanent",
|
|
1564
|
+
});
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1404
1567
|
}
|
|
1568
|
+
if (!parsed) throw new Error("Target app did not return an embed session.");
|
|
1405
1569
|
if (!parsed.startUrl) {
|
|
1406
1570
|
throw new Error("Target app did not return an embed start URL.");
|
|
1407
1571
|
}
|