@agent-native/dispatch 0.24.6 → 0.25.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.
Files changed (151) hide show
  1. package/README.md +12 -1
  2. package/dist/actions/connect-external-agent.d.ts +18 -0
  3. package/dist/actions/connect-external-agent.d.ts.map +1 -0
  4. package/dist/actions/connect-external-agent.js +68 -0
  5. package/dist/actions/connect-external-agent.js.map +1 -0
  6. package/dist/actions/create-workspace-resource.d.ts +14 -14
  7. package/dist/actions/create-workspace-resource.js +9 -2
  8. package/dist/actions/create-workspace-resource.js.map +1 -1
  9. package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
  10. package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
  11. package/dist/actions/create_workspace_app_embed_session.js +30 -0
  12. package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
  13. package/dist/actions/import-agent-pack.d.ts +96 -0
  14. package/dist/actions/import-agent-pack.d.ts.map +1 -0
  15. package/dist/actions/import-agent-pack.js +116 -0
  16. package/dist/actions/import-agent-pack.js.map +1 -0
  17. package/dist/actions/import-agent.d.ts +60 -0
  18. package/dist/actions/import-agent.d.ts.map +1 -0
  19. package/dist/actions/import-agent.js +82 -0
  20. package/dist/actions/import-agent.js.map +1 -0
  21. package/dist/actions/index.d.ts.map +1 -1
  22. package/dist/actions/index.js +10 -0
  23. package/dist/actions/index.js.map +1 -1
  24. package/dist/actions/list-agent-pack.d.ts +36 -0
  25. package/dist/actions/list-agent-pack.d.ts.map +1 -0
  26. package/dist/actions/list-agent-pack.js +32 -0
  27. package/dist/actions/list-agent-pack.js.map +1 -0
  28. package/dist/actions/list-workspace-resource-options.d.ts +1 -1
  29. package/dist/actions/list-workspace-resource-options.js +8 -1
  30. package/dist/actions/list-workspace-resource-options.js.map +1 -1
  31. package/dist/actions/list-workspace-resources.d.ts +1 -1
  32. package/dist/actions/list-workspace-resources.js +8 -1
  33. package/dist/actions/list-workspace-resources.js.map +1 -1
  34. package/dist/actions/navigate.js +1 -1
  35. package/dist/actions/navigate.js.map +1 -1
  36. package/dist/actions/provider-api-register.d.ts +10 -10
  37. package/dist/actions/update-workspace-resource.d.ts +13 -13
  38. package/dist/actions/view-screen.js +18 -1
  39. package/dist/actions/view-screen.js.map +1 -1
  40. package/dist/components/index.d.ts +1 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +1 -0
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/layout/Layout.d.ts +1 -8
  45. package/dist/components/layout/Layout.d.ts.map +1 -1
  46. package/dist/components/layout/Layout.js +33 -106
  47. package/dist/components/layout/Layout.js.map +1 -1
  48. package/dist/components/simple-agents-panel.d.ts +9 -0
  49. package/dist/components/simple-agents-panel.d.ts.map +1 -0
  50. package/dist/components/simple-agents-panel.js +405 -0
  51. package/dist/components/simple-agents-panel.js.map +1 -0
  52. package/dist/components/workspace-app-host.d.ts +29 -0
  53. package/dist/components/workspace-app-host.d.ts.map +1 -0
  54. package/dist/components/workspace-app-host.js +145 -0
  55. package/dist/components/workspace-app-host.js.map +1 -0
  56. package/dist/db/migrations.d.ts +2 -4
  57. package/dist/db/migrations.d.ts.map +1 -1
  58. package/dist/db/migrations.js +25 -0
  59. package/dist/db/migrations.js.map +1 -1
  60. package/dist/db/schema.js +1 -1
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/hooks/use-navigation-state.d.ts +1 -0
  63. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  64. package/dist/hooks/use-navigation-state.js +10 -0
  65. package/dist/hooks/use-navigation-state.js.map +1 -1
  66. package/dist/lib/agent-pack.d.ts +31 -0
  67. package/dist/lib/agent-pack.d.ts.map +1 -0
  68. package/dist/lib/agent-pack.js +149 -0
  69. package/dist/lib/agent-pack.js.map +1 -0
  70. package/dist/lib/simple-agent-profile.d.ts +36 -0
  71. package/dist/lib/simple-agent-profile.d.ts.map +1 -0
  72. package/dist/lib/simple-agent-profile.js +183 -0
  73. package/dist/lib/simple-agent-profile.js.map +1 -0
  74. package/dist/routes/pages/agents.d.ts.map +1 -1
  75. package/dist/routes/pages/agents.js +11 -1
  76. package/dist/routes/pages/agents.js.map +1 -1
  77. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  78. package/dist/routes/pages/apps.$appId.js +4 -95
  79. package/dist/routes/pages/apps.$appId.js.map +1 -1
  80. package/dist/routes/pages/chat.d.ts.map +1 -1
  81. package/dist/routes/pages/chat.js +62 -10
  82. package/dist/routes/pages/chat.js.map +1 -1
  83. package/dist/server/lib/agent-pack-store.d.ts +24 -0
  84. package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
  85. package/dist/server/lib/agent-pack-store.js +22 -0
  86. package/dist/server/lib/agent-pack-store.js.map +1 -0
  87. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  88. package/dist/server/lib/dispatch-store.js +4 -0
  89. package/dist/server/lib/dispatch-store.js.map +1 -1
  90. package/dist/server/lib/mcp-gateway.d.ts +11 -0
  91. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  92. package/dist/server/lib/mcp-gateway.js +155 -9
  93. package/dist/server/lib/mcp-gateway.js.map +1 -1
  94. package/dist/server/lib/workspace-resources-store.d.ts +15 -1
  95. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  96. package/dist/server/lib/workspace-resources-store.js +11 -2
  97. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  98. package/dist/server/plugins/agent-chat.js +13 -0
  99. package/dist/server/plugins/agent-chat.js.map +1 -1
  100. package/dist/shared/feature-flags.d.ts +7 -0
  101. package/dist/shared/feature-flags.d.ts.map +1 -0
  102. package/dist/shared/feature-flags.js +12 -0
  103. package/dist/shared/feature-flags.js.map +1 -0
  104. package/dist/shared/workspace-sso.d.ts +36 -0
  105. package/dist/shared/workspace-sso.d.ts.map +1 -0
  106. package/dist/shared/workspace-sso.js +99 -0
  107. package/dist/shared/workspace-sso.js.map +1 -0
  108. package/package.json +5 -3
  109. package/src/actions/connect-external-agent.ts +92 -0
  110. package/src/actions/create-workspace-resource.ts +9 -2
  111. package/src/actions/create_workspace_app_embed_session.ts +32 -0
  112. package/src/actions/import-agent-pack.ts +146 -0
  113. package/src/actions/import-agent.ts +102 -0
  114. package/src/actions/index.spec.ts +13 -0
  115. package/src/actions/index.ts +10 -0
  116. package/src/actions/list-agent-pack.ts +43 -0
  117. package/src/actions/list-workspace-resource-options.ts +8 -1
  118. package/src/actions/list-workspace-resources.ts +8 -1
  119. package/src/actions/navigate.ts +1 -1
  120. package/src/actions/view-screen.spec.ts +29 -1
  121. package/src/actions/view-screen.ts +19 -1
  122. package/src/components/app-icon.spec.tsx +10 -0
  123. package/src/components/index.ts +1 -0
  124. package/src/components/layout/Layout.spec.tsx +2 -0
  125. package/src/components/layout/Layout.tsx +59 -171
  126. package/src/components/simple-agents-panel.spec.tsx +73 -0
  127. package/src/components/simple-agents-panel.tsx +1226 -0
  128. package/src/components/workspace-app-host.spec.tsx +204 -0
  129. package/src/components/workspace-app-host.tsx +359 -0
  130. package/src/db/migrations.spec.ts +5 -1
  131. package/src/db/migrations.ts +28 -1
  132. package/src/db/schema.ts +1 -1
  133. package/src/hooks/use-navigation-state.spec.ts +25 -0
  134. package/src/hooks/use-navigation-state.ts +11 -0
  135. package/src/lib/agent-pack.spec.ts +47 -0
  136. package/src/lib/agent-pack.ts +226 -0
  137. package/src/lib/simple-agent-profile.spec.ts +77 -0
  138. package/src/lib/simple-agent-profile.ts +247 -0
  139. package/src/routes/index.spec.ts +1 -0
  140. package/src/routes/pages/agents.tsx +21 -1
  141. package/src/routes/pages/apps.$appId.tsx +3 -240
  142. package/src/routes/pages/chat.spec.tsx +62 -0
  143. package/src/routes/pages/chat.tsx +117 -9
  144. package/src/server/lib/agent-pack-store.ts +35 -0
  145. package/src/server/lib/dispatch-store.ts +8 -0
  146. package/src/server/lib/mcp-gateway.spec.ts +141 -0
  147. package/src/server/lib/mcp-gateway.ts +227 -15
  148. package/src/server/lib/workspace-resources-store.ts +9 -2
  149. package/src/server/plugins/agent-chat.ts +13 -0
  150. package/src/shared/feature-flags.ts +13 -0
  151. package/src/shared/workspace-sso.ts +118 -0
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  const mocks = vi.hoisted(() => ({
4
4
  discoverAgents: vi.fn(),
5
+ listWorkspaceApps: vi.fn(),
5
6
  getUserSetting: vi.fn(),
6
7
  getOrgSetting: vi.fn(),
7
8
  createEmbedSessionTicket: vi.fn(),
@@ -18,12 +19,26 @@ const mocks = vi.hoisted(() => ({
18
19
  signA2AToken: vi.fn(),
19
20
  getOrgA2ASecret: vi.fn(),
20
21
  getOrgDomain: vi.fn(),
22
+ isFeatureFlagEnabled: vi.fn(),
21
23
  }));
22
24
 
23
25
  vi.mock("@agent-native/core/server/agent-discovery", () => ({
24
26
  discoverAgents: mocks.discoverAgents,
25
27
  }));
26
28
 
29
+ vi.mock("./app-creation-store.js", () => ({
30
+ listWorkspaceApps: mocks.listWorkspaceApps,
31
+ }));
32
+
33
+ vi.mock("@agent-native/core/feature-flags", async (importOriginal) => {
34
+ const actual =
35
+ await importOriginal<typeof import("@agent-native/core/feature-flags")>();
36
+ return {
37
+ ...actual,
38
+ isFeatureFlagEnabled: mocks.isFeatureFlagEnabled,
39
+ };
40
+ });
41
+
27
42
  vi.mock("@agent-native/core/settings", () => ({
28
43
  getUserSetting: mocks.getUserSetting,
29
44
  getOrgSetting: mocks.getOrgSetting,
@@ -89,6 +104,7 @@ import { runWithRequestContext } from "@agent-native/core/server";
89
104
 
90
105
  import {
91
106
  createGrantedDispatchMcpEmbedSession,
107
+ createWorkspaceSsoEmbedSession,
92
108
  askGrantedDispatchMcpApp,
93
109
  getGrantedDispatchMcpAppTask,
94
110
  listGrantedDispatchMcpApps,
@@ -111,6 +127,7 @@ beforeEach(() => {
111
127
  mocks.a2aGetTask.mockReset();
112
128
  mocks.signA2AToken.mockReset();
113
129
  mocks.discoverAgents.mockResolvedValue([analyticsAgent]);
130
+ mocks.listWorkspaceApps.mockResolvedValue([]);
114
131
  mocks.getUserSetting.mockResolvedValue({ mode: "all-apps" });
115
132
  mocks.getOrgSetting.mockResolvedValue({ mode: "all-apps" });
116
133
  mocks.createEmbedSessionTicket.mockResolvedValue({
@@ -138,6 +155,7 @@ beforeEach(() => {
138
155
  mocks.signA2AToken.mockResolvedValue("signed-token");
139
156
  mocks.getOrgA2ASecret.mockResolvedValue(null);
140
157
  mocks.getOrgDomain.mockResolvedValue(null);
158
+ mocks.isFeatureFlagEnabled.mockResolvedValue(true);
141
159
  });
142
160
 
143
161
  afterEach(() => {
@@ -864,6 +882,129 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
864
882
  });
865
883
  });
866
884
 
885
+ it("keeps workspace sign-in behind the rollout flag", async () => {
886
+ mocks.isFeatureFlagEnabled.mockResolvedValueOnce(false);
887
+
888
+ await expect(
889
+ runWithRequestContext(
890
+ {
891
+ userEmail: "owner@example.test",
892
+ requestOrigin: "http://localhost:8092",
893
+ },
894
+ () =>
895
+ createWorkspaceSsoEmbedSession({
896
+ app: "analytics",
897
+ path: "/overview",
898
+ }),
899
+ ),
900
+ ).rejects.toThrow(/not enabled/);
901
+ expect(mocks.managerConstructor).not.toHaveBeenCalled();
902
+ });
903
+
904
+ it("allows an exact canonical app without requiring an MCP app grant", async () => {
905
+ mocks.getUserSetting.mockResolvedValue({
906
+ mode: "selected-apps",
907
+ selectedAppIds: [],
908
+ });
909
+ mocks.discoverAgents.mockResolvedValue([
910
+ {
911
+ ...analyticsAgent,
912
+ url: "https://analytics.agent-native.com",
913
+ },
914
+ ]);
915
+ mocks.managerCallTool.mockResolvedValueOnce({
916
+ structuredContent: {
917
+ startUrl:
918
+ "https://analytics.agent-native.com/_agent-native/embed/start?ticket=remote",
919
+ },
920
+ });
921
+
922
+ const result = await runWithRequestContext(
923
+ {
924
+ userEmail: "owner@example.test",
925
+ requestOrigin: "https://dispatch.agent-native.com",
926
+ },
927
+ () =>
928
+ createWorkspaceSsoEmbedSession({
929
+ app: "analytics",
930
+ path: "/overview",
931
+ chrome: "minimal",
932
+ }),
933
+ );
934
+
935
+ expect(result).toMatchObject({
936
+ app: "analytics",
937
+ startUrl:
938
+ "https://analytics.agent-native.com/_agent-native/embed/start?ticket=remote",
939
+ });
940
+ expect(mocks.managerConstructor).toHaveBeenCalled();
941
+ });
942
+
943
+ it("allows an exact custom registry entry and rejects an unregistered external app", async () => {
944
+ vi.stubEnv(
945
+ "IDENTITY_SSO_APP_REGISTRY_JSON",
946
+ JSON.stringify([
947
+ {
948
+ appId: "workspace",
949
+ clientId: "workspace-client",
950
+ origin: "https://workspace.example.com",
951
+ callbackPath: "/_agent-native/identity/callback",
952
+ capabilities: ["identity-sso"],
953
+ },
954
+ ]),
955
+ );
956
+ mocks.discoverAgents.mockResolvedValue([
957
+ {
958
+ id: "workspace",
959
+ name: "Workspace",
960
+ description: "Custom workspace app",
961
+ url: "https://workspace.example.com",
962
+ color: "#111827",
963
+ },
964
+ {
965
+ id: "unregistered",
966
+ name: "Unregistered",
967
+ description: "External app without registration",
968
+ url: "https://unregistered.example.com",
969
+ color: "#111827",
970
+ },
971
+ ]);
972
+ mocks.managerCallTool.mockResolvedValueOnce({
973
+ structuredContent: {
974
+ startUrl:
975
+ "https://workspace.example.com/_agent-native/embed/start?ticket=remote",
976
+ },
977
+ });
978
+
979
+ await expect(
980
+ runWithRequestContext(
981
+ {
982
+ userEmail: "owner@example.test",
983
+ requestOrigin: "https://dispatch.agent-native.com",
984
+ },
985
+ () =>
986
+ createWorkspaceSsoEmbedSession({
987
+ app: "workspace",
988
+ path: "/home",
989
+ }),
990
+ ),
991
+ ).resolves.toMatchObject({ app: "workspace" });
992
+
993
+ await expect(
994
+ runWithRequestContext(
995
+ {
996
+ userEmail: "owner@example.test",
997
+ requestOrigin: "https://dispatch.agent-native.com",
998
+ },
999
+ () =>
1000
+ createWorkspaceSsoEmbedSession({
1001
+ app: "unregistered",
1002
+ path: "/home",
1003
+ }),
1004
+ ),
1005
+ ).rejects.toThrow(/not registered/);
1006
+ });
1007
+
867
1008
  it("rejects traversal into Dispatch-owned embed routes on sibling apps", async () => {
868
1009
  await expect(
869
1010
  runWithRequestContext(
@@ -1,4 +1,5 @@
1
1
  import { A2AClient, signA2AToken, type Task } from "@agent-native/core/a2a";
2
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
2
3
  import {
3
4
  buildMcpToolName,
4
5
  McpClientManager,
@@ -17,6 +18,15 @@ import {
17
18
  type DiscoveredAgent,
18
19
  } from "@agent-native/core/server/agent-discovery";
19
20
 
21
+ import {
22
+ CANONICAL_WORKSPACE_SSO_APP_ORIGINS,
23
+ DISPATCH_WORKSPACE_SSO_FLAG,
24
+ parseWorkspaceSsoAppRegistrations,
25
+ } from "../../shared/workspace-sso.js";
26
+ import {
27
+ listWorkspaceApps,
28
+ type WorkspaceAppSummary,
29
+ } from "./app-creation-store.js";
20
30
  import {
21
31
  getDispatchMcpAppAccessSettings,
22
32
  isAppAllowedByMcpAccess,
@@ -447,6 +457,17 @@ function safeAppOrigin(app: DispatchMcpAccessibleApp): string | null {
447
457
  }
448
458
  }
449
459
 
460
+ function isLoopbackOrigin(origin: string): boolean {
461
+ try {
462
+ const hostname = new URL(origin).hostname;
463
+ return ["localhost", "127.0.0.1", "::1"].includes(hostname);
464
+ } catch {
465
+ // coercion-ok: only a previously validated app origin reaches this helper;
466
+ // malformed values are ineligible for the loopback development exception.
467
+ return false;
468
+ }
469
+ }
470
+
450
471
  function appBaseUrl(app: DispatchMcpAccessibleApp): string {
451
472
  return app.url.replace(/\/+$/, "");
452
473
  }
@@ -588,6 +609,116 @@ export async function resolveGrantedDispatchMcpApp(
588
609
  return match;
589
610
  }
590
611
 
612
+ function workspaceSsoOriginForApp(appId: string): string | null {
613
+ const canonical =
614
+ CANONICAL_WORKSPACE_SSO_APP_ORIGINS[
615
+ appId as keyof typeof CANONICAL_WORKSPACE_SSO_APP_ORIGINS
616
+ ];
617
+ if (canonical) return canonical;
618
+ const custom = parseWorkspaceSsoAppRegistrations(
619
+ process.env.IDENTITY_SSO_APP_REGISTRY_JSON,
620
+ ).find((registration) => registration.appId === appId);
621
+ return custom?.origin ?? null;
622
+ }
623
+
624
+ function isWorkspaceSsoCanonicalApp(app: DispatchMcpAccessibleApp): boolean {
625
+ const origin = safeAppOrigin(app);
626
+ if (!origin) return false;
627
+ const registeredOrigin = workspaceSsoOriginForApp(app.id);
628
+ if (origin === registeredOrigin) return true;
629
+ // The built-in discovery table intentionally points at local dev ports when
630
+ // running outside production. Keep local development usable without ever
631
+ // weakening the production exact-origin check.
632
+ return process.env.NODE_ENV !== "production" && isLoopbackOrigin(origin)
633
+ ? Object.prototype.hasOwnProperty.call(
634
+ CANONICAL_WORKSPACE_SSO_APP_ORIGINS,
635
+ app.id,
636
+ )
637
+ : false;
638
+ }
639
+
640
+ function sameWorkspaceApp(
641
+ candidate: DispatchMcpAccessibleApp,
642
+ mounted: WorkspaceAppSummary,
643
+ ): boolean {
644
+ if (candidate.id !== mounted.id || !mounted.url) return false;
645
+ return candidate.url.replace(/\/+$/, "") === mounted.url.replace(/\/+$/, "");
646
+ }
647
+
648
+ async function isEligibleWorkspaceSsoApp(
649
+ candidate: DispatchMcpAccessibleApp,
650
+ mountedApps: WorkspaceAppSummary[],
651
+ ): Promise<boolean> {
652
+ const origin = safeAppOrigin(candidate);
653
+ if (!origin) return false;
654
+ if (isWorkspaceSsoCanonicalApp(candidate)) return true;
655
+
656
+ const customOrigin = workspaceSsoOriginForApp(candidate.id);
657
+ if (customOrigin === origin) return true;
658
+
659
+ const dispatchOrigin = safeAppOrigin({
660
+ id: DISPATCH_APP_ID,
661
+ name: DISPATCH_NAME,
662
+ description: DISPATCH_DESCRIPTION,
663
+ url: dispatchSelfBaseUrl(),
664
+ color: DISPATCH_COLOR,
665
+ granted: true,
666
+ });
667
+ if (origin !== dispatchOrigin) return false;
668
+ return mountedApps.some((mounted) => sameWorkspaceApp(candidate, mounted));
669
+ }
670
+
671
+ async function listWorkspaceSsoApps(): Promise<DispatchMcpAccessibleApp[]> {
672
+ const [agents, mountedApps] = await Promise.all([
673
+ discoverAgents("dispatch"),
674
+ listWorkspaceApps({ includeAgentCards: false }),
675
+ ]);
676
+ const candidates = agents
677
+ .filter((agent) => normalizeAppId(agent.id) !== DISPATCH_APP_ID)
678
+ .map((agent) => ({
679
+ ...toAccessibleApp(agent, {
680
+ mode: "all-apps",
681
+ selectedAppIds: [],
682
+ }),
683
+ granted: true,
684
+ }));
685
+ const eligible = [] as DispatchMcpAccessibleApp[];
686
+ for (const candidate of candidates) {
687
+ if (await isEligibleWorkspaceSsoApp(candidate, mountedApps)) {
688
+ eligible.push(candidate);
689
+ }
690
+ }
691
+ return [
692
+ {
693
+ id: DISPATCH_APP_ID,
694
+ name: DISPATCH_NAME,
695
+ description: DISPATCH_DESCRIPTION,
696
+ url: dispatchSelfBaseUrl(),
697
+ color: DISPATCH_COLOR,
698
+ granted: true,
699
+ },
700
+ ...eligible,
701
+ ];
702
+ }
703
+
704
+ async function resolveWorkspaceSsoApp(
705
+ app: string,
706
+ ): Promise<DispatchMcpAccessibleApp> {
707
+ const target = normalizeAppId(app);
708
+ if (!target) throw new Error("app is required");
709
+ const apps = await listWorkspaceSsoApps();
710
+ const match = apps.find(
711
+ (candidate) =>
712
+ candidate.id === target || candidate.name.toLowerCase() === target,
713
+ );
714
+ if (!match) {
715
+ throw new Error(
716
+ `Workspace app "${app}" is not registered for workspace sign-in.`,
717
+ );
718
+ }
719
+ return match;
720
+ }
721
+
591
722
  export async function askGrantedDispatchMcpApp(
592
723
  app: string,
593
724
  message: string,
@@ -860,13 +991,20 @@ async function callTargetCreateEmbedSession(input: {
860
991
  }
861
992
  }
862
993
 
863
- async function resolveDispatchEmbedTarget(input: {
864
- app?: string;
865
- url?: string;
866
- path?: string;
867
- }): Promise<{ app: DispatchMcpAccessibleApp; path: string; url: string }> {
994
+ async function resolveEmbedTarget(
995
+ input: {
996
+ app?: string;
997
+ url?: string;
998
+ path?: string;
999
+ },
1000
+ options: {
1001
+ resolveApp: (app: string) => Promise<DispatchMcpAccessibleApp>;
1002
+ listApps: () => Promise<DispatchMcpAccessibleApp[]>;
1003
+ urlError: string;
1004
+ },
1005
+ ): Promise<{ app: DispatchMcpAccessibleApp; path: string; url: string }> {
868
1006
  const explicitApp = input.app?.trim()
869
- ? await resolveGrantedDispatchMcpApp(input.app)
1007
+ ? await options.resolveApp(input.app)
870
1008
  : null;
871
1009
  if (explicitApp && input.path) {
872
1010
  const path = safeAppPath(input.path);
@@ -899,14 +1037,12 @@ async function resolveDispatchEmbedTarget(input: {
899
1037
  };
900
1038
  }
901
1039
 
902
- const apps = explicitApp ? [explicitApp] : await listGrantedDispatchMcpApps();
1040
+ const apps = explicitApp ? [explicitApp] : await options.listApps();
903
1041
  const target = apps
904
1042
  .filter((app) => appMatchesUrlPath(app, parsed))
905
1043
  .sort((a, b) => appPathSpecificity(b) - appPathSpecificity(a))[0];
906
1044
  if (!target) {
907
- throw new Error(
908
- "Embed URL must belong to an app granted through Dispatch.",
909
- );
1045
+ throw new Error(options.urlError);
910
1046
  }
911
1047
  const path = safeAppPath(appRelativePath(target, parsed));
912
1048
  if (!path) throw new Error("Embed URL path is not safe.");
@@ -914,6 +1050,31 @@ async function resolveDispatchEmbedTarget(input: {
914
1050
  return { app: target, path, url: `${appBaseUrl(target)}${path}` };
915
1051
  }
916
1052
 
1053
+ async function resolveDispatchEmbedTarget(input: {
1054
+ app?: string;
1055
+ url?: string;
1056
+ path?: string;
1057
+ }): Promise<{ app: DispatchMcpAccessibleApp; path: string; url: string }> {
1058
+ return resolveEmbedTarget(input, {
1059
+ resolveApp: resolveGrantedDispatchMcpApp,
1060
+ listApps: listGrantedDispatchMcpApps,
1061
+ urlError: "Embed URL must belong to an app granted through Dispatch.",
1062
+ });
1063
+ }
1064
+
1065
+ async function resolveWorkspaceSsoEmbedTarget(input: {
1066
+ app?: string;
1067
+ url?: string;
1068
+ path?: string;
1069
+ }): Promise<{ app: DispatchMcpAccessibleApp; path: string; url: string }> {
1070
+ return resolveEmbedTarget(input, {
1071
+ resolveApp: resolveWorkspaceSsoApp,
1072
+ listApps: listWorkspaceSsoApps,
1073
+ urlError:
1074
+ "Embed URL must belong to an app registered for Dispatch workspace sign-in.",
1075
+ });
1076
+ }
1077
+
917
1078
  async function createDispatchSelfEmbedSession(input: {
918
1079
  ownerEmail: string;
919
1080
  orgId?: string;
@@ -956,14 +1117,34 @@ export async function createGrantedDispatchMcpEmbedSession(input: {
956
1117
  if (!userEmail) throw new Error("no authenticated user");
957
1118
  const target = await resolveDispatchEmbedTarget(input);
958
1119
 
959
- const orgId = getRequestOrgId();
1120
+ return createEmbedSessionForResolvedApp({
1121
+ ownerEmail: userEmail,
1122
+ orgId: getRequestOrgId(),
1123
+ target,
1124
+ chrome: input.chrome,
1125
+ });
1126
+ }
1127
+
1128
+ async function createEmbedSessionForResolvedApp(input: {
1129
+ ownerEmail: string;
1130
+ orgId?: string;
1131
+ target: { app: DispatchMcpAccessibleApp; path: string; url: string };
1132
+ chrome?: "full" | "minimal";
1133
+ }): Promise<{
1134
+ startUrl: string;
1135
+ targetPath?: string;
1136
+ expiresAt?: number;
1137
+ app: string;
1138
+ }> {
1139
+ const { ownerEmail, orgId, target, chrome } = input;
1140
+
960
1141
  if (target.app.id === DISPATCH_APP_ID) {
961
1142
  return createDispatchSelfEmbedSession({
962
- ownerEmail: userEmail,
1143
+ ownerEmail,
963
1144
  orgId,
964
1145
  path: target.path,
965
1146
  baseUrl: appBaseUrl(target.app),
966
- chrome: input.chrome,
1147
+ chrome,
967
1148
  });
968
1149
  }
969
1150
 
@@ -980,7 +1161,7 @@ export async function createGrantedDispatchMcpEmbedSession(input: {
980
1161
  const useOrgSigning = usableOrgDomain && usableOrgSecret;
981
1162
  const signedOrgDomain = usableOrgDomain ? orgDomain.trim() : undefined;
982
1163
  const token = await signA2AToken(
983
- userEmail,
1164
+ ownerEmail,
984
1165
  signedOrgDomain,
985
1166
  useOrgSigning ? orgSecret.trim() : undefined,
986
1167
  {
@@ -996,7 +1177,7 @@ export async function createGrantedDispatchMcpEmbedSession(input: {
996
1177
  app: target.app,
997
1178
  token,
998
1179
  url: target.url,
999
- chrome: input.chrome,
1180
+ chrome,
1000
1181
  });
1001
1182
  const parsed = parseMcpToolTextResult(result) as {
1002
1183
  startUrl?: string;
@@ -1019,3 +1200,34 @@ export async function createGrantedDispatchMcpEmbedSession(input: {
1019
1200
  if (typeof parsed.expiresAt === "number") output.expiresAt = parsed.expiresAt;
1020
1201
  return output;
1021
1202
  }
1203
+
1204
+ export async function createWorkspaceSsoEmbedSession(input: {
1205
+ app?: string;
1206
+ url?: string;
1207
+ path?: string;
1208
+ chrome?: "full" | "minimal";
1209
+ }): Promise<{
1210
+ startUrl: string;
1211
+ targetPath?: string;
1212
+ expiresAt?: number;
1213
+ app: string;
1214
+ }> {
1215
+ const ownerEmail = getRequestUserEmail();
1216
+ if (!ownerEmail) throw new Error("no authenticated user");
1217
+ const enabled = await isFeatureFlagEnabled(DISPATCH_WORKSPACE_SSO_FLAG, {
1218
+ userEmail: ownerEmail,
1219
+ userKey: ownerEmail,
1220
+ orgId: getRequestOrgId(),
1221
+ });
1222
+ if (!enabled) {
1223
+ throw new Error("Dispatch workspace sign-in is not enabled.");
1224
+ }
1225
+
1226
+ const target = await resolveWorkspaceSsoEmbedTarget(input);
1227
+ return createEmbedSessionForResolvedApp({
1228
+ ownerEmail,
1229
+ orgId: getRequestOrgId(),
1230
+ target,
1231
+ chrome: input.chrome,
1232
+ });
1233
+ }
@@ -83,7 +83,13 @@ interface MaterializableWorkspaceResource {
83
83
  function mimeTypeForWorkspaceResource(
84
84
  resource: MaterializableWorkspaceResource,
85
85
  ) {
86
- return resource.path.endsWith(".json") ? "application/json" : "text/markdown";
86
+ if (resource.path.endsWith(".json")) return "application/json";
87
+ if (resource.path.endsWith(".txt")) return "text/plain";
88
+ if (resource.path.endsWith(".csv")) return "text/csv";
89
+ if (resource.path.endsWith(".yaml") || resource.path.endsWith(".yml")) {
90
+ return "text/yaml";
91
+ }
92
+ return "text/markdown";
87
93
  }
88
94
 
89
95
  function parseResourceMetadata(metadata: string | null): Record<string, any> {
@@ -188,6 +194,7 @@ export type WorkspaceResourceKind =
188
194
  | "skill"
189
195
  | "instruction"
190
196
  | "agent"
197
+ | "agent-file"
191
198
  | "knowledge"
192
199
  | "mcp-server";
193
200
  export type WorkspaceResourceScope = "all" | "selected";
@@ -526,7 +533,7 @@ async function writeStarterSeedMarker(ctx: WorkspaceResourceCtx) {
526
533
  }
527
534
  }
528
535
 
529
- async function getWorkspaceResourceByPath(
536
+ export async function getWorkspaceResourceByPath(
530
537
  resourcePath: string,
531
538
  ctx: WorkspaceResourceCtx,
532
539
  ) {
@@ -13,6 +13,10 @@ const INITIAL_TOOL_NAMES = [
13
13
  "list-workspace-resources",
14
14
  "create-workspace-resource",
15
15
  "update-workspace-resource",
16
+ "import-agent",
17
+ "import-agent-pack",
18
+ "list-agent-pack",
19
+ "connect-external-agent",
16
20
  "list-vault-secrets",
17
21
  "request-vault-secret",
18
22
  "create-vault-secret",
@@ -62,6 +66,15 @@ Use the standard workspace primitives:
62
66
  - Read and update resources like AGENTS.md, LEARNINGS.md, jobs/*.md, agents/*.md, and remote-agents/*.json when appropriate.
63
67
  - Use recurring jobs for scheduled behavior.
64
68
  - Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.
69
+ - For a Claude-style Markdown or JSON agent setup, use import-agent so the
70
+ profile is normalized into agents/<slug>.md. Never import credentials, hooks,
71
+ shell commands, or local environment settings.
72
+ - For a folder of agent instructions, references, or skills, use
73
+ import-agent-pack. It preserves the text files under agents/<slug>/ and
74
+ creates the normalized profile alongside them. Use list-agent-pack to inspect
75
+ the pack before editing or handing it off to an app.
76
+ - For an existing HTTP/A2A endpoint, use connect-external-agent and let the
77
+ normal A2A/MCP connection flow handle authentication.
65
78
  - You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.
66
79
  - Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
67
80
  - When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.
@@ -0,0 +1,13 @@
1
+ import { defineFeatureFlag } from "@agent-native/core/feature-flags";
2
+
3
+ /**
4
+ * Browser-only rollout for app-scoped sessions in Dispatch panes. The server
5
+ * action checks this flag too; the client hook only selects the presentation
6
+ * path and keeps the legacy embed route available while the flag is off.
7
+ */
8
+ export const DISPATCH_WORKSPACE_SSO_FLAG = defineFeatureFlag({
9
+ key: "dispatch.workspace-sso",
10
+ displayName: "Dispatch workspace sign-in",
11
+ description:
12
+ "Let Dispatch use the signed-in workspace identity for exact registered app panes.",
13
+ });
@@ -0,0 +1,118 @@
1
+ import { DISPATCH_WORKSPACE_SSO_FLAG } from "./feature-flags.js";
2
+
3
+ export { DISPATCH_WORKSPACE_SSO_FLAG };
4
+
5
+ /** Exact first-party origins. Never replace this with a suffix or wildcard. */
6
+ export const CANONICAL_WORKSPACE_SSO_APP_ORIGINS = {
7
+ analytics: "https://analytics.agent-native.com",
8
+ assets: "https://assets.agent-native.com",
9
+ brain: "https://brain.agent-native.com",
10
+ calendar: "https://calendar.agent-native.com",
11
+ chat: "https://chat.agent-native.com",
12
+ clips: "https://clips.agent-native.com",
13
+ content: "https://content.agent-native.com",
14
+ crm: "https://crm.agent-native.com",
15
+ design: "https://design.agent-native.com",
16
+ dispatch: "https://dispatch.agent-native.com",
17
+ forms: "https://forms.agent-native.com",
18
+ macros: "https://macros.agent-native.com",
19
+ mail: "https://mail.agent-native.com",
20
+ plan: "https://plan.agent-native.com",
21
+ slides: "https://slides.agent-native.com",
22
+ tasks: "https://tasks.agent-native.com",
23
+ } as const;
24
+
25
+ export const WORKSPACE_SSO_CALLBACK_PATH =
26
+ "/_agent-native/identity/callback" as const;
27
+
28
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
29
+ const APP_ID = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
30
+ const CLIENT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
31
+ const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
32
+
33
+ export interface WorkspaceSsoAppRegistration {
34
+ appId: string;
35
+ clientId: string;
36
+ origin: string;
37
+ callbackPath: typeof WORKSPACE_SSO_CALLBACK_PATH;
38
+ }
39
+
40
+ export function exactWorkspaceSsoOrigin(raw: unknown): string | null {
41
+ if (typeof raw !== "string" || !raw || CONTROL_CHARS.test(raw)) return null;
42
+ try {
43
+ const url = new URL(raw);
44
+ if (
45
+ url.protocol !== "https:" &&
46
+ !(url.protocol === "http:" && LOCALHOST_HOSTS.has(url.hostname))
47
+ ) {
48
+ return null;
49
+ }
50
+ if (
51
+ url.username ||
52
+ url.password ||
53
+ url.pathname !== "/" ||
54
+ url.search ||
55
+ url.hash
56
+ ) {
57
+ return null;
58
+ }
59
+ return url.origin;
60
+ } catch {
61
+ // coercion-ok: malformed registry origins are rejected as absent.
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Parse the same additive custom registry used by browser identity SSO. The
68
+ * raw value is passed in so this shared module stays safe to import in the
69
+ * browser bundle.
70
+ */
71
+ export function parseWorkspaceSsoAppRegistrations(
72
+ raw: unknown,
73
+ ): WorkspaceSsoAppRegistration[] {
74
+ if (typeof raw !== "string" || !raw.trim()) return [];
75
+ let value: unknown;
76
+ try {
77
+ value = JSON.parse(raw);
78
+ } catch {
79
+ // coercion-ok: malformed custom registry data must fail closed.
80
+ return [];
81
+ }
82
+ if (!Array.isArray(value)) return [];
83
+
84
+ const registrations: WorkspaceSsoAppRegistration[] = [];
85
+ const seen = new Set<string>();
86
+ for (const entry of value) {
87
+ if (!entry || typeof entry !== "object") continue;
88
+ const candidate = entry as Record<string, unknown>;
89
+ const appId = candidate.appId;
90
+ const clientId = candidate.clientId;
91
+ const origin = exactWorkspaceSsoOrigin(candidate.origin);
92
+ if (
93
+ typeof appId !== "string" ||
94
+ !APP_ID.test(appId) ||
95
+ typeof clientId !== "string" ||
96
+ !CLIENT_ID.test(clientId) ||
97
+ !origin ||
98
+ candidate.callbackPath !== WORKSPACE_SSO_CALLBACK_PATH ||
99
+ !Array.isArray(candidate.capabilities) ||
100
+ !candidate.capabilities.includes("identity-sso") ||
101
+ seen.has(appId) ||
102
+ Object.prototype.hasOwnProperty.call(
103
+ CANONICAL_WORKSPACE_SSO_APP_ORIGINS,
104
+ appId,
105
+ )
106
+ ) {
107
+ continue;
108
+ }
109
+ seen.add(appId);
110
+ registrations.push({
111
+ appId,
112
+ clientId,
113
+ origin,
114
+ callbackPath: WORKSPACE_SSO_CALLBACK_PATH,
115
+ });
116
+ }
117
+ return registrations;
118
+ }