@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.
- package/CHANGELOG.md +28 -10
- package/dist/actions/provider-api-register.d.ts +11 -11
- package/dist/actions/upsert-destination.d.ts +12 -12
- package/dist/components/action-query-error.d.ts.map +1 -1
- package/dist/components/action-query-error.js +8 -0
- package/dist/components/action-query-error.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +48 -3
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +8 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +71 -11
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/workspace-app-chat-proxy.d.ts +6 -5
- package/dist/server/lib/workspace-app-chat-proxy.d.ts.map +1 -1
- package/dist/server/lib/workspace-app-chat-proxy.js +78 -11
- package/dist/server/lib/workspace-app-chat-proxy.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +1 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/package.json +3 -3
- package/src/components/action-query-error.spec.tsx +37 -0
- package/src/components/action-query-error.tsx +11 -0
- package/src/server/lib/app-creation-store.spec.ts +101 -1
- package/src/server/lib/app-creation-store.ts +65 -4
- package/src/server/lib/mcp-gateway.spec.ts +135 -1
- package/src/server/lib/mcp-gateway.ts +122 -22
- package/src/server/lib/workspace-app-chat-proxy.spec.ts +309 -5
- package/src/server/lib/workspace-app-chat-proxy.ts +123 -25
- package/src/server/plugins/agent-chat.spec.ts +1 -0
- package/src/server/plugins/agent-chat.ts +1 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getOrgContext } from "@agent-native/core/org";
|
|
2
|
-
import { getSession, runWithRequestContext } from "@agent-native/core/server";
|
|
2
|
+
import { getAppProductionUrl, getForwardedRequestOrigin, getSession, resolveAppRuntimeUrl, runWithRequestContext, } from "@agent-native/core/server";
|
|
3
3
|
import { EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, } from "@agent-native/core/shared";
|
|
4
4
|
import { parseWorkspaceAppChatProxyPath } from "../../shared/workspace-app-chat.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat, createWorkspaceSsoEmbedSessionForWorkspaceAppChat, } from "./mcp-gateway.js";
|
|
6
6
|
const TARGET_CHAT_PATH = "/_agent-native/agent-chat";
|
|
7
7
|
/**
|
|
8
8
|
* `X-Agent-Native-Embed-Target` names the path a bearer embed token was minted
|
|
@@ -43,7 +43,48 @@ const DROPPED_RESPONSE_HEADERS = new Set([
|
|
|
43
43
|
]);
|
|
44
44
|
const sessionCache = new Map();
|
|
45
45
|
function sessionCacheKey(input) {
|
|
46
|
-
return [
|
|
46
|
+
return [
|
|
47
|
+
input.userEmail,
|
|
48
|
+
input.orgId ?? "",
|
|
49
|
+
input.requestOrigin,
|
|
50
|
+
input.appId,
|
|
51
|
+
].join("\u0000");
|
|
52
|
+
}
|
|
53
|
+
function httpOrigin(value) {
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
return { kind: "absent" };
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(value);
|
|
58
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
59
|
+
return {
|
|
60
|
+
kind: "invalid",
|
|
61
|
+
cause: new Error("Application origin must use HTTP or HTTPS."),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { kind: "valid", origin: url.origin };
|
|
65
|
+
}
|
|
66
|
+
catch (cause) {
|
|
67
|
+
return { kind: "invalid", cause };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function trustedWorkspaceRequestOrigin(event) {
|
|
71
|
+
const configured = httpOrigin(getAppProductionUrl());
|
|
72
|
+
const runtime = httpOrigin(resolveAppRuntimeUrl());
|
|
73
|
+
const request = httpOrigin(getForwardedRequestOrigin(event));
|
|
74
|
+
const allowedOrigins = new Set([configured, runtime]
|
|
75
|
+
.filter((result) => result.kind === "valid")
|
|
76
|
+
.map((result) => result.origin));
|
|
77
|
+
if (request.kind === "valid" && allowedOrigins.has(request.origin)) {
|
|
78
|
+
return request.origin;
|
|
79
|
+
}
|
|
80
|
+
if (configured.kind === "valid")
|
|
81
|
+
return configured.origin;
|
|
82
|
+
if (runtime.kind === "valid")
|
|
83
|
+
return runtime.origin;
|
|
84
|
+
if (configured.kind === "invalid" || runtime.kind === "invalid") {
|
|
85
|
+
throw new Error("Configured application origin is invalid.");
|
|
86
|
+
}
|
|
87
|
+
throw new Error("No trusted application origin is configured for app chat.");
|
|
47
88
|
}
|
|
48
89
|
function errorMessage(error) {
|
|
49
90
|
return error instanceof Error
|
|
@@ -75,13 +116,12 @@ export function embedSessionTokenExpiry(token) {
|
|
|
75
116
|
return exp * 1000;
|
|
76
117
|
}
|
|
77
118
|
async function mintEmbedStartUrl(appId) {
|
|
78
|
-
const input = { app: appId, path: "/", chrome: "minimal" };
|
|
79
119
|
try {
|
|
80
|
-
return
|
|
120
|
+
return await createWorkspaceSsoEmbedSessionForWorkspaceAppChat(appId);
|
|
81
121
|
}
|
|
82
122
|
catch (ssoError) {
|
|
83
123
|
try {
|
|
84
|
-
return
|
|
124
|
+
return await createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat(appId);
|
|
85
125
|
}
|
|
86
126
|
catch (grantError) {
|
|
87
127
|
throw new Error(`Could not create an app session for "${appId}". ` +
|
|
@@ -90,13 +130,12 @@ async function mintEmbedStartUrl(appId) {
|
|
|
90
130
|
}
|
|
91
131
|
}
|
|
92
132
|
}
|
|
93
|
-
export async function exchangeEmbedStartUrl(startUrl, fetchImpl = fetch) {
|
|
133
|
+
export async function exchangeEmbedStartUrl(startUrl, expectedAppBaseUrl, fetchImpl = fetch) {
|
|
94
134
|
const start = new URL(startUrl);
|
|
95
135
|
const startIndex = start.pathname.lastIndexOf(EMBED_START_PATH);
|
|
96
136
|
if (startIndex < 0) {
|
|
97
137
|
throw new Error("App session start URL is not an embed start route.");
|
|
98
138
|
}
|
|
99
|
-
const appBaseUrl = `${start.origin}${start.pathname.slice(0, startIndex)}`;
|
|
100
139
|
const response = await fetchImpl(start.toString(), { redirect: "manual" });
|
|
101
140
|
await response.body?.cancel().catch(() => undefined);
|
|
102
141
|
const location = response.headers.get("location");
|
|
@@ -104,17 +143,36 @@ export async function exchangeEmbedStartUrl(startUrl, fetchImpl = fetch) {
|
|
|
104
143
|
throw new Error(`App session exchange failed with status ${response.status}.`);
|
|
105
144
|
}
|
|
106
145
|
const resolved = new URL(location, start);
|
|
146
|
+
if (resolved.origin !== start.origin) {
|
|
147
|
+
throw new Error("App session exchange redirected to an untrusted origin.");
|
|
148
|
+
}
|
|
149
|
+
const expectedApp = new URL(expectedAppBaseUrl);
|
|
150
|
+
if (expectedApp.origin !== start.origin ||
|
|
151
|
+
!isWithinAppMount(expectedApp.pathname, resolved.pathname)) {
|
|
152
|
+
throw new Error("App session exchange redirected outside its app mount.");
|
|
153
|
+
}
|
|
107
154
|
const token = resolved.searchParams.get(EMBED_TOKEN_QUERY_PARAM);
|
|
108
155
|
if (!token) {
|
|
109
156
|
throw new Error("App session exchange returned no embed token.");
|
|
110
157
|
}
|
|
111
158
|
return {
|
|
112
|
-
appBaseUrl
|
|
159
|
+
appBaseUrl: `${expectedApp.origin}${expectedApp.pathname.replace(/\/+$/, "")}`,
|
|
113
160
|
token,
|
|
114
161
|
embedTarget: resolved.pathname,
|
|
115
162
|
expiresAt: embedSessionTokenExpiry(token),
|
|
116
163
|
};
|
|
117
164
|
}
|
|
165
|
+
function appPathSegments(pathname) {
|
|
166
|
+
return pathname.split("/").filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
function isWithinAppMount(mountPath, targetPath) {
|
|
169
|
+
const mount = appPathSegments(mountPath);
|
|
170
|
+
const target = appPathSegments(targetPath);
|
|
171
|
+
return (mount !== null &&
|
|
172
|
+
target !== null &&
|
|
173
|
+
mount.length <= target.length &&
|
|
174
|
+
mount.every((segment, index) => target[index] === segment));
|
|
175
|
+
}
|
|
118
176
|
async function resolveWorkspaceAppChatSession(input) {
|
|
119
177
|
const key = sessionCacheKey(input);
|
|
120
178
|
const cached = sessionCache.get(key);
|
|
@@ -135,7 +193,11 @@ async function resolveWorkspaceAppChatSession(input) {
|
|
|
135
193
|
const pending = Promise.resolve(runWithRequestContext({
|
|
136
194
|
userEmail: input.userEmail,
|
|
137
195
|
...(input.orgId ? { orgId: input.orgId } : {}),
|
|
138
|
-
|
|
196
|
+
requestOrigin: input.requestOrigin,
|
|
197
|
+
}, async () => {
|
|
198
|
+
const embedSession = await mintEmbedStartUrl(input.appId);
|
|
199
|
+
return exchangeEmbedStartUrl(embedSession.startUrl, embedSession.appBaseUrl, input.fetchImpl);
|
|
200
|
+
}));
|
|
139
201
|
sessionCache.set(key, pending);
|
|
140
202
|
try {
|
|
141
203
|
return await pending;
|
|
@@ -175,7 +237,8 @@ export function createWorkspaceAppChatProxyHandler(options = {}) {
|
|
|
175
237
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
176
238
|
return async (event) => {
|
|
177
239
|
const url = event.url;
|
|
178
|
-
const
|
|
240
|
+
const rawMountedPathname = event.context?._mountedPathname;
|
|
241
|
+
const mountedPathname = typeof rawMountedPathname === "string" ? rawMountedPathname : undefined;
|
|
179
242
|
const parsed = parseWorkspaceAppChatProxyPath(mountedPathname ?? url.pathname);
|
|
180
243
|
if (!parsed) {
|
|
181
244
|
return proxyErrorResponse(404, "Workspace app chat route not found.");
|
|
@@ -192,12 +255,15 @@ export function createWorkspaceAppChatProxyHandler(options = {}) {
|
|
|
192
255
|
if (!session?.email) {
|
|
193
256
|
return proxyErrorResponse(401, "Sign in to Dispatch to use app chat.");
|
|
194
257
|
}
|
|
258
|
+
let requestOrigin;
|
|
195
259
|
let appSession;
|
|
196
260
|
try {
|
|
261
|
+
requestOrigin = trustedWorkspaceRequestOrigin(event);
|
|
197
262
|
appSession = await resolveWorkspaceAppChatSession({
|
|
198
263
|
appId: parsed.appId,
|
|
199
264
|
userEmail: session.email,
|
|
200
265
|
orgId,
|
|
266
|
+
requestOrigin,
|
|
201
267
|
fetchImpl,
|
|
202
268
|
});
|
|
203
269
|
}
|
|
@@ -237,6 +303,7 @@ export function createWorkspaceAppChatProxyHandler(options = {}) {
|
|
|
237
303
|
appId: parsed.appId,
|
|
238
304
|
userEmail: session.email,
|
|
239
305
|
orgId,
|
|
306
|
+
requestOrigin,
|
|
240
307
|
}));
|
|
241
308
|
}
|
|
242
309
|
const headers = new Headers();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-app-chat-proxy.js","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-chat-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { getOrgContext } from \"@agent-native/core/org\";\nimport { getSession, runWithRequestContext } from \"@agent-native/core/server\";\nimport {\n EMBED_START_PATH,\n EMBED_TOKEN_QUERY_PARAM,\n} from \"@agent-native/core/shared\";\n\nimport { parseWorkspaceAppChatProxyPath } from \"../../shared/workspace-app-chat.js\";\nimport {\n createGrantedDispatchMcpEmbedSession,\n createWorkspaceSsoEmbedSession,\n} from \"./mcp-gateway.js\";\n\nconst TARGET_CHAT_PATH = \"/_agent-native/agent-chat\";\n/**\n * `X-Agent-Native-Embed-Target` names the path a bearer embed token was minted\n * for. Without it the target only honours the token on requests whose own path\n * matches, which an agent-chat call never does. Duplicated from\n * `@agent-native/core/shared/embed-auth` because that constant is not part of\n * the package's public export surface.\n */\nconst EMBED_TARGET_HEADER = \"x-agent-native-embed-target\";\nconst CSRF_MARKER_HEADER = \"x-agent-native-csrf\";\n/** Re-mint this far before the token's own expiry so a long turn cannot age out mid-stream. */\nconst SESSION_REFRESH_MARGIN_MS = 5 * 60 * 1000;\n\nconst HOP_BY_HOP_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\nconst DROPPED_REQUEST_HEADERS = new Set([\n \"accept-encoding\",\n \"authorization\",\n \"content-length\",\n \"cookie\",\n \"host\",\n \"origin\",\n \"referer\",\n EMBED_TARGET_HEADER,\n]);\n\nconst DROPPED_RESPONSE_HEADERS = new Set([\n \"content-encoding\",\n \"content-length\",\n \"set-cookie\",\n \"transfer-encoding\",\n]);\n\nexport interface WorkspaceAppChatSession {\n appBaseUrl: string;\n token: string;\n embedTarget: string;\n expiresAt: number;\n}\n\nconst sessionCache = new Map<string, Promise<WorkspaceAppChatSession>>();\n\nfunction sessionCacheKey(input: {\n appId: string;\n userEmail: string;\n orgId: string | undefined;\n}): string {\n return [input.userEmail, input.orgId ?? \"\", input.appId].join(\"\\u0000\");\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error\n ? error.message\n : typeof error === \"string\" || typeof error === \"number\"\n ? String(error)\n : JSON.stringify(error);\n}\n\n/**\n * Read the embed token's own `exp` claim. Guessing a TTL here would silently\n * produce a session that the target has already rejected, so a token we cannot\n * read an expiry from is an error rather than a default.\n */\nexport function embedSessionTokenExpiry(token: string): number {\n const payload = token.split(\".\")[0];\n if (!payload) throw new Error(\"Embed session token is malformed.\");\n let claims: unknown;\n try {\n claims = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\"));\n } catch {\n throw new Error(\"Embed session token payload could not be decoded.\");\n }\n const exp = (claims as { exp?: unknown } | null)?.exp;\n if (typeof exp !== \"number\" || !Number.isFinite(exp)) {\n throw new Error(\"Embed session token has no usable expiry.\");\n }\n return exp * 1000;\n}\n\nasync function mintEmbedStartUrl(appId: string): Promise<string> {\n const input = { app: appId, path: \"/\", chrome: \"minimal\" } as const;\n try {\n return (await createWorkspaceSsoEmbedSession(input)).startUrl;\n } catch (ssoError) {\n try {\n return (await createGrantedDispatchMcpEmbedSession(input)).startUrl;\n } catch (grantError) {\n throw new Error(\n `Could not create an app session for \"${appId}\". ` +\n `Workspace sign-in: ${errorMessage(ssoError)}. ` +\n `Dispatch app grant: ${errorMessage(grantError)}.`,\n );\n }\n }\n}\n\nexport async function exchangeEmbedStartUrl(\n startUrl: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<WorkspaceAppChatSession> {\n const start = new URL(startUrl);\n const startIndex = start.pathname.lastIndexOf(EMBED_START_PATH);\n if (startIndex < 0) {\n throw new Error(\"App session start URL is not an embed start route.\");\n }\n const appBaseUrl = `${start.origin}${start.pathname.slice(0, startIndex)}`;\n\n const response = await fetchImpl(start.toString(), { redirect: \"manual\" });\n await response.body?.cancel().catch(() => undefined);\n const location = response.headers.get(\"location\");\n if (response.status < 300 || response.status >= 400 || !location) {\n throw new Error(\n `App session exchange failed with status ${response.status}.`,\n );\n }\n\n const resolved = new URL(location, start);\n const token = resolved.searchParams.get(EMBED_TOKEN_QUERY_PARAM);\n if (!token) {\n throw new Error(\"App session exchange returned no embed token.\");\n }\n return {\n appBaseUrl,\n token,\n embedTarget: resolved.pathname,\n expiresAt: embedSessionTokenExpiry(token),\n };\n}\n\nasync function resolveWorkspaceAppChatSession(input: {\n appId: string;\n userEmail: string;\n orgId: string | undefined;\n fetchImpl: typeof fetch;\n}): Promise<WorkspaceAppChatSession> {\n const key = sessionCacheKey(input);\n const cached = sessionCache.get(key);\n if (cached) {\n try {\n const session = await cached;\n if (session.expiresAt - Date.now() > SESSION_REFRESH_MARGIN_MS) {\n return session;\n }\n // coercion-ok: a mint that already failed is retried below, and that fresh\n // attempt reports its own error rather than replaying a stale one.\n } catch {\n void 0;\n }\n sessionCache.delete(key);\n }\n\n const pending = Promise.resolve(\n runWithRequestContext(\n {\n userEmail: input.userEmail,\n ...(input.orgId ? { orgId: input.orgId } : {}),\n },\n async () =>\n exchangeEmbedStartUrl(\n await mintEmbedStartUrl(input.appId),\n input.fetchImpl,\n ),\n ),\n );\n sessionCache.set(key, pending);\n try {\n return await pending;\n } catch (error) {\n sessionCache.delete(key);\n throw error;\n }\n}\n\nexport function clearWorkspaceAppChatSessions(): void {\n sessionCache.clear();\n}\n\nfunction upstreamRequestHeaders(\n request: Request,\n session: WorkspaceAppChatSession,\n): Headers {\n const headers = new Headers();\n for (const [name, value] of request.headers) {\n const lower = name.toLowerCase();\n if (HOP_BY_HOP_HEADERS.has(lower) || DROPPED_REQUEST_HEADERS.has(lower)) {\n continue;\n }\n headers.set(name, value);\n }\n headers.set(\"authorization\", `Bearer ${session.token}`);\n headers.set(EMBED_TARGET_HEADER, session.embedTarget);\n headers.set(CSRF_MARKER_HEADER, \"1\");\n return headers;\n}\n\nfunction proxyErrorResponse(status: number, message: string): Response {\n return new Response(JSON.stringify({ error: message }), {\n status,\n headers: {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n },\n });\n}\n\ninterface WorkspaceAppChatProxyEvent {\n url: URL;\n req: Request;\n}\n\nexport function createWorkspaceAppChatProxyHandler(\n options: { fetchImpl?: typeof fetch } = {},\n) {\n const fetchImpl = options.fetchImpl ?? fetch;\n return async (event: WorkspaceAppChatProxyEvent): Promise<Response> => {\n const url = event.url;\n const mountedPathname = (\n event as WorkspaceAppChatProxyEvent & {\n context?: { _mountedPathname?: string };\n }\n ).context?._mountedPathname;\n const parsed = parseWorkspaceAppChatProxyPath(\n mountedPathname ?? url.pathname,\n );\n if (!parsed) {\n return proxyErrorResponse(404, \"Workspace app chat route not found.\");\n }\n\n let session: Awaited<ReturnType<typeof getSession>>;\n let orgId: string | undefined;\n try {\n session = await getSession(event as never);\n orgId = (await getOrgContext(event as never)).orgId ?? undefined;\n } catch (error) {\n return proxyErrorResponse(\n 503,\n `Dispatch could not read your workspace session: ${errorMessage(error)}`,\n );\n }\n if (!session?.email) {\n return proxyErrorResponse(401, \"Sign in to Dispatch to use app chat.\");\n }\n\n let appSession: WorkspaceAppChatSession;\n try {\n appSession = await resolveWorkspaceAppChatSession({\n appId: parsed.appId,\n userEmail: session.email,\n orgId,\n fetchImpl,\n });\n } catch (error) {\n console.warn(\"[dispatch] workspace app chat proxy could not connect\", {\n app: parsed.appId,\n error: errorMessage(error),\n });\n return proxyErrorResponse(\n 502,\n `${parsed.appId} chat is unavailable: ${errorMessage(error)}`,\n );\n }\n\n const request = event.req;\n const target = new URL(\n `${appSession.appBaseUrl}${TARGET_CHAT_PATH}${parsed.targetSubPath}`,\n );\n target.search = url.search;\n\n const method = request.method.toUpperCase();\n const body =\n method === \"GET\" || method === \"HEAD\"\n ? undefined\n : await request.arrayBuffer();\n\n let upstream: Response;\n try {\n upstream = await fetchImpl(target.toString(), {\n method,\n headers: upstreamRequestHeaders(request, appSession),\n ...(body === undefined ? {} : { body }),\n redirect: \"manual\",\n });\n } catch (error) {\n console.warn(\"[dispatch] workspace app chat proxy request failed\", {\n app: parsed.appId,\n targetPath: target.pathname,\n error: errorMessage(error),\n });\n return proxyErrorResponse(\n 502,\n `${parsed.appId} chat did not respond: ${errorMessage(error)}`,\n );\n }\n\n if (upstream.status === 401 || upstream.status === 403) {\n sessionCache.delete(\n sessionCacheKey({\n appId: parsed.appId,\n userEmail: session.email,\n orgId,\n }),\n );\n }\n\n const headers = new Headers();\n for (const [name, value] of upstream.headers) {\n const lower = name.toLowerCase();\n if (\n HOP_BY_HOP_HEADERS.has(lower) ||\n DROPPED_RESPONSE_HEADERS.has(lower)\n ) {\n continue;\n }\n headers.set(name, value);\n }\n // `upstream.body` is the live stream: SSE run events and the turn's token\n // stream reach the browser as the app emits them, never buffered here.\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers,\n });\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"workspace-app-chat-proxy.js","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-chat-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { getOrgContext } from \"@agent-native/core/org\";\nimport {\n getAppProductionUrl,\n getForwardedRequestOrigin,\n getSession,\n resolveAppRuntimeUrl,\n runWithRequestContext,\n} from \"@agent-native/core/server\";\nimport {\n EMBED_START_PATH,\n EMBED_TOKEN_QUERY_PARAM,\n} from \"@agent-native/core/shared\";\n\nimport { parseWorkspaceAppChatProxyPath } from \"../../shared/workspace-app-chat.js\";\nimport {\n createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat,\n createWorkspaceSsoEmbedSessionForWorkspaceAppChat,\n} from \"./mcp-gateway.js\";\n\nconst TARGET_CHAT_PATH = \"/_agent-native/agent-chat\";\n/**\n * `X-Agent-Native-Embed-Target` names the path a bearer embed token was minted\n * for. Without it the target only honours the token on requests whose own path\n * matches, which an agent-chat call never does. Duplicated from\n * `@agent-native/core/shared/embed-auth` because that constant is not part of\n * the package's public export surface.\n */\nconst EMBED_TARGET_HEADER = \"x-agent-native-embed-target\";\nconst CSRF_MARKER_HEADER = \"x-agent-native-csrf\";\n/** Re-mint this far before the token's own expiry so a long turn cannot age out mid-stream. */\nconst SESSION_REFRESH_MARGIN_MS = 5 * 60 * 1000;\n\nconst HOP_BY_HOP_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\nconst DROPPED_REQUEST_HEADERS = new Set([\n \"accept-encoding\",\n \"authorization\",\n \"content-length\",\n \"cookie\",\n \"host\",\n \"origin\",\n \"referer\",\n EMBED_TARGET_HEADER,\n]);\n\nconst DROPPED_RESPONSE_HEADERS = new Set([\n \"content-encoding\",\n \"content-length\",\n \"set-cookie\",\n \"transfer-encoding\",\n]);\n\nexport interface WorkspaceAppChatSession {\n appBaseUrl: string;\n token: string;\n embedTarget: string;\n expiresAt: number;\n}\n\nconst sessionCache = new Map<string, Promise<WorkspaceAppChatSession>>();\n\ninterface WorkspaceAppChatProxyEvent {\n url: URL;\n req: Request;\n context?: Record<string, unknown>;\n}\n\nfunction sessionCacheKey(input: {\n appId: string;\n userEmail: string;\n orgId: string | undefined;\n requestOrigin: string;\n}): string {\n return [\n input.userEmail,\n input.orgId ?? \"\",\n input.requestOrigin,\n input.appId,\n ].join(\"\\u0000\");\n}\n\ntype HttpOriginResult =\n | { kind: \"absent\" }\n | { kind: \"invalid\"; cause: unknown }\n | { kind: \"valid\"; origin: string };\n\nfunction httpOrigin(value: string | undefined): HttpOriginResult {\n if (value === undefined) return { kind: \"absent\" };\n try {\n const url = new URL(value);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n return {\n kind: \"invalid\",\n cause: new Error(\"Application origin must use HTTP or HTTPS.\"),\n };\n }\n return { kind: \"valid\", origin: url.origin };\n } catch (cause) {\n return { kind: \"invalid\", cause };\n }\n}\n\nfunction trustedWorkspaceRequestOrigin(\n event: WorkspaceAppChatProxyEvent,\n): string {\n const configured = httpOrigin(getAppProductionUrl());\n const runtime = httpOrigin(resolveAppRuntimeUrl());\n const request = httpOrigin(\n getForwardedRequestOrigin(\n event as unknown as Parameters<typeof getForwardedRequestOrigin>[0],\n ),\n );\n const allowedOrigins = new Set(\n [configured, runtime]\n .filter(\n (result): result is Extract<HttpOriginResult, { kind: \"valid\" }> =>\n result.kind === \"valid\",\n )\n .map((result) => result.origin),\n );\n\n if (request.kind === \"valid\" && allowedOrigins.has(request.origin)) {\n return request.origin;\n }\n if (configured.kind === \"valid\") return configured.origin;\n if (runtime.kind === \"valid\") return runtime.origin;\n if (configured.kind === \"invalid\" || runtime.kind === \"invalid\") {\n throw new Error(\"Configured application origin is invalid.\");\n }\n throw new Error(\"No trusted application origin is configured for app chat.\");\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error\n ? error.message\n : typeof error === \"string\" || typeof error === \"number\"\n ? String(error)\n : JSON.stringify(error);\n}\n\n/**\n * Read the embed token's own `exp` claim. Guessing a TTL here would silently\n * produce a session that the target has already rejected, so a token we cannot\n * read an expiry from is an error rather than a default.\n */\nexport function embedSessionTokenExpiry(token: string): number {\n const payload = token.split(\".\")[0];\n if (!payload) throw new Error(\"Embed session token is malformed.\");\n let claims: unknown;\n try {\n claims = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\"));\n } catch {\n throw new Error(\"Embed session token payload could not be decoded.\");\n }\n const exp = (claims as { exp?: unknown } | null)?.exp;\n if (typeof exp !== \"number\" || !Number.isFinite(exp)) {\n throw new Error(\"Embed session token has no usable expiry.\");\n }\n return exp * 1000;\n}\n\nasync function mintEmbedStartUrl(\n appId: string,\n): Promise<{ startUrl: string; appBaseUrl: string }> {\n try {\n return await createWorkspaceSsoEmbedSessionForWorkspaceAppChat(appId);\n } catch (ssoError) {\n try {\n return await createGrantedDispatchMcpEmbedSessionForWorkspaceAppChat(\n appId,\n );\n } catch (grantError) {\n throw new Error(\n `Could not create an app session for \"${appId}\". ` +\n `Workspace sign-in: ${errorMessage(ssoError)}. ` +\n `Dispatch app grant: ${errorMessage(grantError)}.`,\n );\n }\n }\n}\n\nexport async function exchangeEmbedStartUrl(\n startUrl: string,\n expectedAppBaseUrl: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<WorkspaceAppChatSession> {\n const start = new URL(startUrl);\n const startIndex = start.pathname.lastIndexOf(EMBED_START_PATH);\n if (startIndex < 0) {\n throw new Error(\"App session start URL is not an embed start route.\");\n }\n const response = await fetchImpl(start.toString(), { redirect: \"manual\" });\n await response.body?.cancel().catch(() => undefined);\n const location = response.headers.get(\"location\");\n if (response.status < 300 || response.status >= 400 || !location) {\n throw new Error(\n `App session exchange failed with status ${response.status}.`,\n );\n }\n\n const resolved = new URL(location, start);\n if (resolved.origin !== start.origin) {\n throw new Error(\"App session exchange redirected to an untrusted origin.\");\n }\n const expectedApp = new URL(expectedAppBaseUrl);\n if (\n expectedApp.origin !== start.origin ||\n !isWithinAppMount(expectedApp.pathname, resolved.pathname)\n ) {\n throw new Error(\"App session exchange redirected outside its app mount.\");\n }\n const token = resolved.searchParams.get(EMBED_TOKEN_QUERY_PARAM);\n if (!token) {\n throw new Error(\"App session exchange returned no embed token.\");\n }\n return {\n appBaseUrl: `${expectedApp.origin}${expectedApp.pathname.replace(/\\/+$/, \"\")}`,\n token,\n embedTarget: resolved.pathname,\n expiresAt: embedSessionTokenExpiry(token),\n };\n}\n\nfunction appPathSegments(pathname: string): string[] | null {\n return pathname.split(\"/\").filter(Boolean);\n}\n\nfunction isWithinAppMount(mountPath: string, targetPath: string): boolean {\n const mount = appPathSegments(mountPath);\n const target = appPathSegments(targetPath);\n return (\n mount !== null &&\n target !== null &&\n mount.length <= target.length &&\n mount.every((segment, index) => target[index] === segment)\n );\n}\n\nasync function resolveWorkspaceAppChatSession(input: {\n appId: string;\n userEmail: string;\n orgId: string | undefined;\n requestOrigin: string;\n fetchImpl: typeof fetch;\n}): Promise<WorkspaceAppChatSession> {\n const key = sessionCacheKey(input);\n const cached = sessionCache.get(key);\n if (cached) {\n try {\n const session = await cached;\n if (session.expiresAt - Date.now() > SESSION_REFRESH_MARGIN_MS) {\n return session;\n }\n // coercion-ok: a mint that already failed is retried below, and that fresh\n // attempt reports its own error rather than replaying a stale one.\n } catch {\n void 0;\n }\n sessionCache.delete(key);\n }\n\n const pending = Promise.resolve(\n runWithRequestContext(\n {\n userEmail: input.userEmail,\n ...(input.orgId ? { orgId: input.orgId } : {}),\n requestOrigin: input.requestOrigin,\n },\n async () => {\n const embedSession = await mintEmbedStartUrl(input.appId);\n return exchangeEmbedStartUrl(\n embedSession.startUrl,\n embedSession.appBaseUrl,\n input.fetchImpl,\n );\n },\n ),\n );\n sessionCache.set(key, pending);\n try {\n return await pending;\n } catch (error) {\n sessionCache.delete(key);\n throw error;\n }\n}\n\nexport function clearWorkspaceAppChatSessions(): void {\n sessionCache.clear();\n}\n\nfunction upstreamRequestHeaders(\n request: Request,\n session: WorkspaceAppChatSession,\n): Headers {\n const headers = new Headers();\n for (const [name, value] of request.headers) {\n const lower = name.toLowerCase();\n if (HOP_BY_HOP_HEADERS.has(lower) || DROPPED_REQUEST_HEADERS.has(lower)) {\n continue;\n }\n headers.set(name, value);\n }\n headers.set(\"authorization\", `Bearer ${session.token}`);\n headers.set(EMBED_TARGET_HEADER, session.embedTarget);\n headers.set(CSRF_MARKER_HEADER, \"1\");\n return headers;\n}\n\nfunction proxyErrorResponse(status: number, message: string): Response {\n return new Response(JSON.stringify({ error: message }), {\n status,\n headers: {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n },\n });\n}\n\nexport function createWorkspaceAppChatProxyHandler(\n options: { fetchImpl?: typeof fetch } = {},\n) {\n const fetchImpl = options.fetchImpl ?? fetch;\n return async (event: WorkspaceAppChatProxyEvent): Promise<Response> => {\n const url = event.url;\n const rawMountedPathname = event.context?._mountedPathname;\n const mountedPathname =\n typeof rawMountedPathname === \"string\" ? rawMountedPathname : undefined;\n const parsed = parseWorkspaceAppChatProxyPath(\n mountedPathname ?? url.pathname,\n );\n if (!parsed) {\n return proxyErrorResponse(404, \"Workspace app chat route not found.\");\n }\n\n let session: Awaited<ReturnType<typeof getSession>>;\n let orgId: string | undefined;\n try {\n session = await getSession(event as never);\n orgId = (await getOrgContext(event as never)).orgId ?? undefined;\n } catch (error) {\n return proxyErrorResponse(\n 503,\n `Dispatch could not read your workspace session: ${errorMessage(error)}`,\n );\n }\n if (!session?.email) {\n return proxyErrorResponse(401, \"Sign in to Dispatch to use app chat.\");\n }\n\n let requestOrigin: string;\n let appSession: WorkspaceAppChatSession;\n try {\n requestOrigin = trustedWorkspaceRequestOrigin(event);\n appSession = await resolveWorkspaceAppChatSession({\n appId: parsed.appId,\n userEmail: session.email,\n orgId,\n requestOrigin,\n fetchImpl,\n });\n } catch (error) {\n console.warn(\"[dispatch] workspace app chat proxy could not connect\", {\n app: parsed.appId,\n error: errorMessage(error),\n });\n return proxyErrorResponse(\n 502,\n `${parsed.appId} chat is unavailable: ${errorMessage(error)}`,\n );\n }\n\n const request = event.req;\n const target = new URL(\n `${appSession.appBaseUrl}${TARGET_CHAT_PATH}${parsed.targetSubPath}`,\n );\n target.search = url.search;\n\n const method = request.method.toUpperCase();\n const body =\n method === \"GET\" || method === \"HEAD\"\n ? undefined\n : await request.arrayBuffer();\n\n let upstream: Response;\n try {\n upstream = await fetchImpl(target.toString(), {\n method,\n headers: upstreamRequestHeaders(request, appSession),\n ...(body === undefined ? {} : { body }),\n redirect: \"manual\",\n });\n } catch (error) {\n console.warn(\"[dispatch] workspace app chat proxy request failed\", {\n app: parsed.appId,\n targetPath: target.pathname,\n error: errorMessage(error),\n });\n return proxyErrorResponse(\n 502,\n `${parsed.appId} chat did not respond: ${errorMessage(error)}`,\n );\n }\n\n if (upstream.status === 401 || upstream.status === 403) {\n sessionCache.delete(\n sessionCacheKey({\n appId: parsed.appId,\n userEmail: session.email,\n orgId,\n requestOrigin,\n }),\n );\n }\n\n const headers = new Headers();\n for (const [name, value] of upstream.headers) {\n const lower = name.toLowerCase();\n if (\n HOP_BY_HOP_HEADERS.has(lower) ||\n DROPPED_RESPONSE_HEADERS.has(lower)\n ) {\n continue;\n }\n headers.set(name, value);\n }\n // `upstream.body` is the live stream: SSE run events and the turn's token\n // stream reach the browser as the app emits them, never buffered here.\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers,\n });\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,EAC/B,0BAA0B,GAC3B,MAAM,qCAAqC,CAAC;AAE7C,MAAM,kBAAkB,GAAG;IACzB,aAAa;IACb,qBAAqB;IACrB,+BAA+B;IAC/B,uBAAuB;IACvB,SAAS;IACT,UAAU;IACV,0BAA0B;IAC1B,2BAA2B;IAC3B,2BAA2B;IAC3B,cAAc;IACd,mBAAmB;IACnB,iBAAiB;IACjB,wBAAwB;IACxB,oBAAoB;IACpB,sBAAsB;IACtB,qBAAqB;IACrB,mBAAmB;IACnB,oBAAoB;IACpB,aAAa;IACb,8BAA8B;IAC9B,6BAA6B;IAC7B,oCAAoC;IACpC,sBAAsB;IACtB,mBAAmB;IACnB,sBAAsB;IACtB,sBAAsB;IACtB,UAAU;CACX,CAAC;AAEF,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,WAAW,EAAE,IAAI;IACjB,qBAAqB,EAAE,IAAI;IAC3B,gBAAgB,EAAE,kBAAkB;IACpC,GAAG,EAAE;QACH,gBAAgB,EAAE;YAChB,oCAAoC;YACpC,8BAA8B;YAC9B,6BAA6B;SAC9B;KACF;IACD,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC,KAAK,CAAC;IACnB,CAAC;IACD,OAAO,EAAE,eAAe;IACxB,eAAe,EAAE,2BAA2B;IAC5C,sBAAsB,EAAE;QACtB,0BAA0B;QAC1B,+BAA+B;KAChC;IACD,aAAa,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;IAC1C,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4EAoC4D;CAC3E,CAAC,CAAC","sourcesContent":["import { getOrgContext } from \"@agent-native/core/org\";\nimport { createAgentChatPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\nimport {\n workspaceAppActionRouteAuth,\n WORKSPACE_APP_CLAIM_ACTION_PATH,\n WORKSPACE_APPS_ACTION_PATH,\n} from \"../lib/workspace-app-action-auth.js\";\n\nconst INITIAL_TOOL_NAMES = [\n \"view-screen\",\n \"list-workspace-apps\",\n \"update-workspace-app-metadata\",\n \"list-connected-agents\",\n \"ask_app\",\n \"open_app\",\n \"list-workspace-resources\",\n \"create-workspace-resource\",\n \"update-workspace-resource\",\n \"import-agent\",\n \"import-agent-pack\",\n \"list-agent-pack\",\n \"connect-external-agent\",\n \"list-vault-secrets\",\n \"request-vault-secret\",\n \"create-vault-secret\",\n \"list-destinations\",\n \"upsert-destination\",\n \"list-dreams\",\n \"start-workspace-app-creation\",\n \"list-dispatch-usage-metrics\",\n \"list-available-workspace-templates\",\n \"provider-api-catalog\",\n \"provider-api-docs\",\n \"provider-api-request\",\n \"query-staged-dataset\",\n \"navigate\",\n];\n\nexport default createAgentChatPlugin({\n appId: \"dispatch\",\n connectApps: true,\n durableBackgroundRuns: true,\n initialToolNames: INITIAL_TOOL_NAMES,\n mcp: {\n connectorCatalog: [\n \"resolve-integration-source-context\",\n \"start-workspace-app-creation\",\n \"list-dispatch-usage-metrics\",\n ],\n },\n resolveOrgId: async (event) => {\n const ctx = await getOrgContext(event);\n return ctx.orgId;\n },\n actions: dispatchActions,\n actionRouteAuth: workspaceAppActionRouteAuth,\n actionRoutePublicPaths: [\n WORKSPACE_APPS_ACTION_PATH,\n WORKSPACE_APP_CLAIM_ACTION_PATH,\n ],\n codeExecution: { production: \"sandboxed\" },\n systemPrompt: `You are the central dispatch for this workspace.\n\nDefault posture:\n- Treat Slack and Telegram as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A when another app owns the job.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Prefer replying in the current external thread unless the user explicitly asks you to send to a saved destination.\n\nUse the standard workspace primitives:\n- Read and update resources like AGENTS.md, LEARNINGS.md, jobs/*.md, agents/*.md, and remote-agents/*.json when appropriate.\n- Use recurring jobs for scheduled behavior.\n- Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.\n- For a Claude-style Markdown or JSON agent setup, use import-agent so the\n profile is normalized into agents/<slug>.md. Never import credentials, hooks,\n shell commands, or local environment settings.\n- For a folder of agent instructions, references, or skills, use\n import-agent-pack. It preserves the text files under agents/<slug>/ and\n creates the normalized profile alongside them. Use list-agent-pack to inspect\n the pack before editing or handing it off to an app.\n- For an existing HTTP/A2A endpoint, use connect-external-agent and let the\n normal A2A/MCP connection flow handle authentication.\n- 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.\n- 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.\n- 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.\n- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.\n- When a user asks to rename an existing workspace app or change its Dispatch title/description, call update-workspace-app-metadata with the existing appId from list-workspace-apps. This is a metadata-only edit — never call start-workspace-app-creation, which creates a new app and Builder branch.\n- When an explicit app-creation request already includes a source brief or a concrete repeatable workflow, call start-workspace-app-creation without asking non-blocking product or UX questions. Choose recommended defaults, let the Builder handoff record assumptions, and ask only for authorization, credentials, a destructive action, an ambiguous target workspace, or a genuinely missing workflow.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- For broad provider searches, joins, classification, corpus counts, or absence claims, fetch every relevant page or an explicitly bounded cohort, stage/save large responses with stageAs/saveToFile/fetchAllPages, and reduce them with query-staged-dataset or run-code. Report source, filters, row counts, pagination, truncation, failed pages, and uncovered gaps.\n- Use list-dispatch-usage-metrics only for this workspace's own members' agent/LLM usage and spend, or aggregate adoption of an accessible workspace app. For agent/LLM usage or spend, call it with scope=workspace and the requested sinceDays; it reports workspace-member usage only. For app adoption, call it with scope=app and appId; app creators can see aggregate daily/weekly active users and tracked actions for their own app, while organization owners/admins can inspect any accessible app. App scope omits individual users and prompt previews. External customer, account, or company usage, credits, activity, or product data belongs to the relevant app: use its identity in <relevant-apps> when clear, or call describe-workspace-apps with a query when ownership is unclear. Do not use this action for external customer/account/company data.\n\nWhen a user asks for something like a digest, reminder, routing rule, or saved behavior:\n- First decide whether it should be a resource, a recurring job, a destination, or a delegated task.\n- Keep responses concise and operational.\n- Avoid inventing integrations or destinations that are not configured yet.`,\n});\n"]}
|
|
1
|
+
{"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,EAC/B,0BAA0B,GAC3B,MAAM,qCAAqC,CAAC;AAE7C,MAAM,kBAAkB,GAAG;IACzB,aAAa;IACb,qBAAqB;IACrB,+BAA+B;IAC/B,uBAAuB;IACvB,SAAS;IACT,UAAU;IACV,0BAA0B;IAC1B,2BAA2B;IAC3B,2BAA2B;IAC3B,cAAc;IACd,mBAAmB;IACnB,iBAAiB;IACjB,wBAAwB;IACxB,oBAAoB;IACpB,sBAAsB;IACtB,qBAAqB;IACrB,mBAAmB;IACnB,oBAAoB;IACpB,aAAa;IACb,8BAA8B;IAC9B,6BAA6B;IAC7B,oCAAoC;IACpC,sBAAsB;IACtB,mBAAmB;IACnB,sBAAsB;IACtB,sBAAsB;IACtB,UAAU;CACX,CAAC;AAEF,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,WAAW,EAAE,IAAI;IACjB,qBAAqB,EAAE,IAAI;IAC3B,gBAAgB,EAAE,kBAAkB;IACpC,GAAG,EAAE;QACH,eAAe,EAAE,IAAI;QACrB,gBAAgB,EAAE;YAChB,oCAAoC;YACpC,8BAA8B;YAC9B,6BAA6B;SAC9B;KACF;IACD,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC,KAAK,CAAC;IACnB,CAAC;IACD,OAAO,EAAE,eAAe;IACxB,eAAe,EAAE,2BAA2B;IAC5C,sBAAsB,EAAE;QACtB,0BAA0B;QAC1B,+BAA+B;KAChC;IACD,aAAa,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;IAC1C,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4EAoC4D;CAC3E,CAAC,CAAC","sourcesContent":["import { getOrgContext } from \"@agent-native/core/org\";\nimport { createAgentChatPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\nimport {\n workspaceAppActionRouteAuth,\n WORKSPACE_APP_CLAIM_ACTION_PATH,\n WORKSPACE_APPS_ACTION_PATH,\n} from \"../lib/workspace-app-action-auth.js\";\n\nconst INITIAL_TOOL_NAMES = [\n \"view-screen\",\n \"list-workspace-apps\",\n \"update-workspace-app-metadata\",\n \"list-connected-agents\",\n \"ask_app\",\n \"open_app\",\n \"list-workspace-resources\",\n \"create-workspace-resource\",\n \"update-workspace-resource\",\n \"import-agent\",\n \"import-agent-pack\",\n \"list-agent-pack\",\n \"connect-external-agent\",\n \"list-vault-secrets\",\n \"request-vault-secret\",\n \"create-vault-secret\",\n \"list-destinations\",\n \"upsert-destination\",\n \"list-dreams\",\n \"start-workspace-app-creation\",\n \"list-dispatch-usage-metrics\",\n \"list-available-workspace-templates\",\n \"provider-api-catalog\",\n \"provider-api-docs\",\n \"provider-api-request\",\n \"query-staged-dataset\",\n \"navigate\",\n];\n\nexport default createAgentChatPlugin({\n appId: \"dispatch\",\n connectApps: true,\n durableBackgroundRuns: true,\n initialToolNames: INITIAL_TOOL_NAMES,\n mcp: {\n appApprovalPage: true,\n connectorCatalog: [\n \"resolve-integration-source-context\",\n \"start-workspace-app-creation\",\n \"list-dispatch-usage-metrics\",\n ],\n },\n resolveOrgId: async (event) => {\n const ctx = await getOrgContext(event);\n return ctx.orgId;\n },\n actions: dispatchActions,\n actionRouteAuth: workspaceAppActionRouteAuth,\n actionRoutePublicPaths: [\n WORKSPACE_APPS_ACTION_PATH,\n WORKSPACE_APP_CLAIM_ACTION_PATH,\n ],\n codeExecution: { production: \"sandboxed\" },\n systemPrompt: `You are the central dispatch for this workspace.\n\nDefault posture:\n- Treat Slack and Telegram as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A when another app owns the job.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Prefer replying in the current external thread unless the user explicitly asks you to send to a saved destination.\n\nUse the standard workspace primitives:\n- Read and update resources like AGENTS.md, LEARNINGS.md, jobs/*.md, agents/*.md, and remote-agents/*.json when appropriate.\n- Use recurring jobs for scheduled behavior.\n- Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.\n- For a Claude-style Markdown or JSON agent setup, use import-agent so the\n profile is normalized into agents/<slug>.md. Never import credentials, hooks,\n shell commands, or local environment settings.\n- For a folder of agent instructions, references, or skills, use\n import-agent-pack. It preserves the text files under agents/<slug>/ and\n creates the normalized profile alongside them. Use list-agent-pack to inspect\n the pack before editing or handing it off to an app.\n- For an existing HTTP/A2A endpoint, use connect-external-agent and let the\n normal A2A/MCP connection flow handle authentication.\n- 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.\n- 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.\n- 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.\n- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.\n- When a user asks to rename an existing workspace app or change its Dispatch title/description, call update-workspace-app-metadata with the existing appId from list-workspace-apps. This is a metadata-only edit — never call start-workspace-app-creation, which creates a new app and Builder branch.\n- When an explicit app-creation request already includes a source brief or a concrete repeatable workflow, call start-workspace-app-creation without asking non-blocking product or UX questions. Choose recommended defaults, let the Builder handoff record assumptions, and ask only for authorization, credentials, a destructive action, an ambiguous target workspace, or a genuinely missing workflow.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- For broad provider searches, joins, classification, corpus counts, or absence claims, fetch every relevant page or an explicitly bounded cohort, stage/save large responses with stageAs/saveToFile/fetchAllPages, and reduce them with query-staged-dataset or run-code. Report source, filters, row counts, pagination, truncation, failed pages, and uncovered gaps.\n- Use list-dispatch-usage-metrics only for this workspace's own members' agent/LLM usage and spend, or aggregate adoption of an accessible workspace app. For agent/LLM usage or spend, call it with scope=workspace and the requested sinceDays; it reports workspace-member usage only. For app adoption, call it with scope=app and appId; app creators can see aggregate daily/weekly active users and tracked actions for their own app, while organization owners/admins can inspect any accessible app. App scope omits individual users and prompt previews. External customer, account, or company usage, credits, activity, or product data belongs to the relevant app: use its identity in <relevant-apps> when clear, or call describe-workspace-apps with a query when ownership is unclear. Do not use this action for external customer/account/company data.\n\nWhen a user asks for something like a digest, reminder, routing rule, or saved behavior:\n- First decide whether it should be a resource, a recurring job, a destination, or a delegated task.\n- Keep responses concise and operational.\n- Avoid inventing integrations or destinations that are not configured yet.`,\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.40.41
|
|
3
|
+
"version": "0.40.41",
|
|
4
4
|
"description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"tailwind-merge": "^3.5.0",
|
|
91
91
|
"vaul": "^1.1.2",
|
|
92
92
|
"zod": "^4.3.6",
|
|
93
|
-
"@agent-native/toolkit": "0.221.0
|
|
93
|
+
"@agent-native/toolkit": "0.221.0"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
96
|
"@react-router/dev": "^8.1.0",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
105
105
|
"vite": "8.1.0",
|
|
106
106
|
"vitest": "^4.1.5",
|
|
107
|
-
"@agent-native/core": "0.221.0
|
|
107
|
+
"@agent-native/core": "0.221.0"
|
|
108
108
|
},
|
|
109
109
|
"peerDependencies": {
|
|
110
110
|
"@agent-native/core": ">=0.220.1-0 || >=0.221.0-0",
|
|
@@ -73,4 +73,41 @@ describe("ActionQueryError", () => {
|
|
|
73
73
|
);
|
|
74
74
|
expect(container.textContent).not.toContain("secret database detail");
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it("shows the request id so a screenshot leads to the server's record", () => {
|
|
78
|
+
act(() => {
|
|
79
|
+
root.render(
|
|
80
|
+
<ActionQueryError
|
|
81
|
+
error={Object.assign(new Error("secret database detail"), {
|
|
82
|
+
status: 503,
|
|
83
|
+
errorCode: "session_unavailable",
|
|
84
|
+
requestId: "ed4b2e72-dbf7-41d2-8a20-2fb6f43e01b7",
|
|
85
|
+
})}
|
|
86
|
+
queryName="list-workspace-apps"
|
|
87
|
+
onRetry={() => {}}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(container.textContent).toContain(
|
|
93
|
+
"list-workspace-apps · HTTP 503 · session_unavailable · ref ed4b2e72-dbf7-41d2-8a20-2fb6f43e01b7",
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("drops a request id that is not a plain identifier", () => {
|
|
98
|
+
act(() => {
|
|
99
|
+
root.render(
|
|
100
|
+
<ActionQueryError
|
|
101
|
+
error={Object.assign(new Error("boom"), {
|
|
102
|
+
status: 500,
|
|
103
|
+
requestId: "<script>alert(1)</script>",
|
|
104
|
+
})}
|
|
105
|
+
onRetry={() => {}}
|
|
106
|
+
/>,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(container.textContent).toContain("HTTP 500");
|
|
111
|
+
expect(container.textContent).not.toContain("script");
|
|
112
|
+
});
|
|
76
113
|
});
|
|
@@ -37,6 +37,17 @@ function actionQueryDiagnostic(
|
|
|
37
37
|
if (typeof code === "string" && /^[a-zA-Z0-9_.-]{1,80}$/.test(code)) {
|
|
38
38
|
parts.push(code);
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
// The id the server stamped on this response. It is on the server's log
|
|
42
|
+
// and error report for the same failure, so a screenshot is enough to find
|
|
43
|
+
// what went wrong.
|
|
44
|
+
const requestId = details.requestId;
|
|
45
|
+
if (
|
|
46
|
+
typeof requestId === "string" &&
|
|
47
|
+
/^[a-zA-Z0-9_.-]{1,80}$/.test(requestId)
|
|
48
|
+
) {
|
|
49
|
+
parts.push(`ref ${requestId}`);
|
|
50
|
+
}
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
return parts.length > 0 ? parts.join(" · ") : null;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
CredentialStoreUnavailableError,
|
|
9
9
|
runWithRequestContext,
|
|
10
10
|
} from "@agent-native/core/server";
|
|
11
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
archiveWorkspaceApp,
|
|
@@ -1226,6 +1226,106 @@ describe("listWorkspaceApps", () => {
|
|
|
1226
1226
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1227
1227
|
});
|
|
1228
1228
|
|
|
1229
|
+
describe("gateway failure trail", () => {
|
|
1230
|
+
let warn: ReturnType<typeof vi.spyOn>;
|
|
1231
|
+
|
|
1232
|
+
beforeEach(() => {
|
|
1233
|
+
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1234
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
1235
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
1236
|
+
stubManifest([{ id: "dispatch", name: "Dispatch", path: "/dispatch" }]);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
afterEach(() => {
|
|
1240
|
+
warn.mockRestore();
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
function gatewayFailureLog(): Record<string, unknown> | undefined {
|
|
1244
|
+
return warn.mock.calls.find(
|
|
1245
|
+
(call) => call[0] === "[dispatch] workspace apps gateway read failed:",
|
|
1246
|
+
)?.[1] as Record<string, unknown> | undefined;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
it("names a gateway that did not answer in time, apart from one that was unreachable", async () => {
|
|
1250
|
+
vi.stubGlobal(
|
|
1251
|
+
"fetch",
|
|
1252
|
+
vi.fn(
|
|
1253
|
+
(_url: unknown, init?: RequestInit) =>
|
|
1254
|
+
new Promise((_resolve, reject) => {
|
|
1255
|
+
init?.signal?.addEventListener("abort", () =>
|
|
1256
|
+
reject(new DOMException("aborted", "AbortError")),
|
|
1257
|
+
);
|
|
1258
|
+
}),
|
|
1259
|
+
),
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
await expect(
|
|
1263
|
+
runWithRequestContext(
|
|
1264
|
+
{ userEmail: "dev@example.test", requestId: "req-dispatch-1" },
|
|
1265
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
1266
|
+
),
|
|
1267
|
+
).rejects.toMatchObject({
|
|
1268
|
+
statusCode: 503,
|
|
1269
|
+
errorCode: "workspace_gateway_timeout",
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
expect(gatewayFailureLog()).toMatchObject({
|
|
1273
|
+
errorCode: "workspace_gateway_timeout",
|
|
1274
|
+
status: 503,
|
|
1275
|
+
requestId: "req-dispatch-1",
|
|
1276
|
+
gateway: "https://agent-workspace.builder.io",
|
|
1277
|
+
cause: expect.stringContaining("aborted"),
|
|
1278
|
+
});
|
|
1279
|
+
}, 10_000);
|
|
1280
|
+
|
|
1281
|
+
it("keeps the cause of a transport failure in the log, not on the screen", async () => {
|
|
1282
|
+
vi.stubGlobal(
|
|
1283
|
+
"fetch",
|
|
1284
|
+
vi.fn(async () => {
|
|
1285
|
+
throw new TypeError("fetch failed", {
|
|
1286
|
+
cause: Object.assign(new Error("getaddrinfo ENOTFOUND"), {
|
|
1287
|
+
code: "ENOTFOUND",
|
|
1288
|
+
}),
|
|
1289
|
+
});
|
|
1290
|
+
}),
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
await expect(
|
|
1294
|
+
runWithRequestContext({ userEmail: "dev@example.test" }, () =>
|
|
1295
|
+
listWorkspaceApps({ includeAgentCards: false }),
|
|
1296
|
+
),
|
|
1297
|
+
).rejects.toMatchObject({ errorCode: "workspace_gateway_unavailable" });
|
|
1298
|
+
|
|
1299
|
+
expect(gatewayFailureLog()?.cause).toContain("ENOTFOUND");
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it("logs the workspace's own request id when it answered with an error", async () => {
|
|
1303
|
+
vi.stubGlobal(
|
|
1304
|
+
"fetch",
|
|
1305
|
+
vi.fn(
|
|
1306
|
+
async () =>
|
|
1307
|
+
new Response("down", {
|
|
1308
|
+
status: 503,
|
|
1309
|
+
headers: { "x-agent-native-request-id": "req-workspace-9" },
|
|
1310
|
+
}),
|
|
1311
|
+
),
|
|
1312
|
+
);
|
|
1313
|
+
|
|
1314
|
+
await expect(
|
|
1315
|
+
runWithRequestContext({ userEmail: "dev@example.test" }, () =>
|
|
1316
|
+
listWorkspaceApps({ includeAgentCards: false }),
|
|
1317
|
+
),
|
|
1318
|
+
).rejects.toMatchObject({ errorCode: "workspace_gateway_http_503" });
|
|
1319
|
+
|
|
1320
|
+
expect(gatewayFailureLog()).toMatchObject({
|
|
1321
|
+
errorCode: "workspace_gateway_http_503",
|
|
1322
|
+
upstreamStatus: 503,
|
|
1323
|
+
upstreamRequestId: "req-workspace-9",
|
|
1324
|
+
});
|
|
1325
|
+
expect(gatewayFailureLog()).not.toHaveProperty("cause");
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1229
1329
|
it("surfaces malformed hosted registry responses instead of using manifest apps", async () => {
|
|
1230
1330
|
const fetchMock = vi.fn(
|
|
1231
1331
|
async () =>
|
|
@@ -193,6 +193,51 @@ function warnWorkspaceAppsGatewayDenial(
|
|
|
193
193
|
);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
interface WorkspaceAppsGatewayAttempt {
|
|
197
|
+
startedAt: number;
|
|
198
|
+
gateway: string;
|
|
199
|
+
upstream?: { status: number; requestId?: string };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function noteWorkspaceAppsGatewayResponse(
|
|
203
|
+
attempt: WorkspaceAppsGatewayAttempt,
|
|
204
|
+
response: Response,
|
|
205
|
+
): void {
|
|
206
|
+
const requestId = response.headers.get("x-agent-native-request-id");
|
|
207
|
+
attempt.upstream = {
|
|
208
|
+
status: response.status,
|
|
209
|
+
...(requestId ? { requestId } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// The action route returns a typed contract error without capturing it, so a
|
|
214
|
+
// refusal, a timeout and an outage would all read as one 503 with no trace.
|
|
215
|
+
// `upstreamRequestId` is the id the workspace stamped on its own answer: it
|
|
216
|
+
// opens that side's log for the same read.
|
|
217
|
+
function logWorkspaceAppsGatewayFailure(
|
|
218
|
+
failure: ActionContractError,
|
|
219
|
+
attempt: WorkspaceAppsGatewayAttempt,
|
|
220
|
+
cause: unknown,
|
|
221
|
+
): void {
|
|
222
|
+
const requestId = getRequestContext()?.requestId;
|
|
223
|
+
console.warn("[dispatch] workspace apps gateway read failed:", {
|
|
224
|
+
errorCode: failure.errorCode,
|
|
225
|
+
status: failure.statusCode,
|
|
226
|
+
...(requestId ? { requestId } : {}),
|
|
227
|
+
gateway: attempt.gateway,
|
|
228
|
+
elapsedMs: Date.now() - attempt.startedAt,
|
|
229
|
+
...(attempt.upstream
|
|
230
|
+
? {
|
|
231
|
+
upstreamStatus: attempt.upstream.status,
|
|
232
|
+
...(attempt.upstream.requestId
|
|
233
|
+
? { upstreamRequestId: attempt.upstream.requestId }
|
|
234
|
+
: {}),
|
|
235
|
+
}
|
|
236
|
+
: {}),
|
|
237
|
+
...(cause === undefined ? {} : { cause: describeErrorWithCauses(cause) }),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
196
241
|
type WorkspaceAppAudience = "internal" | "public";
|
|
197
242
|
type WorkspaceAppVisibility = "private" | "org";
|
|
198
243
|
|
|
@@ -2112,6 +2157,10 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
|
|
|
2112
2157
|
() => controller.abort(),
|
|
2113
2158
|
WORKSPACE_APPS_GATEWAY_TIMEOUT_MS,
|
|
2114
2159
|
);
|
|
2160
|
+
const attempt: WorkspaceAppsGatewayAttempt = {
|
|
2161
|
+
startedAt: Date.now(),
|
|
2162
|
+
gateway: `${baseUrl.origin}${baseUrl.pathname.replace(/\/+$/, "")}`,
|
|
2163
|
+
};
|
|
2115
2164
|
|
|
2116
2165
|
const gatewayUrl = (pathname: string): URL => {
|
|
2117
2166
|
const url = new URL(baseUrl.toString());
|
|
@@ -2169,6 +2218,7 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
|
|
|
2169
2218
|
signal: controller.signal,
|
|
2170
2219
|
},
|
|
2171
2220
|
);
|
|
2221
|
+
noteWorkspaceAppsGatewayResponse(attempt, localResponse);
|
|
2172
2222
|
if (localResponse.status === 401 || localResponse.status === 403) {
|
|
2173
2223
|
throw new WorkspaceAppsGatewayAuthorizationError(
|
|
2174
2224
|
localResponse.status,
|
|
@@ -2198,6 +2248,7 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
|
|
|
2198
2248
|
...protectedRedirect,
|
|
2199
2249
|
signal: controller.signal,
|
|
2200
2250
|
});
|
|
2251
|
+
noteWorkspaceAppsGatewayResponse(attempt, actionResponse);
|
|
2201
2252
|
if (actionResponse.status === 401 || actionResponse.status === 403) {
|
|
2202
2253
|
throw new WorkspaceAppsGatewayAuthorizationError(
|
|
2203
2254
|
actionResponse.status,
|
|
@@ -2216,14 +2267,24 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
|
|
|
2216
2267
|
}
|
|
2217
2268
|
return await readAppsResponse(actionResponse);
|
|
2218
2269
|
} catch (error) {
|
|
2219
|
-
if (error instanceof ActionContractError)
|
|
2220
|
-
|
|
2221
|
-
|
|
2270
|
+
if (error instanceof ActionContractError) {
|
|
2271
|
+
logWorkspaceAppsGatewayFailure(error, attempt, undefined);
|
|
2272
|
+
throw error;
|
|
2273
|
+
}
|
|
2274
|
+
const timedOut = controller.signal.aborted;
|
|
2275
|
+
const failure = new ActionContractError(
|
|
2276
|
+
timedOut
|
|
2277
|
+
? "Workspace apps gateway did not answer in time."
|
|
2278
|
+
: "Workspace apps gateway could not be reached.",
|
|
2222
2279
|
{
|
|
2223
2280
|
statusCode: 503,
|
|
2224
|
-
errorCode:
|
|
2281
|
+
errorCode: timedOut
|
|
2282
|
+
? "workspace_gateway_timeout"
|
|
2283
|
+
: "workspace_gateway_unavailable",
|
|
2225
2284
|
},
|
|
2226
2285
|
);
|
|
2286
|
+
logWorkspaceAppsGatewayFailure(failure, attempt, error);
|
|
2287
|
+
throw failure;
|
|
2227
2288
|
} finally {
|
|
2228
2289
|
clearTimeout(timeout);
|
|
2229
2290
|
}
|