@agent-native/dispatch 0.27.6 → 0.27.8
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/actions/create_workspace_app_embed_session.d.ts +6 -0
- package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -1
- package/dist/actions/create_workspace_app_embed_session.js +49 -1
- package/dist/actions/create_workspace_app_embed_session.js.map +1 -1
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/update-workspace-resource.d.ts +13 -13
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +15 -53
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +2 -1
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/workspace-app-action-auth.d.ts.map +1 -1
- package/dist/server/lib/workspace-app-action-auth.js +15 -0
- package/dist/server/lib/workspace-app-action-auth.js.map +1 -1
- package/dist/shared/feature-flags.d.ts +1 -0
- package/dist/shared/feature-flags.d.ts.map +1 -1
- package/dist/shared/feature-flags.js +5 -0
- package/dist/shared/feature-flags.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/create_workspace_app_embed_session.ts +60 -1
- package/src/components/workspace-app-host.spec.tsx +22 -0
- package/src/components/workspace-app-host.tsx +14 -80
- package/src/server/lib/mcp-gateway.spec.ts +5 -0
- package/src/server/lib/mcp-gateway.ts +7 -1
- package/src/server/lib/workspace-app-action-auth.ts +17 -0
- package/src/shared/feature-flags.ts +7 -0
|
@@ -11,12 +11,7 @@ import {
|
|
|
11
11
|
} from "@agent-native/core/client/hooks";
|
|
12
12
|
import { useT } from "@agent-native/core/client/i18n";
|
|
13
13
|
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
14
|
-
import {
|
|
15
|
-
IconArrowLeft,
|
|
16
|
-
IconArrowUpRight,
|
|
17
|
-
IconClockHour4,
|
|
18
|
-
IconLock,
|
|
19
|
-
} from "@tabler/icons-react";
|
|
14
|
+
import { IconArrowLeft, IconClockHour4 } from "@tabler/icons-react";
|
|
20
15
|
import { useTheme } from "next-themes";
|
|
21
16
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
22
17
|
import { Link } from "react-router";
|
|
@@ -57,31 +52,6 @@ interface GrantedWorkspaceAppsResult {
|
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
type WorkspaceAppTheme = "light" | "dark";
|
|
60
|
-
type WorkspaceAppAuthState = "unknown" | "authenticated" | "unauthenticated";
|
|
61
|
-
|
|
62
|
-
function resolveWorkspaceAppAuthState(
|
|
63
|
-
rawUrl: string | null,
|
|
64
|
-
): WorkspaceAppAuthState {
|
|
65
|
-
if (!rawUrl) return "unknown";
|
|
66
|
-
try {
|
|
67
|
-
const lastSegment = new URL(rawUrl, window.location.origin).pathname
|
|
68
|
-
.split("/")
|
|
69
|
-
.filter(Boolean)
|
|
70
|
-
.at(-1)
|
|
71
|
-
?.toLowerCase();
|
|
72
|
-
if (
|
|
73
|
-
lastSegment === "sign-in" ||
|
|
74
|
-
lastSegment === "login" ||
|
|
75
|
-
lastSegment === "signup"
|
|
76
|
-
) {
|
|
77
|
-
return "unauthenticated";
|
|
78
|
-
}
|
|
79
|
-
return "authenticated";
|
|
80
|
-
} catch {
|
|
81
|
-
return "unknown";
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
55
|
function buildWorkspaceAppThemeUpdate(theme: WorkspaceAppTheme) {
|
|
86
56
|
return {
|
|
87
57
|
type: "agent-native-theme-update" as const,
|
|
@@ -131,7 +101,6 @@ export function WorkspaceAppFrame({
|
|
|
131
101
|
const [embedError, setEmbedError] = useState<Error | null>(null);
|
|
132
102
|
const [isDirectFallback, setIsDirectFallback] = useState(false);
|
|
133
103
|
const [embedAttempt, setEmbedAttempt] = useState(0);
|
|
134
|
-
const [authState, setAuthState] = useState<WorkspaceAppAuthState>("unknown");
|
|
135
104
|
const embedFrameRef = useRef<HTMLIFrameElement>(null);
|
|
136
105
|
const postThemeToFrame = useCallback(() => {
|
|
137
106
|
embedFrameRef.current?.contentWindow?.postMessage(
|
|
@@ -142,17 +111,7 @@ export function WorkspaceAppFrame({
|
|
|
142
111
|
const handleFrameLoad = useCallback(() => {
|
|
143
112
|
postThemeToFrame();
|
|
144
113
|
if (isDirectFallback) setEmbedError(null);
|
|
145
|
-
|
|
146
|
-
let frameUrl = embedUrl;
|
|
147
|
-
try {
|
|
148
|
-
frameUrl = frame?.contentWindow?.location.href ?? frameUrl;
|
|
149
|
-
// coercion-ok: Cross-origin frames cannot expose location; their auth state arrives by message.
|
|
150
|
-
} catch {
|
|
151
|
-
// Cross-origin app frames report their auth state through postMessage.
|
|
152
|
-
}
|
|
153
|
-
const nextAuthState = resolveWorkspaceAppAuthState(frameUrl);
|
|
154
|
-
if (nextAuthState !== "unknown") setAuthState(nextAuthState);
|
|
155
|
-
}, [embedUrl, isDirectFallback, postThemeToFrame]);
|
|
114
|
+
}, [isDirectFallback, postThemeToFrame]);
|
|
156
115
|
const workspaceSsoEnabled = useFeatureFlag(DISPATCH_WORKSPACE_SSO_FLAG.key);
|
|
157
116
|
const createEmbedSession = useActionMutation<
|
|
158
117
|
EmbedSessionResult,
|
|
@@ -190,7 +149,6 @@ export function WorkspaceAppFrame({
|
|
|
190
149
|
setEmbedUrl(null);
|
|
191
150
|
setEmbedError(null);
|
|
192
151
|
setIsDirectFallback(false);
|
|
193
|
-
setAuthState("unknown");
|
|
194
152
|
const createSession = workspaceSsoEnabled
|
|
195
153
|
? createWorkspaceSsoEmbedSession
|
|
196
154
|
: createEmbedSession;
|
|
@@ -201,6 +159,17 @@ export function WorkspaceAppFrame({
|
|
|
201
159
|
})
|
|
202
160
|
.catch((cause: unknown) => {
|
|
203
161
|
if (cancelled) return;
|
|
162
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
163
|
+
if (workspaceSsoEnabled) {
|
|
164
|
+
// An SSO-enabled pane must never fall back to the child app's
|
|
165
|
+
// unauthenticated shell. Keep the parent-owned retry surface in
|
|
166
|
+
// place so a transient exchange failure cannot expose another
|
|
167
|
+
// login form.
|
|
168
|
+
setIsDirectFallback(false);
|
|
169
|
+
setEmbedUrl(null);
|
|
170
|
+
setEmbedError(error);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
204
173
|
setIsDirectFallback(true);
|
|
205
174
|
setEmbedUrl(
|
|
206
175
|
workspaceAppDirectHref(
|
|
@@ -208,9 +177,7 @@ export function WorkspaceAppFrame({
|
|
|
208
177
|
embedPath ?? "/",
|
|
209
178
|
),
|
|
210
179
|
);
|
|
211
|
-
setEmbedError(
|
|
212
|
-
cause instanceof Error ? cause : new Error(String(cause)),
|
|
213
|
-
);
|
|
180
|
+
setEmbedError(error);
|
|
214
181
|
});
|
|
215
182
|
return () => {
|
|
216
183
|
cancelled = true;
|
|
@@ -242,21 +209,6 @@ export function WorkspaceAppFrame({
|
|
|
242
209
|
window.removeEventListener("message", handleEmbedSessionExpired);
|
|
243
210
|
}, [embedUrl]);
|
|
244
211
|
|
|
245
|
-
useEffect(() => {
|
|
246
|
-
const handleAuthState = (event: MessageEvent) => {
|
|
247
|
-
const frame = embedFrameRef.current;
|
|
248
|
-
if (!frame || event.source !== frame.contentWindow) return;
|
|
249
|
-
if (event.data?.type !== "agentNative.authState") return;
|
|
250
|
-
const status = event.data.data?.status;
|
|
251
|
-
if (status === "authenticated" || status === "unauthenticated") {
|
|
252
|
-
setAuthState(status);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
window.addEventListener("message", handleAuthState);
|
|
257
|
-
return () => window.removeEventListener("message", handleAuthState);
|
|
258
|
-
}, []);
|
|
259
|
-
|
|
260
212
|
useEffect(() => {
|
|
261
213
|
postThemeToFrame();
|
|
262
214
|
}, [embedUrl, postThemeToFrame]);
|
|
@@ -315,24 +267,6 @@ export function WorkspaceAppFrame({
|
|
|
315
267
|
dynamicSuggestions={false}
|
|
316
268
|
suggestions={[]}
|
|
317
269
|
emptyStateText={`Ask about ${app.name}`}
|
|
318
|
-
composerSlot={
|
|
319
|
-
authState === "unauthenticated" ? (
|
|
320
|
-
<div className="flex shrink-0 items-center px-3 pb-1">
|
|
321
|
-
<button
|
|
322
|
-
type="button"
|
|
323
|
-
data-dispatch-app-sign-in
|
|
324
|
-
aria-label={`Sign in to ${app.name} on the right`}
|
|
325
|
-
title={`Sign in to ${app.name} on the right`}
|
|
326
|
-
onClick={() => embedFrameRef.current?.focus()}
|
|
327
|
-
className="inline-flex h-6 shrink-0 items-center gap-1 rounded-full border border-border/70 bg-background/60 px-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
328
|
-
>
|
|
329
|
-
<IconLock size={12} stroke={1.8} />
|
|
330
|
-
<span>Sign in on the right</span>
|
|
331
|
-
<IconArrowUpRight size={12} stroke={1.8} />
|
|
332
|
-
</button>
|
|
333
|
-
</div>
|
|
334
|
-
) : null
|
|
335
|
-
}
|
|
336
270
|
>
|
|
337
271
|
{appPane}
|
|
338
272
|
</AgentSidebar>
|
|
@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
17
17
|
a2aSend: vi.fn(),
|
|
18
18
|
a2aGetTask: vi.fn(),
|
|
19
19
|
signA2AToken: vi.fn(),
|
|
20
|
+
canonicalA2AAudience: vi.fn((url: string) => url.replace(/\/+$/, "")),
|
|
20
21
|
getOrgA2ASecret: vi.fn(),
|
|
21
22
|
getOrgDomain: vi.fn(),
|
|
22
23
|
isFeatureFlagEnabled: vi.fn(),
|
|
@@ -71,6 +72,7 @@ vi.mock("@agent-native/core/a2a", () => ({
|
|
|
71
72
|
}
|
|
72
73
|
},
|
|
73
74
|
signA2AToken: mocks.signA2AToken,
|
|
75
|
+
canonicalA2AAudience: mocks.canonicalA2AAudience,
|
|
74
76
|
}));
|
|
75
77
|
|
|
76
78
|
vi.mock("@agent-native/core/org", () => ({
|
|
@@ -1255,6 +1257,7 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
1255
1257
|
"org-specific-secret",
|
|
1256
1258
|
{
|
|
1257
1259
|
expiresIn: "5m",
|
|
1260
|
+
audience: "http://localhost:8086",
|
|
1258
1261
|
preferGlobalSecret: false,
|
|
1259
1262
|
},
|
|
1260
1263
|
);
|
|
@@ -1283,6 +1286,7 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
1283
1286
|
undefined,
|
|
1284
1287
|
{
|
|
1285
1288
|
expiresIn: "5m",
|
|
1289
|
+
audience: "http://localhost:8086",
|
|
1286
1290
|
preferGlobalSecret: true,
|
|
1287
1291
|
},
|
|
1288
1292
|
);
|
|
@@ -1311,6 +1315,7 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
1311
1315
|
undefined,
|
|
1312
1316
|
{
|
|
1313
1317
|
expiresIn: "5m",
|
|
1318
|
+
audience: "http://localhost:8086",
|
|
1314
1319
|
preferGlobalSecret: true,
|
|
1315
1320
|
},
|
|
1316
1321
|
);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
A2AClient,
|
|
3
|
+
canonicalA2AAudience,
|
|
4
|
+
signA2AToken,
|
|
5
|
+
type Task,
|
|
6
|
+
} from "@agent-native/core/a2a";
|
|
2
7
|
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
3
8
|
import {
|
|
4
9
|
buildMcpToolName,
|
|
@@ -1195,6 +1200,7 @@ async function createEmbedSessionForResolvedApp(input: {
|
|
|
1195
1200
|
useOrgSigning ? orgSecret.trim() : undefined,
|
|
1196
1201
|
{
|
|
1197
1202
|
expiresIn: "5m",
|
|
1203
|
+
audience: canonicalA2AAudience(appBaseUrl(target.app)),
|
|
1198
1204
|
// Prefer the synced org A2A secret when present because first-party
|
|
1199
1205
|
// production apps do not have to share the same deployment env secret.
|
|
1200
1206
|
// Fall back to the global A2A_SECRET for orgs that have not synced yet.
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ActionRouteAuthAdapter,
|
|
9
9
|
ActionRouteResolvedCaller,
|
|
10
10
|
} from "@agent-native/core/server";
|
|
11
|
+
import { getSession } from "@agent-native/core/server";
|
|
11
12
|
|
|
12
13
|
export const WORKSPACE_APPS_ACTION_PATH =
|
|
13
14
|
"/_agent-native/actions/list-workspace-apps";
|
|
@@ -57,6 +58,22 @@ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
|
|
|
57
58
|
throw new Error("Invalid workspace registry authorization");
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Native clients use their normal persisted session bearer for the same
|
|
62
|
+
// read-only action. Let the framework resolve that credential before
|
|
63
|
+
// treating an otherwise opaque bearer as an A2A token.
|
|
64
|
+
const hasRequestUrl =
|
|
65
|
+
typeof event?.node?.req?.url === "string" ||
|
|
66
|
+
typeof event?.req?.url === "string" ||
|
|
67
|
+
typeof event?.url === "string";
|
|
68
|
+
const session = hasRequestUrl ? await getSession(event) : null;
|
|
69
|
+
if (session?.email) {
|
|
70
|
+
return {
|
|
71
|
+
owner: session.email,
|
|
72
|
+
anonymous: false,
|
|
73
|
+
...(session.orgId ? { orgId: session.orgId } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
const token = authorization.slice("Bearer ".length).trim();
|
|
61
78
|
if (!token) throw new Error("Invalid workspace registry authorization");
|
|
62
79
|
|
|
@@ -11,3 +11,10 @@ export const DISPATCH_WORKSPACE_SSO_FLAG = defineFeatureFlag({
|
|
|
11
11
|
description:
|
|
12
12
|
"Let Dispatch use the signed-in workspace identity for exact registered app panes.",
|
|
13
13
|
});
|
|
14
|
+
|
|
15
|
+
export const DISPATCH_WORKSPACE_APP_LIST_FLAG = defineFeatureFlag({
|
|
16
|
+
key: "dispatch.workspace-app-list",
|
|
17
|
+
displayName: "Workspace app list",
|
|
18
|
+
description:
|
|
19
|
+
"Show apps from the signed-in workspace in native desktop and mobile app lists.",
|
|
20
|
+
});
|