@agent-native/dispatch 0.40.41-same-host-mcp-20261027-20261027081850 → 0.40.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +28 -10
  2. package/dist/actions/provider-api-register.d.ts +11 -11
  3. package/dist/actions/upsert-destination.d.ts +12 -12
  4. package/dist/components/action-query-error.d.ts.map +1 -1
  5. package/dist/components/action-query-error.js +8 -0
  6. package/dist/components/action-query-error.js.map +1 -1
  7. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  8. package/dist/server/lib/app-creation-store.js +48 -3
  9. package/dist/server/lib/app-creation-store.js.map +1 -1
  10. package/dist/server/lib/mcp-gateway.d.ts +8 -0
  11. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  12. package/dist/server/lib/mcp-gateway.js +71 -11
  13. package/dist/server/lib/mcp-gateway.js.map +1 -1
  14. package/dist/server/lib/workspace-app-chat-proxy.d.ts +6 -5
  15. package/dist/server/lib/workspace-app-chat-proxy.d.ts.map +1 -1
  16. package/dist/server/lib/workspace-app-chat-proxy.js +78 -11
  17. package/dist/server/lib/workspace-app-chat-proxy.js.map +1 -1
  18. package/dist/server/plugins/agent-chat.js +1 -0
  19. package/dist/server/plugins/agent-chat.js.map +1 -1
  20. package/package.json +3 -3
  21. package/src/components/action-query-error.spec.tsx +37 -0
  22. package/src/components/action-query-error.tsx +11 -0
  23. package/src/server/lib/app-creation-store.spec.ts +101 -1
  24. package/src/server/lib/app-creation-store.ts +65 -4
  25. package/src/server/lib/mcp-gateway.spec.ts +135 -1
  26. package/src/server/lib/mcp-gateway.ts +122 -22
  27. package/src/server/lib/workspace-app-chat-proxy.spec.ts +309 -5
  28. package/src/server/lib/workspace-app-chat-proxy.ts +123 -25
  29. package/src/server/plugins/agent-chat.spec.ts +1 -0
  30. package/src/server/plugins/agent-chat.ts +1 -0
@@ -132,7 +132,9 @@ import { runWithRequestContext } from "@agent-native/core/server";
132
132
 
133
133
  import {
134
134
  createGrantedDispatchMcpEmbedSession,
135
+ createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat,
135
136
  createWorkspaceSsoEmbedSession,
137
+ createWorkspaceSsoEmbedSessionForWorkspaceAppChat,
136
138
  askGrantedDispatchMcpApp,
137
139
  getGrantedDispatchMcpAppTask,
138
140
  listGrantedDispatchMcpApps,
@@ -208,6 +210,7 @@ beforeEach(() => {
208
210
  mocks.getOrgA2ASecret.mockResolvedValue(null);
209
211
  mocks.getOrgDomain.mockResolvedValue(null);
210
212
  vi.stubEnv("A2A_SECRET", "");
213
+ vi.stubEnv("APP_URL", "http://localhost:8092");
211
214
  mocks.isFeatureFlagEnabled.mockResolvedValue(true);
212
215
  });
213
216
 
@@ -1465,6 +1468,58 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
1465
1468
  });
1466
1469
  });
1467
1470
 
1471
+ it("pins Dispatch self embeds to configured hosts instead of request context", async () => {
1472
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "");
1473
+ vi.stubEnv("APP_URL", "https://dispatch.agent-native.test");
1474
+ vi.stubEnv("APP_BASE_PATH", "");
1475
+ vi.stubEnv("VITE_APP_BASE_PATH", "");
1476
+
1477
+ const result = await runWithRequestContext(
1478
+ {
1479
+ userEmail: "owner@example.test",
1480
+ requestOrigin: "https://attacker.example",
1481
+ },
1482
+ () =>
1483
+ createGrantedDispatchMcpEmbedSession({
1484
+ app: "dispatch",
1485
+ path: "/extensions/ext-1/github-stars-over-time",
1486
+ chrome: "minimal",
1487
+ }),
1488
+ );
1489
+
1490
+ expect(result.startUrl).toBe(
1491
+ "https://dispatch.agent-native.test/_agent-native/embed/start?ticket=ticket-123",
1492
+ );
1493
+ });
1494
+
1495
+ it("logs an invalid production origin while accepting a trusted runtime origin", async () => {
1496
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
1497
+ vi.stubEnv("APP_URL", "not-a-url");
1498
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://runtime.example/dispatch");
1499
+ vi.stubEnv("APP_BASE_PATH", "");
1500
+
1501
+ const result = await runWithRequestContext(
1502
+ {
1503
+ userEmail: "owner@example.test",
1504
+ requestOrigin: "https://runtime.example",
1505
+ },
1506
+ () =>
1507
+ createGrantedDispatchMcpEmbedSession({
1508
+ app: "dispatch",
1509
+ path: "/extensions/ext-1/github-stars-over-time",
1510
+ chrome: "minimal",
1511
+ }),
1512
+ );
1513
+
1514
+ expect(result.startUrl).toBe(
1515
+ "https://runtime.example/_agent-native/embed/start?ticket=ticket-123",
1516
+ );
1517
+ expect(warn).toHaveBeenCalledWith(
1518
+ "[dispatch] invalid configured request origin",
1519
+ { source: "production" },
1520
+ );
1521
+ });
1522
+
1468
1523
  it("keeps workspace sign-in behind the rollout flag", async () => {
1469
1524
  mocks.isFeatureFlagEnabled.mockResolvedValue(false);
1470
1525
 
@@ -1627,7 +1682,7 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
1627
1682
  },
1628
1683
  ]);
1629
1684
  mocks.getUserSetting.mockResolvedValue({ mode: "all-apps" });
1630
- mocks.managerCallTool.mockResolvedValueOnce({
1685
+ mocks.managerCallTool.mockResolvedValue({
1631
1686
  structuredContent: {
1632
1687
  startUrl:
1633
1688
  "https://beta.agent-workspace.builder.io/customer-ticket-triage/_agent-native/embed/start?ticket=remote",
@@ -1681,6 +1736,22 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
1681
1736
  expect(result.startUrl).toBe(
1682
1737
  "https://beta.agent-workspace.builder.io/customer-ticket-triage/_agent-native/embed/start?ticket=remote",
1683
1738
  );
1739
+
1740
+ const chatSession = await runWithRequestContext(
1741
+ {
1742
+ userEmail: "owner@example.test",
1743
+ requestOrigin: "https://beta.agent-workspace.builder.io",
1744
+ },
1745
+ () =>
1746
+ route === "workspace-sso"
1747
+ ? createWorkspaceSsoEmbedSessionForWorkspaceAppChat(workspaceApp.id)
1748
+ : createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat(
1749
+ workspaceApp.id,
1750
+ ),
1751
+ );
1752
+ expect(chatSession.appBaseUrl).toBe(
1753
+ "https://beta.agent-workspace.builder.io/customer-ticket-triage",
1754
+ );
1684
1755
  },
1685
1756
  );
1686
1757
 
@@ -1831,6 +1902,69 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
1831
1902
  },
1832
1903
  );
1833
1904
 
1905
+ it.each(["workspace-sso", "dispatch-grant"] as const)(
1906
+ "retains the Workspace gateway path for discovered-only app chats through %s",
1907
+ async (route) => {
1908
+ vi.stubEnv("A2A_SECRET", "test-a2a-secret");
1909
+ vi.stubEnv("APP_URL", "https://beta.agent-workspace.builder.io");
1910
+ vi.stubEnv(
1911
+ "WORKSPACE_GATEWAY_URL",
1912
+ "https://beta.agent-workspace.builder.io/dispatch",
1913
+ );
1914
+ vi.stubEnv("APP_BASE_PATH", "");
1915
+ vi.stubEnv("VITE_APP_BASE_PATH", "");
1916
+ vi.stubEnv(
1917
+ "IDENTITY_SSO_APP_REGISTRY_JSON",
1918
+ JSON.stringify([
1919
+ {
1920
+ appId: "customer-ticket-triage",
1921
+ clientId: "workspace-client",
1922
+ origin: "https://beta.agent-workspace.builder.io",
1923
+ callbackPath: "/_agent-native/identity/callback",
1924
+ capabilities: ["identity-sso"],
1925
+ },
1926
+ ]),
1927
+ );
1928
+ mocks.listWorkspaceApps.mockResolvedValue([]);
1929
+ mocks.discoverAgents.mockResolvedValue([
1930
+ {
1931
+ id: "customer-ticket-triage",
1932
+ name: "Customer Ticket Triage",
1933
+ description: "Triage customer tickets",
1934
+ url: "https://beta.agent-workspace.builder.io",
1935
+ color: "#14B8A6",
1936
+ },
1937
+ ]);
1938
+ mocks.managerCallTool.mockResolvedValue({
1939
+ structuredContent: {
1940
+ startUrl:
1941
+ "https://beta.agent-workspace.builder.io/dispatch/_agent-native/embed/start?ticket=remote",
1942
+ },
1943
+ });
1944
+
1945
+ const result = await runWithRequestContext(
1946
+ {
1947
+ userEmail: "owner@example.test",
1948
+ requestOrigin: "https://beta.dispatch.agent-native.com",
1949
+ },
1950
+ () =>
1951
+ route === "workspace-sso"
1952
+ ? createWorkspaceSsoEmbedSessionForWorkspaceAppChat(
1953
+ "customer-ticket-triage",
1954
+ )
1955
+ : createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat(
1956
+ "customer-ticket-triage",
1957
+ ),
1958
+ );
1959
+
1960
+ expect(result).toEqual({
1961
+ startUrl:
1962
+ "https://beta.agent-workspace.builder.io/dispatch/_agent-native/embed/start?ticket=remote",
1963
+ appBaseUrl: "https://beta.agent-workspace.builder.io/dispatch",
1964
+ });
1965
+ },
1966
+ );
1967
+
1834
1968
  it("does not fall back when the configured Workspace gateway URL is invalid", async () => {
1835
1969
  vi.stubEnv("WORKSPACE_GATEWAY_URL", "not-a-url");
1836
1970
  mocks.listWorkspaceApps.mockResolvedValue([
@@ -21,10 +21,12 @@ import {
21
21
  buildDeepLink,
22
22
  buildEmbedStartPath,
23
23
  createEmbedSessionTicket,
24
+ getAppProductionUrl,
24
25
  getConfiguredAppBasePath,
25
26
  getRequestContext,
26
27
  getRequestOrgId,
27
28
  getRequestUserEmail,
29
+ resolveAppRuntimeUrl,
28
30
  } from "@agent-native/core/server";
29
31
  import {
30
32
  discoverAgents,
@@ -484,7 +486,9 @@ function withConfiguredBasePath(baseUrl: string): string {
484
486
 
485
487
  function dispatchSelfBaseUrl(): string {
486
488
  const requestOrigin = normalizeBaseUrl(getRequestContext()?.requestOrigin);
487
- if (requestOrigin) return withConfiguredBasePath(requestOrigin);
489
+ if (requestOrigin && isTrustedRequestOrigin(requestOrigin)) {
490
+ return withConfiguredBasePath(requestOrigin);
491
+ }
488
492
 
489
493
  const configured =
490
494
  normalizeBaseUrl(process.env.WORKSPACE_GATEWAY_URL) ??
@@ -499,6 +503,24 @@ function dispatchSelfBaseUrl(): string {
499
503
  : "http://localhost:8092";
500
504
  }
501
505
 
506
+ function isTrustedRequestOrigin(value: string): boolean {
507
+ // An embed ticket is fetched from this origin and becomes a bearer token.
508
+ const origin = new URL(value).origin;
509
+ const candidates = [
510
+ { source: "production", value: getAppProductionUrl() },
511
+ { source: "runtime", value: resolveAppRuntimeUrl() },
512
+ ];
513
+ return candidates.some(({ source, value }) => {
514
+ if (!value) return false;
515
+ const url = normalizeBaseUrl(value);
516
+ if (!url) {
517
+ console.warn("[dispatch] invalid configured request origin", { source });
518
+ return false;
519
+ }
520
+ return new URL(url).origin === origin;
521
+ });
522
+ }
523
+
502
524
  function dispatchSelfApp(
503
525
  settings: DispatchMcpAppAccessSettings,
504
526
  ): DispatchMcpAccessibleApp {
@@ -647,6 +669,14 @@ function appMcpBaseUrl(app: DispatchMcpAccessibleApp): string {
647
669
  return url.toString().replace(/\/$/, "");
648
670
  }
649
671
 
672
+ function appChatBaseUrl(app: DispatchMcpAccessibleApp): string {
673
+ const homeUrl = new URL(appHomeBaseUrl(app));
674
+ if (homeUrl.pathname !== "/") {
675
+ return homeUrl.toString().replace(/\/+$/, "");
676
+ }
677
+ return appMcpBaseUrl(app);
678
+ }
679
+
650
680
  function resolveGrantedAppEmbedStartUrl(
651
681
  app: DispatchMcpAccessibleApp,
652
682
  startUrl: string,
@@ -1469,17 +1499,34 @@ async function createDispatchSelfEmbedSession(input: {
1469
1499
  };
1470
1500
  }
1471
1501
 
1472
- export async function createGrantedDispatchMcpEmbedSession(input: {
1473
- app?: string;
1474
- url?: string;
1475
- path?: string;
1476
- chrome?: "full" | "minimal";
1477
- }): Promise<{
1502
+ type InternalEmbedSession = {
1478
1503
  startUrl: string;
1479
1504
  targetPath?: string;
1480
1505
  expiresAt?: number;
1481
1506
  app: string;
1482
- }> {
1507
+ appBaseUrl: string;
1508
+ };
1509
+
1510
+ function publicEmbedSession({
1511
+ startUrl,
1512
+ targetPath,
1513
+ expiresAt,
1514
+ app,
1515
+ }: InternalEmbedSession) {
1516
+ return {
1517
+ startUrl,
1518
+ ...(targetPath ? { targetPath } : {}),
1519
+ ...(typeof expiresAt === "number" ? { expiresAt } : {}),
1520
+ app,
1521
+ };
1522
+ }
1523
+
1524
+ async function createGrantedDispatchMcpEmbedSessionResult(input: {
1525
+ app?: string;
1526
+ url?: string;
1527
+ path?: string;
1528
+ chrome?: "full" | "minimal";
1529
+ }): Promise<InternalEmbedSession> {
1483
1530
  const userEmail = getRequestUserEmail();
1484
1531
  if (!userEmail) throw new Error("no authenticated user");
1485
1532
  const target = await resolveDispatchEmbedTarget(input);
@@ -1492,6 +1539,33 @@ export async function createGrantedDispatchMcpEmbedSession(input: {
1492
1539
  });
1493
1540
  }
1494
1541
 
1542
+ export async function createGrantedDispatchMcpEmbedSession(input: {
1543
+ app?: string;
1544
+ url?: string;
1545
+ path?: string;
1546
+ chrome?: "full" | "minimal";
1547
+ }): Promise<{
1548
+ startUrl: string;
1549
+ targetPath?: string;
1550
+ expiresAt?: number;
1551
+ app: string;
1552
+ }> {
1553
+ return publicEmbedSession(
1554
+ await createGrantedDispatchMcpEmbedSessionResult(input),
1555
+ );
1556
+ }
1557
+
1558
+ export async function createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat(
1559
+ appId: string,
1560
+ ): Promise<{ startUrl: string; appBaseUrl: string }> {
1561
+ const session = await createGrantedDispatchMcpEmbedSessionResult({
1562
+ app: appId,
1563
+ path: "/",
1564
+ chrome: "minimal",
1565
+ });
1566
+ return { startUrl: session.startUrl, appBaseUrl: session.appBaseUrl };
1567
+ }
1568
+
1495
1569
  async function createEmbedSessionForResolvedApp(input: {
1496
1570
  ownerEmail: string;
1497
1571
  orgId?: string;
@@ -1502,17 +1576,21 @@ async function createEmbedSessionForResolvedApp(input: {
1502
1576
  targetPath?: string;
1503
1577
  expiresAt?: number;
1504
1578
  app: string;
1579
+ appBaseUrl: string;
1505
1580
  }> {
1506
1581
  const { ownerEmail, orgId, target, chrome } = input;
1507
1582
 
1508
1583
  if (target.app.id === DISPATCH_APP_ID) {
1509
- return createDispatchSelfEmbedSession({
1510
- ownerEmail,
1511
- orgId,
1512
- path: target.path,
1513
- baseUrl: appBaseUrl(target.app),
1514
- chrome,
1515
- });
1584
+ return {
1585
+ ...(await createDispatchSelfEmbedSession({
1586
+ ownerEmail,
1587
+ ...(orgId !== undefined ? { orgId } : {}),
1588
+ path: target.path,
1589
+ baseUrl: appBaseUrl(target.app),
1590
+ ...(chrome !== undefined ? { chrome } : {}),
1591
+ })),
1592
+ appBaseUrl: appBaseUrl(target.app),
1593
+ };
1516
1594
  }
1517
1595
 
1518
1596
  const targetMcpUrl = appMcpUrl(target.app);
@@ -1600,26 +1678,23 @@ async function createEmbedSessionForResolvedApp(input: {
1600
1678
  targetPath?: string;
1601
1679
  expiresAt?: number;
1602
1680
  app: string;
1681
+ appBaseUrl: string;
1603
1682
  } = {
1604
1683
  startUrl,
1605
1684
  app: target.app.id,
1685
+ appBaseUrl: appChatBaseUrl(target.app),
1606
1686
  };
1607
1687
  if (parsed.targetPath) output.targetPath = parsed.targetPath;
1608
1688
  if (typeof parsed.expiresAt === "number") output.expiresAt = parsed.expiresAt;
1609
1689
  return output;
1610
1690
  }
1611
1691
 
1612
- export async function createWorkspaceSsoEmbedSession(input: {
1692
+ async function createWorkspaceSsoEmbedSessionResult(input: {
1613
1693
  app?: string;
1614
1694
  url?: string;
1615
1695
  path?: string;
1616
1696
  chrome?: "full" | "minimal";
1617
- }): Promise<{
1618
- startUrl: string;
1619
- targetPath?: string;
1620
- expiresAt?: number;
1621
- app: string;
1622
- }> {
1697
+ }): Promise<InternalEmbedSession> {
1623
1698
  const ownerEmail = getRequestUserEmail();
1624
1699
  if (!ownerEmail) {
1625
1700
  console.warn("[dispatch] workspace embed mint rejected", {
@@ -1660,3 +1735,28 @@ export async function createWorkspaceSsoEmbedSession(input: {
1660
1735
  chrome: input.chrome,
1661
1736
  });
1662
1737
  }
1738
+
1739
+ export async function createWorkspaceSsoEmbedSession(input: {
1740
+ app?: string;
1741
+ url?: string;
1742
+ path?: string;
1743
+ chrome?: "full" | "minimal";
1744
+ }): Promise<{
1745
+ startUrl: string;
1746
+ targetPath?: string;
1747
+ expiresAt?: number;
1748
+ app: string;
1749
+ }> {
1750
+ return publicEmbedSession(await createWorkspaceSsoEmbedSessionResult(input));
1751
+ }
1752
+
1753
+ export async function createWorkspaceSsoEmbedSessionForWorkspaceAppChat(
1754
+ appId: string,
1755
+ ): Promise<{ startUrl: string; appBaseUrl: string }> {
1756
+ const session = await createWorkspaceSsoEmbedSessionResult({
1757
+ app: appId,
1758
+ path: "/",
1759
+ chrome: "minimal",
1760
+ });
1761
+ return { startUrl: session.startUrl, appBaseUrl: session.appBaseUrl };
1762
+ }