@agent-native/dispatch 0.27.19 → 0.28.0
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/update-workspace-app-metadata.js +1 -1
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/actions/view-screen.js +50 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +3 -8
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts +17 -0
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +90 -8
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +2 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +18 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +0 -1
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/workspace-app-chat-proxy.d.ts +52 -0
- package/dist/server/lib/workspace-app-chat-proxy.d.ts.map +1 -0
- package/dist/server/lib/workspace-app-chat-proxy.js +306 -0
- package/dist/server/lib/workspace-app-chat-proxy.js.map +1 -0
- package/dist/server/plugins/core-routes.d.ts +3 -2
- package/dist/server/plugins/core-routes.d.ts.map +1 -1
- package/dist/server/plugins/core-routes.js +12 -4
- package/dist/server/plugins/core-routes.js.map +1 -1
- package/dist/shared/chat-first-pane.d.ts +8 -0
- package/dist/shared/chat-first-pane.d.ts.map +1 -0
- package/dist/shared/chat-first-pane.js +8 -0
- package/dist/shared/chat-first-pane.js.map +1 -0
- package/dist/shared/workspace-app-chat.d.ts +23 -0
- package/dist/shared/workspace-app-chat.d.ts.map +1 -0
- package/dist/shared/workspace-app-chat.js +45 -0
- package/dist/shared/workspace-app-chat.js.map +1 -0
- package/package.json +2 -2
- package/src/actions/update-workspace-app-metadata.ts +1 -1
- package/src/actions/view-screen.spec.ts +93 -1
- package/src/actions/view-screen.ts +74 -1
- package/src/components/layout/Layout.app-chat.spec.ts +22 -13
- package/src/components/layout/Layout.tsx +6 -19
- package/src/components/workspace-app-chat-rail.spec.tsx +182 -0
- package/src/components/workspace-app-host.tsx +172 -21
- package/src/hooks/use-navigation-state.spec.ts +33 -0
- package/src/hooks/use-navigation-state.ts +28 -1
- package/src/server/lib/app-creation-store.spec.ts +32 -20
- package/src/server/lib/app-creation-store.ts +0 -1
- package/src/server/lib/workspace-app-chat-proxy.spec.ts +322 -0
- package/src/server/lib/workspace-app-chat-proxy.ts +389 -0
- package/src/server/plugins/core-routes.ts +21 -4
- package/src/shared/chat-first-pane.ts +7 -0
- package/src/shared/workspace-app-chat.ts +45 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side agent-chat proxy for an app opened inside Dispatch.
|
|
3
|
+
*
|
|
4
|
+
* The Desktop app already solves this by pointing the rail's `apiUrl` at a
|
|
5
|
+
* loopback relay onto the target app's `/_agent-native/agent-chat`, so parity
|
|
6
|
+
* with the app's native chat holds structurally instead of by copying tools and
|
|
7
|
+
* instructions into Dispatch's agent. This is the web equivalent: Dispatch
|
|
8
|
+
* forwards the whole agent-chat subtree to the app's own server.
|
|
9
|
+
*
|
|
10
|
+
* Credential: the app's OWN embed session, minted through the existing paths in
|
|
11
|
+
* `mcp-gateway.ts` — the same credential the app pane's iframe already runs on.
|
|
12
|
+
* Dispatch consumes the one-time ticket server-side and reuses the resulting
|
|
13
|
+
* short-lived embed token as a bearer, which is an authentication path the
|
|
14
|
+
* framework already accepts (`resolveEmbedSessionFromRequest`). No new auth
|
|
15
|
+
* scheme, and no token gains scope it did not already have.
|
|
16
|
+
*
|
|
17
|
+
* A dedicated Nitro route rather than an action: agent-chat streams (SSE for
|
|
18
|
+
* `/runs/:id/events`, a token stream for the turn itself) and returns non-JSON
|
|
19
|
+
* bodies, which is the documented exception in CLAUDE.md to the
|
|
20
|
+
* actions-over-routes rule.
|
|
21
|
+
*/
|
|
22
|
+
export interface WorkspaceAppChatSession {
|
|
23
|
+
/** Target app root, including any configured base path. */
|
|
24
|
+
appBaseUrl: string;
|
|
25
|
+
token: string;
|
|
26
|
+
embedTarget: string;
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Read the embed token's own `exp` claim. Guessing a TTL here would silently
|
|
31
|
+
* produce a session that the target has already rejected, so a token we cannot
|
|
32
|
+
* read an expiry from is an error rather than a default.
|
|
33
|
+
*/
|
|
34
|
+
export declare function embedSessionTokenExpiry(token: string): number;
|
|
35
|
+
/**
|
|
36
|
+
* Exchange the one-time embed ticket for the reusable short-lived token. The
|
|
37
|
+
* target's embed-start route answers a redirect whose location carries both the
|
|
38
|
+
* token and the path the token is bound to.
|
|
39
|
+
*/
|
|
40
|
+
export declare function exchangeEmbedStartUrl(startUrl: string, fetchImpl?: typeof fetch): Promise<WorkspaceAppChatSession>;
|
|
41
|
+
/** Test seam: drop cached app sessions so a spec starts from a cold proxy. */
|
|
42
|
+
export declare function clearWorkspaceAppChatSessions(): void;
|
|
43
|
+
/** h3 v2 request event: `url` is the parsed URL and `req` is the web Request. */
|
|
44
|
+
interface WorkspaceAppChatProxyEvent {
|
|
45
|
+
url: URL;
|
|
46
|
+
req: Request;
|
|
47
|
+
}
|
|
48
|
+
export declare function createWorkspaceAppChatProxyHandler(options?: {
|
|
49
|
+
fetchImpl?: typeof fetch;
|
|
50
|
+
}): (event: WorkspaceAppChatProxyEvent) => Promise<Response>;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=workspace-app-chat-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-app-chat-proxy.d.ts","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-chat-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA8DH,MAAM,WAAW,uBAAuB;IACtC,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAgBD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAc7D;AAuBD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,uBAAuB,CAAC,CA4BlC;AA8CD,8EAA8E;AAC9E,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAoCD,iFAAiF;AACjF,UAAU,0BAA0B;IAClC,GAAG,EAAE,GAAG,CAAC;IACT,GAAG,EAAE,OAAO,CAAC;CACd;AAED,wBAAgB,kCAAkC,CAChD,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAAO,WAGrB,0BAA0B,KAAG,OAAO,CAAC,QAAQ,CAAC,CA+GpE"}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side agent-chat proxy for an app opened inside Dispatch.
|
|
3
|
+
*
|
|
4
|
+
* The Desktop app already solves this by pointing the rail's `apiUrl` at a
|
|
5
|
+
* loopback relay onto the target app's `/_agent-native/agent-chat`, so parity
|
|
6
|
+
* with the app's native chat holds structurally instead of by copying tools and
|
|
7
|
+
* instructions into Dispatch's agent. This is the web equivalent: Dispatch
|
|
8
|
+
* forwards the whole agent-chat subtree to the app's own server.
|
|
9
|
+
*
|
|
10
|
+
* Credential: the app's OWN embed session, minted through the existing paths in
|
|
11
|
+
* `mcp-gateway.ts` — the same credential the app pane's iframe already runs on.
|
|
12
|
+
* Dispatch consumes the one-time ticket server-side and reuses the resulting
|
|
13
|
+
* short-lived embed token as a bearer, which is an authentication path the
|
|
14
|
+
* framework already accepts (`resolveEmbedSessionFromRequest`). No new auth
|
|
15
|
+
* scheme, and no token gains scope it did not already have.
|
|
16
|
+
*
|
|
17
|
+
* A dedicated Nitro route rather than an action: agent-chat streams (SSE for
|
|
18
|
+
* `/runs/:id/events`, a token stream for the turn itself) and returns non-JSON
|
|
19
|
+
* bodies, which is the documented exception in CLAUDE.md to the
|
|
20
|
+
* actions-over-routes rule.
|
|
21
|
+
*/
|
|
22
|
+
import { getOrgContext } from "@agent-native/core/org";
|
|
23
|
+
import { getSession, runWithRequestContext } from "@agent-native/core/server";
|
|
24
|
+
import { EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, } from "@agent-native/core/shared";
|
|
25
|
+
import { parseWorkspaceAppChatProxyPath } from "../../shared/workspace-app-chat.js";
|
|
26
|
+
import { createGrantedDispatchMcpEmbedSession, createWorkspaceSsoEmbedSession, } from "./mcp-gateway.js";
|
|
27
|
+
const TARGET_CHAT_PATH = "/_agent-native/agent-chat";
|
|
28
|
+
/**
|
|
29
|
+
* `X-Agent-Native-Embed-Target` names the path a bearer embed token was minted
|
|
30
|
+
* for. Without it the target only honours the token on requests whose own path
|
|
31
|
+
* matches, which an agent-chat call never does. Duplicated from
|
|
32
|
+
* `@agent-native/core/shared/embed-auth` because that constant is not part of
|
|
33
|
+
* the package's public export surface.
|
|
34
|
+
*/
|
|
35
|
+
const EMBED_TARGET_HEADER = "x-agent-native-embed-target";
|
|
36
|
+
const CSRF_MARKER_HEADER = "x-agent-native-csrf";
|
|
37
|
+
/** Re-mint this far before the token's own expiry so a long turn cannot age out mid-stream. */
|
|
38
|
+
const SESSION_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
39
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
40
|
+
"connection",
|
|
41
|
+
"keep-alive",
|
|
42
|
+
"proxy-authenticate",
|
|
43
|
+
"proxy-authorization",
|
|
44
|
+
"te",
|
|
45
|
+
"trailer",
|
|
46
|
+
"transfer-encoding",
|
|
47
|
+
"upgrade",
|
|
48
|
+
]);
|
|
49
|
+
/** Headers Dispatch owns on the upstream call, or that must not cross origins. */
|
|
50
|
+
const DROPPED_REQUEST_HEADERS = new Set([
|
|
51
|
+
"accept-encoding",
|
|
52
|
+
"authorization",
|
|
53
|
+
"content-length",
|
|
54
|
+
"cookie",
|
|
55
|
+
"host",
|
|
56
|
+
"origin",
|
|
57
|
+
"referer",
|
|
58
|
+
EMBED_TARGET_HEADER,
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* `set-cookie` would land on Dispatch's origin, and the encoding/length headers
|
|
62
|
+
* describe the upstream wire body, which `fetch` has already decoded.
|
|
63
|
+
*/
|
|
64
|
+
const DROPPED_RESPONSE_HEADERS = new Set([
|
|
65
|
+
"content-encoding",
|
|
66
|
+
"content-length",
|
|
67
|
+
"set-cookie",
|
|
68
|
+
"transfer-encoding",
|
|
69
|
+
]);
|
|
70
|
+
const sessionCache = new Map();
|
|
71
|
+
function sessionCacheKey(input) {
|
|
72
|
+
return [input.userEmail, input.orgId ?? "", input.appId].join("\u0000");
|
|
73
|
+
}
|
|
74
|
+
function errorMessage(error) {
|
|
75
|
+
return error instanceof Error ? error.message : String(error ?? "");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the embed token's own `exp` claim. Guessing a TTL here would silently
|
|
79
|
+
* produce a session that the target has already rejected, so a token we cannot
|
|
80
|
+
* read an expiry from is an error rather than a default.
|
|
81
|
+
*/
|
|
82
|
+
export function embedSessionTokenExpiry(token) {
|
|
83
|
+
const payload = token.split(".")[0];
|
|
84
|
+
if (!payload)
|
|
85
|
+
throw new Error("Embed session token is malformed.");
|
|
86
|
+
let claims;
|
|
87
|
+
try {
|
|
88
|
+
claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error("Embed session token payload could not be decoded.");
|
|
92
|
+
}
|
|
93
|
+
const exp = claims?.exp;
|
|
94
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) {
|
|
95
|
+
throw new Error("Embed session token has no usable expiry.");
|
|
96
|
+
}
|
|
97
|
+
return exp * 1000;
|
|
98
|
+
}
|
|
99
|
+
async function mintEmbedStartUrl(appId) {
|
|
100
|
+
const input = { app: appId, path: "/", chrome: "minimal" };
|
|
101
|
+
// Mirrors `WorkspaceAppFrame`: the workspace-sign-in mint when the rollout
|
|
102
|
+
// covers this app, the Dispatch MCP grant mint otherwise. Both return the
|
|
103
|
+
// target app's own embed session — neither is a fallback onto Dispatch's
|
|
104
|
+
// agent, which this proxy must never do.
|
|
105
|
+
try {
|
|
106
|
+
return (await createWorkspaceSsoEmbedSession(input)).startUrl;
|
|
107
|
+
}
|
|
108
|
+
catch (ssoError) {
|
|
109
|
+
try {
|
|
110
|
+
return (await createGrantedDispatchMcpEmbedSession(input)).startUrl;
|
|
111
|
+
}
|
|
112
|
+
catch (grantError) {
|
|
113
|
+
throw new Error(`Could not create an app session for "${appId}". ` +
|
|
114
|
+
`Workspace sign-in: ${errorMessage(ssoError)}. ` +
|
|
115
|
+
`Dispatch app grant: ${errorMessage(grantError)}.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Exchange the one-time embed ticket for the reusable short-lived token. The
|
|
121
|
+
* target's embed-start route answers a redirect whose location carries both the
|
|
122
|
+
* token and the path the token is bound to.
|
|
123
|
+
*/
|
|
124
|
+
export async function exchangeEmbedStartUrl(startUrl, fetchImpl = fetch) {
|
|
125
|
+
const start = new URL(startUrl);
|
|
126
|
+
const startIndex = start.pathname.lastIndexOf(EMBED_START_PATH);
|
|
127
|
+
if (startIndex < 0) {
|
|
128
|
+
throw new Error("App session start URL is not an embed start route.");
|
|
129
|
+
}
|
|
130
|
+
const appBaseUrl = `${start.origin}${start.pathname.slice(0, startIndex)}`;
|
|
131
|
+
const response = await fetchImpl(start.toString(), { redirect: "manual" });
|
|
132
|
+
await response.body?.cancel().catch(() => undefined);
|
|
133
|
+
const location = response.headers.get("location");
|
|
134
|
+
if (response.status < 300 || response.status >= 400 || !location) {
|
|
135
|
+
throw new Error(`App session exchange failed with status ${response.status}.`);
|
|
136
|
+
}
|
|
137
|
+
const resolved = new URL(location, start);
|
|
138
|
+
const token = resolved.searchParams.get(EMBED_TOKEN_QUERY_PARAM);
|
|
139
|
+
if (!token) {
|
|
140
|
+
throw new Error("App session exchange returned no embed token.");
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
appBaseUrl,
|
|
144
|
+
token,
|
|
145
|
+
embedTarget: resolved.pathname,
|
|
146
|
+
expiresAt: embedSessionTokenExpiry(token),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function resolveWorkspaceAppChatSession(input) {
|
|
150
|
+
const key = sessionCacheKey(input);
|
|
151
|
+
const cached = sessionCache.get(key);
|
|
152
|
+
if (cached) {
|
|
153
|
+
try {
|
|
154
|
+
const session = await cached;
|
|
155
|
+
if (session.expiresAt - Date.now() > SESSION_REFRESH_MARGIN_MS) {
|
|
156
|
+
return session;
|
|
157
|
+
}
|
|
158
|
+
// coercion-ok: a mint that already failed is retried below, and that fresh
|
|
159
|
+
// attempt reports its own error rather than replaying a stale one.
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
void 0;
|
|
163
|
+
}
|
|
164
|
+
sessionCache.delete(key);
|
|
165
|
+
}
|
|
166
|
+
const pending = Promise.resolve(runWithRequestContext({
|
|
167
|
+
userEmail: input.userEmail,
|
|
168
|
+
...(input.orgId ? { orgId: input.orgId } : {}),
|
|
169
|
+
}, async () => exchangeEmbedStartUrl(await mintEmbedStartUrl(input.appId), input.fetchImpl)));
|
|
170
|
+
sessionCache.set(key, pending);
|
|
171
|
+
try {
|
|
172
|
+
return await pending;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
sessionCache.delete(key);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Test seam: drop cached app sessions so a spec starts from a cold proxy. */
|
|
180
|
+
export function clearWorkspaceAppChatSessions() {
|
|
181
|
+
sessionCache.clear();
|
|
182
|
+
}
|
|
183
|
+
function upstreamRequestHeaders(request, session) {
|
|
184
|
+
const headers = new Headers();
|
|
185
|
+
for (const [name, value] of request.headers) {
|
|
186
|
+
const lower = name.toLowerCase();
|
|
187
|
+
if (HOP_BY_HOP_HEADERS.has(lower) || DROPPED_REQUEST_HEADERS.has(lower)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
headers.set(name, value);
|
|
191
|
+
}
|
|
192
|
+
headers.set("authorization", `Bearer ${session.token}`);
|
|
193
|
+
headers.set(EMBED_TARGET_HEADER, session.embedTarget);
|
|
194
|
+
// Documented same-origin marker (see packages/core/src/server/csrf.ts). The
|
|
195
|
+
// proxy sends no cookies, but stating first-party intent keeps the contract
|
|
196
|
+
// explicit rather than relying on the no-cookie shortcut.
|
|
197
|
+
headers.set(CSRF_MARKER_HEADER, "1");
|
|
198
|
+
return headers;
|
|
199
|
+
}
|
|
200
|
+
function proxyErrorResponse(status, message) {
|
|
201
|
+
// An unavailable proxy is a visible failure. Dispatch must never answer an
|
|
202
|
+
// app-scoped chat request with its own agent: that would look like it worked
|
|
203
|
+
// while silently supplying the wrong tools and instructions.
|
|
204
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
205
|
+
status,
|
|
206
|
+
headers: {
|
|
207
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
208
|
+
"Cache-Control": "no-store",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
export function createWorkspaceAppChatProxyHandler(options = {}) {
|
|
213
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
214
|
+
return async (event) => {
|
|
215
|
+
const url = event.url;
|
|
216
|
+
const parsed = parseWorkspaceAppChatProxyPath(url.pathname);
|
|
217
|
+
if (!parsed) {
|
|
218
|
+
return proxyErrorResponse(404, "Workspace app chat route not found.");
|
|
219
|
+
}
|
|
220
|
+
// "Not signed in" and "the session store did not answer" are different
|
|
221
|
+
// failures. Telling a signed-in user to sign in during an outage is the
|
|
222
|
+
// confidently-wrong report this proxy must not produce.
|
|
223
|
+
let session;
|
|
224
|
+
let orgId;
|
|
225
|
+
try {
|
|
226
|
+
session = await getSession(event);
|
|
227
|
+
orgId = (await getOrgContext(event)).orgId ?? undefined;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
return proxyErrorResponse(503, `Dispatch could not read your workspace session: ${errorMessage(error)}`);
|
|
231
|
+
}
|
|
232
|
+
if (!session?.email) {
|
|
233
|
+
return proxyErrorResponse(401, "Sign in to Dispatch to use app chat.");
|
|
234
|
+
}
|
|
235
|
+
let appSession;
|
|
236
|
+
try {
|
|
237
|
+
appSession = await resolveWorkspaceAppChatSession({
|
|
238
|
+
appId: parsed.appId,
|
|
239
|
+
userEmail: session.email,
|
|
240
|
+
orgId,
|
|
241
|
+
fetchImpl,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.warn("[dispatch] workspace app chat proxy could not connect", {
|
|
246
|
+
app: parsed.appId,
|
|
247
|
+
error: errorMessage(error),
|
|
248
|
+
});
|
|
249
|
+
return proxyErrorResponse(502, `${parsed.appId} chat is unavailable: ${errorMessage(error)}`);
|
|
250
|
+
}
|
|
251
|
+
const request = event.req;
|
|
252
|
+
const target = new URL(`${appSession.appBaseUrl}${TARGET_CHAT_PATH}${parsed.targetSubPath}`);
|
|
253
|
+
target.search = url.search;
|
|
254
|
+
const method = request.method.toUpperCase();
|
|
255
|
+
// The turn body is a small JSON payload, so buffering the REQUEST costs
|
|
256
|
+
// nothing and avoids half-duplex streaming support varying by runtime. The
|
|
257
|
+
// RESPONSE is the streaming half and is piped through untouched below.
|
|
258
|
+
const body = method === "GET" || method === "HEAD"
|
|
259
|
+
? undefined
|
|
260
|
+
: await request.arrayBuffer();
|
|
261
|
+
let upstream;
|
|
262
|
+
try {
|
|
263
|
+
upstream = await fetchImpl(target.toString(), {
|
|
264
|
+
method,
|
|
265
|
+
headers: upstreamRequestHeaders(request, appSession),
|
|
266
|
+
...(body === undefined ? {} : { body }),
|
|
267
|
+
redirect: "manual",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
console.warn("[dispatch] workspace app chat proxy request failed", {
|
|
272
|
+
app: parsed.appId,
|
|
273
|
+
targetPath: target.pathname,
|
|
274
|
+
error: errorMessage(error),
|
|
275
|
+
});
|
|
276
|
+
return proxyErrorResponse(502, `${parsed.appId} chat did not respond: ${errorMessage(error)}`);
|
|
277
|
+
}
|
|
278
|
+
// A rejected embed session is not a transport failure: drop the cached
|
|
279
|
+
// credential so the next request re-mints instead of looping on a token the
|
|
280
|
+
// app has already stopped honouring.
|
|
281
|
+
if (upstream.status === 401 || upstream.status === 403) {
|
|
282
|
+
sessionCache.delete(sessionCacheKey({
|
|
283
|
+
appId: parsed.appId,
|
|
284
|
+
userEmail: session.email,
|
|
285
|
+
orgId,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
const headers = new Headers();
|
|
289
|
+
for (const [name, value] of upstream.headers) {
|
|
290
|
+
const lower = name.toLowerCase();
|
|
291
|
+
if (HOP_BY_HOP_HEADERS.has(lower) ||
|
|
292
|
+
DROPPED_RESPONSE_HEADERS.has(lower)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
headers.set(name, value);
|
|
296
|
+
}
|
|
297
|
+
// `upstream.body` is the live stream: SSE run events and the turn's token
|
|
298
|
+
// stream reach the browser as the app emits them, never buffered here.
|
|
299
|
+
return new Response(upstream.body, {
|
|
300
|
+
status: upstream.status,
|
|
301
|
+
statusText: upstream.statusText,
|
|
302
|
+
headers,
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=workspace-app-chat-proxy.js.map
|
|
@@ -0,0 +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":["/**\n * Server-side agent-chat proxy for an app opened inside Dispatch.\n *\n * The Desktop app already solves this by pointing the rail's `apiUrl` at a\n * loopback relay onto the target app's `/_agent-native/agent-chat`, so parity\n * with the app's native chat holds structurally instead of by copying tools and\n * instructions into Dispatch's agent. This is the web equivalent: Dispatch\n * forwards the whole agent-chat subtree to the app's own server.\n *\n * Credential: the app's OWN embed session, minted through the existing paths in\n * `mcp-gateway.ts` — the same credential the app pane's iframe already runs on.\n * Dispatch consumes the one-time ticket server-side and reuses the resulting\n * short-lived embed token as a bearer, which is an authentication path the\n * framework already accepts (`resolveEmbedSessionFromRequest`). No new auth\n * scheme, and no token gains scope it did not already have.\n *\n * A dedicated Nitro route rather than an action: agent-chat streams (SSE for\n * `/runs/:id/events`, a token stream for the turn itself) and returns non-JSON\n * bodies, which is the documented exception in CLAUDE.md to the\n * actions-over-routes rule.\n */\n\nimport { 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\n/** Headers Dispatch owns on the upstream call, or that must not cross origins. */\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\n/**\n * `set-cookie` would land on Dispatch's origin, and the encoding/length headers\n * describe the upstream wire body, which `fetch` has already decoded.\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 /** Target app root, including any configured base path. */\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 ? error.message : String(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 // Mirrors `WorkspaceAppFrame`: the workspace-sign-in mint when the rollout\n // covers this app, the Dispatch MCP grant mint otherwise. Both return the\n // target app's own embed session — neither is a fallback onto Dispatch's\n // agent, which this proxy must never do.\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\n/**\n * Exchange the one-time embed ticket for the reusable short-lived token. The\n * target's embed-start route answers a redirect whose location carries both the\n * token and the path the token is bound to.\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\n/** Test seam: drop cached app sessions so a spec starts from a cold proxy. */\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 // Documented same-origin marker (see packages/core/src/server/csrf.ts). The\n // proxy sends no cookies, but stating first-party intent keeps the contract\n // explicit rather than relying on the no-cookie shortcut.\n headers.set(CSRF_MARKER_HEADER, \"1\");\n return headers;\n}\n\nfunction proxyErrorResponse(status: number, message: string): Response {\n // An unavailable proxy is a visible failure. Dispatch must never answer an\n // app-scoped chat request with its own agent: that would look like it worked\n // while silently supplying the wrong tools and instructions.\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\n/** h3 v2 request event: `url` is the parsed URL and `req` is the web Request. */\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 parsed = parseWorkspaceAppChatProxyPath(url.pathname);\n if (!parsed) {\n return proxyErrorResponse(404, \"Workspace app chat route not found.\");\n }\n\n // \"Not signed in\" and \"the session store did not answer\" are different\n // failures. Telling a signed-in user to sign in during an outage is the\n // confidently-wrong report this proxy must not produce.\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 // The turn body is a small JSON payload, so buffering the REQUEST costs\n // nothing and avoids half-duplex streaming support varying by runtime. The\n // RESPONSE is the streaming half and is piped through untouched below.\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 // A rejected embed session is not a transport failure: drop the cached\n // credential so the next request re-mints instead of looping on a token the\n // app has already stopped honouring.\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,3 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type NitroPluginDef } from "@agent-native/core/server";
|
|
2
|
+
declare const dispatchCoreRoutesPlugin: NitroPluginDef;
|
|
3
|
+
export default dispatchCoreRoutesPlugin;
|
|
3
4
|
//# sourceMappingURL=core-routes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core-routes.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/core-routes.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"core-routes.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/core-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAanC,QAAA,MAAM,wBAAwB,EAAE,cAS/B,CAAC;eAEa,wBAAwB"}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { createCoreRoutesPlugin } from "@agent-native/core/server";
|
|
1
|
+
import { createCoreRoutesPlugin, getH3App, } from "@agent-native/core/server";
|
|
2
|
+
import { WORKSPACE_APP_CHAT_PROXY_PREFIX } from "../../shared/workspace-app-chat.js";
|
|
2
3
|
import { envKeys } from "../lib/env-config.js";
|
|
3
4
|
import { registerDispatchOnboardingSteps } from "../lib/onboarding-steps.js";
|
|
5
|
+
import { createWorkspaceAppChatProxyHandler } from "../lib/workspace-app-chat-proxy.js";
|
|
4
6
|
// Register before the core plugin so "create your first app" (order 5) appears
|
|
5
7
|
// above the auto-generated Slack/Telegram steps (order 60). Idempotent.
|
|
6
8
|
registerDispatchOnboardingSteps();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const corePlugin = createCoreRoutesPlugin({ envKeys });
|
|
10
|
+
const dispatchCoreRoutesPlugin = (nitroApp) => {
|
|
11
|
+
const coreInit = corePlugin(nitroApp);
|
|
12
|
+
// `getH3App` installs the framework CSRF middleware on its first call for
|
|
13
|
+
// this app, so any route registered through it is already behind that check.
|
|
14
|
+
getH3App(nitroApp).use(WORKSPACE_APP_CHAT_PROXY_PREFIX, createWorkspaceAppChatProxyHandler());
|
|
15
|
+
return coreInit;
|
|
16
|
+
};
|
|
17
|
+
export default dispatchCoreRoutesPlugin;
|
|
10
18
|
//# sourceMappingURL=core-routes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core-routes.js","sourceRoot":"","sources":["../../../src/server/plugins/core-routes.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"core-routes.js","sourceRoot":"","sources":["../../../src/server/plugins/core-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,QAAQ,GAET,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,+BAA+B,EAAE,MAAM,oCAAoC,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,EAAE,kCAAkC,EAAE,MAAM,oCAAoC,CAAC;AAExF,+EAA+E;AAC/E,wEAAwE;AACxE,+BAA+B,EAAE,CAAC;AAElC,MAAM,UAAU,GAAG,sBAAsB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAEvD,MAAM,wBAAwB,GAAmB,CAAC,QAAQ,EAAE,EAAE;IAC5D,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACtC,0EAA0E;IAC1E,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,CACpB,+BAA+B,EAC/B,kCAAkC,EAAE,CACrC,CAAC;IACF,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,eAAe,wBAAwB,CAAC","sourcesContent":["import {\n createCoreRoutesPlugin,\n getH3App,\n type NitroPluginDef,\n} from \"@agent-native/core/server\";\n\nimport { WORKSPACE_APP_CHAT_PROXY_PREFIX } from \"../../shared/workspace-app-chat.js\";\nimport { envKeys } from \"../lib/env-config.js\";\nimport { registerDispatchOnboardingSteps } from \"../lib/onboarding-steps.js\";\nimport { createWorkspaceAppChatProxyHandler } from \"../lib/workspace-app-chat-proxy.js\";\n\n// Register before the core plugin so \"create your first app\" (order 5) appears\n// above the auto-generated Slack/Telegram steps (order 60). Idempotent.\nregisterDispatchOnboardingSteps();\n\nconst corePlugin = createCoreRoutesPlugin({ envKeys });\n\nconst dispatchCoreRoutesPlugin: NitroPluginDef = (nitroApp) => {\n const coreInit = corePlugin(nitroApp);\n // `getH3App` installs the framework CSRF middleware on its first call for\n // this app, so any route registered through it is already behind that check.\n getH3App(nitroApp).use(\n WORKSPACE_APP_CHAT_PROXY_PREFIX,\n createWorkspaceAppChatProxyHandler(),\n );\n return coreInit;\n};\n\nexport default dispatchCoreRoutesPlugin;\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-state key holding the workspace app the chat-first shell has on
|
|
3
|
+
* screen. In chat-first mode the app opens as a surface tab while the route
|
|
4
|
+
* stays on /chat, so this key is the only signal that names the open app.
|
|
5
|
+
* Written by the layout, read by view-screen — keep them on this one constant.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CHAT_FIRST_PANE_STATE_KEY = "chat-first-pane";
|
|
8
|
+
//# sourceMappingURL=chat-first-pane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-first-pane.d.ts","sourceRoot":"","sources":["../../src/shared/chat-first-pane.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-state key holding the workspace app the chat-first shell has on
|
|
3
|
+
* screen. In chat-first mode the app opens as a surface tab while the route
|
|
4
|
+
* stays on /chat, so this key is the only signal that names the open app.
|
|
5
|
+
* Written by the layout, read by view-screen — keep them on this one constant.
|
|
6
|
+
*/
|
|
7
|
+
export const CHAT_FIRST_PANE_STATE_KEY = "chat-first-pane";
|
|
8
|
+
//# sourceMappingURL=chat-first-pane.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-first-pane.js","sourceRoot":"","sources":["../../src/shared/chat-first-pane.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,iBAAiB,CAAC","sourcesContent":["/**\n * Application-state key holding the workspace app the chat-first shell has on\n * screen. In chat-first mode the app opens as a surface tab while the route\n * stays on /chat, so this key is the only signal that names the open app.\n * Written by the layout, read by view-screen — keep them on this one constant.\n */\nexport const CHAT_FIRST_PANE_STATE_KEY = \"chat-first-pane\";\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route contract for the Dispatch → workspace app agent-chat proxy.
|
|
3
|
+
*
|
|
4
|
+
* When an app is open in Dispatch, its chat rail must talk to that app's own
|
|
5
|
+
* agent (its tools, its AGENTS.md and skills, its org resources, its dev-mode
|
|
6
|
+
* surface) instead of to Dispatch's agent. The rail points `apiUrl` at this
|
|
7
|
+
* Dispatch-side prefix and the server forwards the whole agent-chat subtree to
|
|
8
|
+
* the app's `/_agent-native/agent-chat`, exactly as the Desktop loopback relay
|
|
9
|
+
* does. Shared so the browser can build the URL without importing server code.
|
|
10
|
+
*/
|
|
11
|
+
export declare const WORKSPACE_APP_CHAT_PROXY_PREFIX = "/_agent-native/workspace-app-chat";
|
|
12
|
+
/** Dispatch-relative base URL the chat rail uses as its `apiUrl`. */
|
|
13
|
+
export declare function workspaceAppChatProxyPath(appId: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Split `/<appId>[/rest]` off a request that already matched the prefix.
|
|
16
|
+
* Returns null when there is no app segment, so the route fails loudly instead
|
|
17
|
+
* of proxying to an unresolved target.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseWorkspaceAppChatProxyPath(pathname: string): {
|
|
20
|
+
appId: string;
|
|
21
|
+
targetSubPath: string;
|
|
22
|
+
} | null;
|
|
23
|
+
//# sourceMappingURL=workspace-app-chat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-app-chat.d.ts","sourceRoot":"","sources":["../../src/shared/workspace-app-chat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,+BAA+B,sCACP,CAAC;AAEtC,qEAAqE;AACrE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,MAAM,GACf;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAkBjD"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route contract for the Dispatch → workspace app agent-chat proxy.
|
|
3
|
+
*
|
|
4
|
+
* When an app is open in Dispatch, its chat rail must talk to that app's own
|
|
5
|
+
* agent (its tools, its AGENTS.md and skills, its org resources, its dev-mode
|
|
6
|
+
* surface) instead of to Dispatch's agent. The rail points `apiUrl` at this
|
|
7
|
+
* Dispatch-side prefix and the server forwards the whole agent-chat subtree to
|
|
8
|
+
* the app's `/_agent-native/agent-chat`, exactly as the Desktop loopback relay
|
|
9
|
+
* does. Shared so the browser can build the URL without importing server code.
|
|
10
|
+
*/
|
|
11
|
+
export const WORKSPACE_APP_CHAT_PROXY_PREFIX = "/_agent-native/workspace-app-chat";
|
|
12
|
+
/** Dispatch-relative base URL the chat rail uses as its `apiUrl`. */
|
|
13
|
+
export function workspaceAppChatProxyPath(appId) {
|
|
14
|
+
return `${WORKSPACE_APP_CHAT_PROXY_PREFIX}/${encodeURIComponent(appId.trim())}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Split `/<appId>[/rest]` off a request that already matched the prefix.
|
|
18
|
+
* Returns null when there is no app segment, so the route fails loudly instead
|
|
19
|
+
* of proxying to an unresolved target.
|
|
20
|
+
*/
|
|
21
|
+
export function parseWorkspaceAppChatProxyPath(pathname) {
|
|
22
|
+
const index = pathname.indexOf(WORKSPACE_APP_CHAT_PROXY_PREFIX);
|
|
23
|
+
if (index < 0)
|
|
24
|
+
return null;
|
|
25
|
+
const rest = pathname.slice(index + WORKSPACE_APP_CHAT_PROXY_PREFIX.length);
|
|
26
|
+
if (!rest.startsWith("/"))
|
|
27
|
+
return null;
|
|
28
|
+
const segments = rest.slice(1).split("/");
|
|
29
|
+
const rawAppId = segments.shift() ?? "";
|
|
30
|
+
let appId;
|
|
31
|
+
try {
|
|
32
|
+
appId = decodeURIComponent(rawAppId).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// coercion-ok: malformed percent-encoding names no workspace app.
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (!appId)
|
|
39
|
+
return null;
|
|
40
|
+
const targetSubPath = segments.length > 0 ? `/${segments.join("/")}` : "";
|
|
41
|
+
if (targetSubPath.includes(".."))
|
|
42
|
+
return null;
|
|
43
|
+
return { appId, targetSubPath };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=workspace-app-chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-app-chat.js","sourceRoot":"","sources":["../../src/shared/workspace-app-chat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,+BAA+B,GAC1C,mCAAmC,CAAC;AAEtC,qEAAqE;AACrE,MAAM,UAAU,yBAAyB,CAAC,KAAa;IACrD,OAAO,GAAG,+BAA+B,IAAI,kBAAkB,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;AAClF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAAgB;IAEhB,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;IAChE,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,+BAA+B,CAAC,MAAM,CAAC,CAAC;IAC5E,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC;IACxC,IAAI,KAAa,CAAC;IAClB,IAAI,CAAC;QACH,KAAK,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,kEAAkE;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,IAAI,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;AAClC,CAAC","sourcesContent":["/**\n * Route contract for the Dispatch → workspace app agent-chat proxy.\n *\n * When an app is open in Dispatch, its chat rail must talk to that app's own\n * agent (its tools, its AGENTS.md and skills, its org resources, its dev-mode\n * surface) instead of to Dispatch's agent. The rail points `apiUrl` at this\n * Dispatch-side prefix and the server forwards the whole agent-chat subtree to\n * the app's `/_agent-native/agent-chat`, exactly as the Desktop loopback relay\n * does. Shared so the browser can build the URL without importing server code.\n */\n\nexport const WORKSPACE_APP_CHAT_PROXY_PREFIX =\n \"/_agent-native/workspace-app-chat\";\n\n/** Dispatch-relative base URL the chat rail uses as its `apiUrl`. */\nexport function workspaceAppChatProxyPath(appId: string): string {\n return `${WORKSPACE_APP_CHAT_PROXY_PREFIX}/${encodeURIComponent(appId.trim())}`;\n}\n\n/**\n * Split `/<appId>[/rest]` off a request that already matched the prefix.\n * Returns null when there is no app segment, so the route fails loudly instead\n * of proxying to an unresolved target.\n */\nexport function parseWorkspaceAppChatProxyPath(\n pathname: string,\n): { appId: string; targetSubPath: string } | null {\n const index = pathname.indexOf(WORKSPACE_APP_CHAT_PROXY_PREFIX);\n if (index < 0) return null;\n const rest = pathname.slice(index + WORKSPACE_APP_CHAT_PROXY_PREFIX.length);\n if (!rest.startsWith(\"/\")) return null;\n const segments = rest.slice(1).split(\"/\");\n const rawAppId = segments.shift() ?? \"\";\n let appId: string;\n try {\n appId = decodeURIComponent(rawAppId).trim();\n } catch {\n // coercion-ok: malformed percent-encoding names no workspace app.\n return null;\n }\n if (!appId) return null;\n const targetSubPath = segments.length > 0 ? `/${segments.join(\"/\")}` : \"\";\n if (targetSubPath.includes(\"..\")) return null;\n return { appId, targetSubPath };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
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": {
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
102
102
|
"vite": "8.1.0",
|
|
103
103
|
"vitest": "^4.1.5",
|
|
104
|
-
"@agent-native/core": "0.
|
|
104
|
+
"@agent-native/core": "0.160.1"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -6,7 +6,7 @@ import { updateWorkspaceAppMetadata } from "../server/lib/app-creation-store.js"
|
|
|
6
6
|
|
|
7
7
|
export default defineAction({
|
|
8
8
|
description:
|
|
9
|
-
"Update the human-editable display name and description Dispatch uses for a workspace app.
|
|
9
|
+
"Update the human-editable display name and description Dispatch uses for a workspace app. Workspace members can edit these shared details, which are also used as connected-agent/A2A context.",
|
|
10
10
|
schema: z.object({
|
|
11
11
|
appId: z
|
|
12
12
|
.string()
|