@jami-studio/core 0.92.13 → 0.92.15
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/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/deploy/build.ts +31 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +1458 -1453
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +30 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +207 -202
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,1453 +1,1458 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agent-native deploy` — build and deploy every app in a workspace to a
|
|
3
|
-
* single origin. Each app is served from `/<app-name>/*`, so:
|
|
4
|
-
*
|
|
5
|
-
* https://your-agents.com/mail/* → apps/mail
|
|
6
|
-
* https://your-agents.com/calendar/* → apps/calendar
|
|
7
|
-
*
|
|
8
|
-
* Benefits of same-origin deploy:
|
|
9
|
-
* - Shared auth cookie → log in once, every app is signed in
|
|
10
|
-
* - Cross-app A2A is a same-origin fetch (no CORS, no JWT for siblings)
|
|
11
|
-
* - One DNS record, one TLS cert, one CDN cache
|
|
12
|
-
*
|
|
13
|
-
* Per-app independent deploy is still supported — just cd into the app and
|
|
14
|
-
* run `agent-native build` as before. This orchestrator is for teams that
|
|
15
|
-
* want the whole workspace behind one domain.
|
|
16
|
-
*/
|
|
17
|
-
import { execFileSync } from "child_process";
|
|
18
|
-
import fs from "fs";
|
|
19
|
-
import path from "path";
|
|
20
|
-
|
|
21
|
-
import { AGENT_CHAT_PROCESS_RUN_PATH } from "../agent/durable-background.js";
|
|
22
|
-
import { findWorkspaceRoot } from "../scripts/utils.js";
|
|
23
|
-
import {
|
|
24
|
-
DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
25
|
-
normalizeWorkspaceAppAudience,
|
|
26
|
-
normalizeWorkspaceAppPathList,
|
|
27
|
-
workspaceAppAudienceFromPackageJson,
|
|
28
|
-
workspaceAppRouteAccessFromPackageJson,
|
|
29
|
-
type WorkspaceAppRouteAccess,
|
|
30
|
-
type WorkspaceAppAudience,
|
|
31
|
-
} from "../shared/workspace-app-audience.js";
|
|
32
|
-
import { DISPATCH_WORKSPACE_ROOT_REDIRECTS } from "../shared/workspace-app-id.js";
|
|
33
|
-
import {
|
|
34
|
-
collectImmutableAssetPaths,
|
|
35
|
-
IMMUTABLE_ASSET_CACHE_HEADERS,
|
|
36
|
-
} from "./immutable-assets.js";
|
|
37
|
-
|
|
38
|
-
export type WorkspaceDeployPreset = "cloudflare_pages" | "netlify" | "vercel";
|
|
39
|
-
|
|
40
|
-
const NETLIFY_WORKSPACE_STATIC_DIR = "_workspace_static";
|
|
41
|
-
const NETLIFY_PUBLIC_ASSET_EXTENSIONS = new Set([
|
|
42
|
-
"avif",
|
|
43
|
-
"css",
|
|
44
|
-
"gif",
|
|
45
|
-
"ico",
|
|
46
|
-
"jpeg",
|
|
47
|
-
"jpg",
|
|
48
|
-
"js",
|
|
49
|
-
"json",
|
|
50
|
-
"map",
|
|
51
|
-
"mp4",
|
|
52
|
-
"pdf",
|
|
53
|
-
"png",
|
|
54
|
-
"svg",
|
|
55
|
-
"txt",
|
|
56
|
-
"wasm",
|
|
57
|
-
"webm",
|
|
58
|
-
"webmanifest",
|
|
59
|
-
"webp",
|
|
60
|
-
"xml",
|
|
61
|
-
]);
|
|
62
|
-
const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
|
|
63
|
-
const WORKSPACE_APPS_MANIFEST_DIR = ".agent-native";
|
|
64
|
-
const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
|
|
65
|
-
const VERCEL_OUTPUT_DIR = ".vercel/output";
|
|
66
|
-
|
|
67
|
-
interface WorkspaceAppManifestEntry {
|
|
68
|
-
id: string;
|
|
69
|
-
name: string;
|
|
70
|
-
description: string;
|
|
71
|
-
path: string;
|
|
72
|
-
url?: string;
|
|
73
|
-
isDispatch: boolean;
|
|
74
|
-
audience: WorkspaceAppAudience;
|
|
75
|
-
publicPaths: string[];
|
|
76
|
-
protectedPaths: string[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface WorkspaceAppManifestOverride {
|
|
80
|
-
id: string;
|
|
81
|
-
url?: string;
|
|
82
|
-
audience?: WorkspaceAppAudience;
|
|
83
|
-
publicPaths?: string[];
|
|
84
|
-
protectedPaths?: string[];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface WorkspaceDeployOptions {
|
|
88
|
-
args?: string[];
|
|
89
|
-
/** Override the workspace root (defaults to walking up from cwd). */
|
|
90
|
-
workspaceRoot?: string;
|
|
91
|
-
/** Only build — don't invoke the deploy platform CLI. */
|
|
92
|
-
buildOnly?: boolean;
|
|
93
|
-
/** Target preset. Defaults to `cloudflare_pages`. */
|
|
94
|
-
preset?: WorkspaceDeployPreset;
|
|
95
|
-
/** @internal Override process execution in tests. */
|
|
96
|
-
execFile?: typeof execFileSync;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function runWorkspaceDeploy(
|
|
100
|
-
opts: WorkspaceDeployOptions = {},
|
|
101
|
-
): Promise<void> {
|
|
102
|
-
const workspaceRoot =
|
|
103
|
-
opts.workspaceRoot ?? findWorkspaceRoot(process.cwd()) ?? process.cwd();
|
|
104
|
-
const appsDir = path.join(workspaceRoot, "apps");
|
|
105
|
-
if (!fs.existsSync(appsDir)) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`No apps/ directory found at ${workspaceRoot}. Run this inside an agent-native workspace.`,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const rawArgs = opts.args ?? [];
|
|
112
|
-
const args = new Set(rawArgs);
|
|
113
|
-
const buildOnly = opts.buildOnly ?? args.has("--build-only");
|
|
114
|
-
|
|
115
|
-
const apps = fs
|
|
116
|
-
.readdirSync(appsDir, { withFileTypes: true })
|
|
117
|
-
.filter((e) => e.isDirectory())
|
|
118
|
-
.map((e) => e.name)
|
|
119
|
-
.filter((n) => fs.existsSync(path.join(appsDir, n, "package.json")))
|
|
120
|
-
.sort(compareWorkspaceAppIds);
|
|
121
|
-
|
|
122
|
-
if (apps.length === 0) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
`Workspace has no apps. Run \`agent-native add-app\` to add one.`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
assertNoReservedWorkspaceAppIds(apps);
|
|
128
|
-
const workspaceApps = readWorkspaceAppManifest(workspaceRoot, apps);
|
|
129
|
-
|
|
130
|
-
const preset = resolvePreset(opts.preset, rawArgs);
|
|
131
|
-
assertWorkspaceDeployProductionEnv({ buildOnly, preset });
|
|
132
|
-
const distDir = path.join(workspaceRoot, "dist");
|
|
133
|
-
const vercelOutputDir = path.join(workspaceRoot, VERCEL_OUTPUT_DIR);
|
|
134
|
-
if (preset === "vercel") {
|
|
135
|
-
fs.rmSync(vercelOutputDir, { recursive: true, force: true });
|
|
136
|
-
fs.mkdirSync(path.join(vercelOutputDir, "static"), { recursive: true });
|
|
137
|
-
fs.mkdirSync(path.join(vercelOutputDir, "functions"), {
|
|
138
|
-
recursive: true,
|
|
139
|
-
});
|
|
140
|
-
} else {
|
|
141
|
-
fs.rmSync(distDir, { recursive: true, force: true });
|
|
142
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (preset === "netlify") {
|
|
146
|
-
const functionsDir = netlifyFunctionsDir(workspaceRoot);
|
|
147
|
-
fs.rmSync(functionsDir, { recursive: true, force: true });
|
|
148
|
-
fs.mkdirSync(functionsDir, { recursive: true });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log(
|
|
152
|
-
`[workspace-deploy] Building ${apps.length} app(s) for preset=${preset}`,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const execFile = opts.execFile ?? execFileSync;
|
|
156
|
-
for (const app of apps) {
|
|
157
|
-
buildOneApp(workspaceRoot, app, preset, execFile, workspaceApps);
|
|
158
|
-
moveAppBuildIntoWorkspaceOutput(
|
|
159
|
-
workspaceRoot,
|
|
160
|
-
app,
|
|
161
|
-
preset,
|
|
162
|
-
distDir,
|
|
163
|
-
vercelOutputDir,
|
|
164
|
-
workspaceApps,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
writeWorkspaceAppManifests(
|
|
168
|
-
workspaceRoot,
|
|
169
|
-
distDir,
|
|
170
|
-
apps,
|
|
171
|
-
workspaceApps,
|
|
172
|
-
preset,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
if (preset === "netlify") {
|
|
176
|
-
writeNetlifyRedirects(distDir, apps);
|
|
177
|
-
writeNetlifyHeaders(distDir, apps);
|
|
178
|
-
} else if (preset === "vercel") {
|
|
179
|
-
writeVercelBuildConfig(vercelOutputDir, apps);
|
|
180
|
-
} else {
|
|
181
|
-
writeCloudflareRoutingManifest(distDir, apps);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (buildOnly) {
|
|
185
|
-
const outputDir = preset === "vercel" ? vercelOutputDir : distDir;
|
|
186
|
-
console.log(
|
|
187
|
-
`\n[workspace-deploy] Build complete at ${outputDir}. Skipping publish (--build-only).`,
|
|
188
|
-
);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
console.log(`\n[workspace-deploy] Build complete. Publish with:\n`);
|
|
193
|
-
console.log(` cd ${path.relative(process.cwd(), workspaceRoot) || "."}`);
|
|
194
|
-
if (preset === "netlify") {
|
|
195
|
-
console.log(
|
|
196
|
-
` netlify deploy --prod --dir=dist --functions=.netlify/functions-internal\n`,
|
|
197
|
-
);
|
|
198
|
-
} else if (preset === "vercel") {
|
|
199
|
-
console.log(` vercel deploy --prebuilt\n`);
|
|
200
|
-
} else {
|
|
201
|
-
console.log(` wrangler pages deploy dist\n`);
|
|
202
|
-
}
|
|
203
|
-
console.log(
|
|
204
|
-
`All apps live at https://<origin>/<app-name>/*. Log in once on any app\nand the session is shared across the workspace.`,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function buildOneApp(
|
|
209
|
-
workspaceRoot: string,
|
|
210
|
-
app: string,
|
|
211
|
-
preset: WorkspaceDeployPreset,
|
|
212
|
-
execFile: typeof execFileSync,
|
|
213
|
-
workspaceApps: WorkspaceAppManifestEntry[],
|
|
214
|
-
): void {
|
|
215
|
-
const appDir = path.join(workspaceRoot, "apps", app);
|
|
216
|
-
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
217
|
-
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
218
|
-
workspaceApps,
|
|
219
|
-
app,
|
|
220
|
-
);
|
|
221
|
-
const workspaceGatewayUrl =
|
|
222
|
-
process.env.VITE_WORKSPACE_GATEWAY_URL || workspaceBaseUrl();
|
|
223
|
-
const workspaceOAuthUrl = workspaceOAuthOrigin(workspaceGatewayUrl);
|
|
224
|
-
const env: NodeJS.ProcessEnv = {
|
|
225
|
-
...process.env,
|
|
226
|
-
NITRO_PRESET: preset,
|
|
227
|
-
AGENT_NATIVE_WORKSPACE: "1",
|
|
228
|
-
AGENT_NATIVE_WORKSPACE_APP_ID: app,
|
|
229
|
-
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
230
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: app,
|
|
231
|
-
APP_BASE_PATH: `/${app}`,
|
|
232
|
-
VITE_APP_BASE_PATH: `/${app}`,
|
|
233
|
-
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: workspaceAppAudience,
|
|
234
|
-
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: JSON.stringify(
|
|
235
|
-
workspaceAppRouteAccess.publicPaths,
|
|
236
|
-
),
|
|
237
|
-
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: JSON.stringify(
|
|
238
|
-
workspaceAppRouteAccess.protectedPaths,
|
|
239
|
-
),
|
|
240
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: workspaceAppAudience,
|
|
241
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: JSON.stringify(
|
|
242
|
-
workspaceAppRouteAccess.publicPaths,
|
|
243
|
-
),
|
|
244
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: JSON.stringify(
|
|
245
|
-
workspaceAppRouteAccess.protectedPaths,
|
|
246
|
-
),
|
|
247
|
-
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: JSON.stringify(workspaceApps),
|
|
248
|
-
...(workspaceGatewayUrl
|
|
249
|
-
? {
|
|
250
|
-
WORKSPACE_GATEWAY_URL:
|
|
251
|
-
process.env.WORKSPACE_GATEWAY_URL || workspaceGatewayUrl,
|
|
252
|
-
VITE_WORKSPACE_GATEWAY_URL: workspaceGatewayUrl,
|
|
253
|
-
...(workspaceOAuthUrl
|
|
254
|
-
? { VITE_WORKSPACE_OAUTH_ORIGIN: workspaceOAuthUrl }
|
|
255
|
-
: {}),
|
|
256
|
-
}
|
|
257
|
-
: {}),
|
|
258
|
-
[WORKSPACE_APPS_ENV_KEY]: JSON.stringify(workspaceApps),
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
if (preset === "netlify" && appUsesNetlifyUnpooledDatabaseUrl(appDir)) {
|
|
262
|
-
env.DATABASE_URL =
|
|
263
|
-
process.env.NETLIFY_DATABASE_URL_UNPOOLED ??
|
|
264
|
-
process.env.DATABASE_URL ??
|
|
265
|
-
env.DATABASE_URL;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
console.log(
|
|
269
|
-
`[workspace-deploy] Building ${app} (base=/${app}, preset=${preset})`,
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
cleanAppBuildOutputs(appDir);
|
|
273
|
-
|
|
274
|
-
execFile("pnpm", ["--filter", app, "build"], {
|
|
275
|
-
cwd: workspaceRoot,
|
|
276
|
-
env,
|
|
277
|
-
stdio: "inherit",
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function moveAppBuildIntoWorkspaceOutput(
|
|
282
|
-
workspaceRoot: string,
|
|
283
|
-
app: string,
|
|
284
|
-
preset: WorkspaceDeployPreset,
|
|
285
|
-
distDir: string,
|
|
286
|
-
vercelOutputDir: string,
|
|
287
|
-
workspaceApps: WorkspaceAppManifestEntry[],
|
|
288
|
-
): void {
|
|
289
|
-
const appDir = path.join(workspaceRoot, "apps", app);
|
|
290
|
-
if (preset === "vercel") {
|
|
291
|
-
copyVercelAppBuildIntoWorkspace(
|
|
292
|
-
workspaceRoot,
|
|
293
|
-
app,
|
|
294
|
-
vercelOutputDir,
|
|
295
|
-
workspaceApps,
|
|
296
|
-
);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Resolve the per-app build output: prefer dist/ (standard), fall back to
|
|
301
|
-
// .output/ (Nitro's default). The Cloudflare preset emits into dist/
|
|
302
|
-
// containing the worker + assets.
|
|
303
|
-
const candidates = ["dist", ".output"];
|
|
304
|
-
const src = candidates
|
|
305
|
-
.map((c) => path.join(appDir, c))
|
|
306
|
-
.find((p) => fs.existsSync(p));
|
|
307
|
-
if (!src) {
|
|
308
|
-
throw new Error(
|
|
309
|
-
`Expected ${candidates.join(" or ")} under ${appDir} but none existed. Check the app's build script.`,
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
if (preset === "netlify") {
|
|
313
|
-
const mountedSrc = path.join(src, app);
|
|
314
|
-
const staticSrc = fs.existsSync(mountedSrc) ? mountedSrc : src;
|
|
315
|
-
const target = path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app);
|
|
316
|
-
fs.mkdirSync(target, { recursive: true });
|
|
317
|
-
copyDir(staticSrc, target);
|
|
318
|
-
// Nitro/Vite mounted builds can contain a nested copy of public assets at
|
|
319
|
-
// dist/<app>/<app>/...; the workspace root already supplies the outer
|
|
320
|
-
// mount path, so keeping it would publish duplicate /<app>/<app> URLs.
|
|
321
|
-
fs.rmSync(path.join(target, app), { recursive: true, force: true });
|
|
322
|
-
copyNetlifyFunctionIntoWorkspace(workspaceRoot, app, workspaceApps, target);
|
|
323
|
-
} else {
|
|
324
|
-
const target = path.join(distDir, app);
|
|
325
|
-
fs.mkdirSync(target, { recursive: true });
|
|
326
|
-
copyDir(src, target);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function copyVercelAppBuildIntoWorkspace(
|
|
331
|
-
workspaceRoot: string,
|
|
332
|
-
app: string,
|
|
333
|
-
vercelOutputDir: string,
|
|
334
|
-
workspaceApps: WorkspaceAppManifestEntry[],
|
|
335
|
-
): void {
|
|
336
|
-
const appDir = path.join(workspaceRoot, "apps", app);
|
|
337
|
-
const src = path.join(appDir, VERCEL_OUTPUT_DIR);
|
|
338
|
-
if (!fs.existsSync(src)) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
`Expected Vercel output at ${src} after building ${app}. Check the app's build script and NITRO_PRESET.`,
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const staticSrc = path.join(src, "static");
|
|
345
|
-
const staticDest = path.join(vercelOutputDir, "static");
|
|
346
|
-
if (fs.existsSync(staticSrc)) {
|
|
347
|
-
copyDir(staticSrc, staticDest);
|
|
348
|
-
// Nitro's Vercel preset already nests assets under baseURL. The shared
|
|
349
|
-
// deploy build also mirrors client assets under baseURL for other Nitro
|
|
350
|
-
// presets, so mounted apps can contain a duplicate /<app>/<app> copy.
|
|
351
|
-
fs.rmSync(path.join(staticDest, app, app), {
|
|
352
|
-
recursive: true,
|
|
353
|
-
force: true,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const functionSrc = path.join(src, "functions", "__server.func");
|
|
358
|
-
if (!fs.existsSync(functionSrc)) {
|
|
359
|
-
throw new Error(
|
|
360
|
-
`Expected Vercel function at ${functionSrc} after building ${app}. Check the app's build script and NITRO_PRESET.`,
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const functionDest = path.join(
|
|
365
|
-
vercelOutputDir,
|
|
366
|
-
"functions",
|
|
367
|
-
`${app}-server.func`,
|
|
368
|
-
);
|
|
369
|
-
fs.rmSync(functionDest, { recursive: true, force: true });
|
|
370
|
-
copyDir(functionSrc, functionDest);
|
|
371
|
-
patchVercelFunctionEntry(functionDest, app, workspaceApps);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Write the Cloudflare Pages `_routes.json` and a dispatcher `_worker.js` at
|
|
376
|
-
* the workspace dist root so each app is reachable under /<app>/*.
|
|
377
|
-
*/
|
|
378
|
-
function writeCloudflareRoutingManifest(distDir: string, apps: string[]): void {
|
|
379
|
-
const dispatchFaviconAsset = apps.includes("dispatch")
|
|
380
|
-
? dispatchRootFaviconAsset(distDir)
|
|
381
|
-
: null;
|
|
382
|
-
// _routes.json tells Cloudflare which paths are dynamic (Functions) vs
|
|
383
|
-
// static. Mark both /<app> and /<app>/* as include so every app's worker
|
|
384
|
-
// handles its root and subtree.
|
|
385
|
-
const include = apps.flatMap((a) => [`/${a}`, `/${a}/*`]).concat(["/"]);
|
|
386
|
-
if (apps.includes("dispatch")) {
|
|
387
|
-
include.push("/_agent-native/*");
|
|
388
|
-
include.push("/.well-known/*");
|
|
389
|
-
include.push(
|
|
390
|
-
...DISPATCH_WORKSPACE_ROOT_REDIRECTS.map(([from]) => `/${from}`),
|
|
391
|
-
);
|
|
392
|
-
include.push("/apps/*");
|
|
393
|
-
if (dispatchFaviconAsset) include.push("/favicon.ico");
|
|
394
|
-
}
|
|
395
|
-
const routes = {
|
|
396
|
-
version: 1,
|
|
397
|
-
include,
|
|
398
|
-
exclude: [],
|
|
399
|
-
};
|
|
400
|
-
fs.writeFileSync(
|
|
401
|
-
path.join(distDir, "_routes.json"),
|
|
402
|
-
JSON.stringify(routes, null, 2) + "\n",
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
// Dispatcher worker: inspects the path and forwards to the matching
|
|
406
|
-
// per-app worker.
|
|
407
|
-
const imports = apps
|
|
408
|
-
.map((a) => `import ${moduleIdent(a)} from "./${a}/_worker.js";`)
|
|
409
|
-
.join("\n");
|
|
410
|
-
const dispatch = apps
|
|
411
|
-
.map(
|
|
412
|
-
(a) =>
|
|
413
|
-
` if (pathname === "/${a}" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
|
|
414
|
-
)
|
|
415
|
-
.join("\n");
|
|
416
|
-
const dispatchRootFrameworkRoutes = apps.includes("dispatch")
|
|
417
|
-
? ` if (pathname === "/_agent-native" || pathname.startsWith("/_agent-native/") || pathname === "/.well-known" || pathname.startsWith("/.well-known/")) return ${moduleIdent("dispatch")}.fetch(request, env, ctx);
|
|
418
|
-
`
|
|
419
|
-
: "";
|
|
420
|
-
const dispatchRootFaviconRoute = dispatchFaviconAsset
|
|
421
|
-
? ` if (pathname === "/favicon.ico") return Response.redirect(new URL("/dispatch/${dispatchFaviconAsset}", request.url).toString(), 302);
|
|
422
|
-
`
|
|
423
|
-
: "";
|
|
424
|
-
const dispatchRootAliasRoutes = apps.includes("dispatch")
|
|
425
|
-
? DISPATCH_WORKSPACE_ROOT_REDIRECTS.map(
|
|
426
|
-
([from, to]) =>
|
|
427
|
-
` if (pathname === "/${from}") return Response.redirect(new URL("/dispatch/${to}" + search, request.url).toString(), 302);`,
|
|
428
|
-
).join("\n") + "\n"
|
|
429
|
-
: "";
|
|
430
|
-
const dispatchRootDynamicAliasRoutes = apps.includes("dispatch")
|
|
431
|
-
? ` if (pathname.startsWith("/apps/")) return Response.redirect(new URL("/dispatch" + pathname + search, request.url).toString(), 302);
|
|
432
|
-
`
|
|
433
|
-
: "";
|
|
434
|
-
const dispatchMountedRootRedirect = apps.includes("dispatch")
|
|
435
|
-
? ` if (pathname === "/dispatch" || pathname === "/dispatch/") return Response.redirect(new URL("/dispatch/overview" + search, request.url).toString(), 302);
|
|
436
|
-
`
|
|
437
|
-
: "";
|
|
438
|
-
|
|
439
|
-
const worker = `${imports}
|
|
440
|
-
|
|
441
|
-
function requestForMountedApp(request, basePath) {
|
|
442
|
-
const url = new URL(request.url);
|
|
443
|
-
if (url.pathname !== basePath && url.pathname !== \`\${basePath}/\`) {
|
|
444
|
-
return request;
|
|
445
|
-
}
|
|
446
|
-
url.pathname = \`\${basePath}//\`;
|
|
447
|
-
return new Request(url, request);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
export default {
|
|
451
|
-
async fetch(request, env, ctx) {
|
|
452
|
-
const { pathname, search } = new URL(request.url);
|
|
453
|
-
${dispatchRootFrameworkRoutes}${dispatchRootFaviconRoute}${dispatchRootAliasRoutes}${dispatchRootDynamicAliasRoutes}${dispatchMountedRootRedirect}${dispatch}
|
|
454
|
-
if (pathname === "/") {
|
|
455
|
-
return Response.redirect(new URL("${cloudflareRootRedirectPath(apps)}", request.url).toString(), 302);
|
|
456
|
-
}
|
|
457
|
-
return new Response("Not found", { status: 404 });
|
|
458
|
-
},
|
|
459
|
-
};
|
|
460
|
-
`;
|
|
461
|
-
fs.writeFileSync(path.join(distDir, "_worker.js"), worker);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function cloudflareRootRedirectPath(apps: string[]): string {
|
|
465
|
-
return apps.includes("dispatch") ? "/dispatch/overview" : `/${apps[0]}/`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function writeNetlifyRedirects(distDir: string, apps: string[]): void {
|
|
469
|
-
const lines: string[] = [
|
|
470
|
-
"# Generated by agent-native deploy --preset netlify",
|
|
471
|
-
"# Static app assets are stored under a safe namespace; dynamic app routes are handled by function route config.",
|
|
472
|
-
];
|
|
473
|
-
|
|
474
|
-
if (apps.includes("dispatch")) {
|
|
475
|
-
lines.push("/_agent-native/* /.netlify/functions/dispatch-server 200");
|
|
476
|
-
lines.push("/.well-known/* /.netlify/functions/dispatch-server 200");
|
|
477
|
-
const faviconAsset = dispatchRootFaviconAsset(distDir);
|
|
478
|
-
if (faviconAsset) {
|
|
479
|
-
lines.push(`/favicon.ico /dispatch/${faviconAsset} 302`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
for (const app of apps) {
|
|
484
|
-
lines.push(...netlifyAssetRedirectsFor(app, distDir));
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (apps.includes("dispatch")) {
|
|
488
|
-
lines.push("/ /dispatch/overview 302");
|
|
489
|
-
lines.push("/dispatch /dispatch/overview 302");
|
|
490
|
-
lines.push("/dispatch/ /dispatch/overview 302");
|
|
491
|
-
for (const [from, to] of DISPATCH_WORKSPACE_ROOT_REDIRECTS) {
|
|
492
|
-
lines.push(`/${from} /dispatch/${to} 302`);
|
|
493
|
-
}
|
|
494
|
-
lines.push("/apps/* /dispatch/apps/:splat 302");
|
|
495
|
-
} else {
|
|
496
|
-
lines.push(`/ /${apps[0]}/ 302`);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
fs.writeFileSync(path.join(distDir, "_redirects"), lines.join("\n") + "\n");
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function writeNetlifyHeaders(distDir: string, apps: string[]): void {
|
|
503
|
-
const blocks = apps.flatMap((app) => {
|
|
504
|
-
const staticDir = path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app);
|
|
505
|
-
return collectImmutableAssetPaths(staticDir).flatMap((assetPath) => [
|
|
506
|
-
netlifyHeaderBlock(`/${app}${assetPath}`),
|
|
507
|
-
netlifyHeaderBlock(`/${NETLIFY_WORKSPACE_STATIC_DIR}/${app}${assetPath}`),
|
|
508
|
-
]);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
if (blocks.length === 0) return;
|
|
512
|
-
fs.writeFileSync(path.join(distDir, "_headers"), blocks.join("\n\n") + "\n");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function netlifyHeaderBlock(pathname: string): string {
|
|
516
|
-
return [
|
|
517
|
-
pathname,
|
|
518
|
-
...Object.entries(IMMUTABLE_ASSET_CACHE_HEADERS).map(
|
|
519
|
-
([name, value]) => ` ${name}: ${value}`,
|
|
520
|
-
),
|
|
521
|
-
].join("\n");
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function writeVercelBuildConfig(outputDir: string, apps: string[]): void {
|
|
525
|
-
const routes: Array<Record<string, any>> = [
|
|
526
|
-
...vercelImmutableAssetHeaderRoutes(outputDir, apps),
|
|
527
|
-
{ handle: "filesystem" },
|
|
528
|
-
];
|
|
529
|
-
|
|
530
|
-
if (apps.includes("dispatch")) {
|
|
531
|
-
routes.push(
|
|
532
|
-
{ src: "/_agent-native", dest: "/dispatch-server" },
|
|
533
|
-
{ src: "/_agent-native/(.*)", dest: "/dispatch-server" },
|
|
534
|
-
{ src: "/\\.well-known", dest: "/dispatch-server" },
|
|
535
|
-
{ src: "/\\.well-known/(.*)", dest: "/dispatch-server" },
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
const faviconAsset = dispatchRootFaviconAsset(
|
|
539
|
-
path.join(outputDir, "static"),
|
|
540
|
-
);
|
|
541
|
-
if (faviconAsset) {
|
|
542
|
-
routes.push(vercelRedirect("/favicon.ico", `/dispatch/${faviconAsset}`));
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
routes.push(
|
|
546
|
-
vercelRedirect("/", "/dispatch/overview"),
|
|
547
|
-
vercelRedirect("/dispatch", "/dispatch/overview"),
|
|
548
|
-
vercelRedirect("/dispatch/", "/dispatch/overview"),
|
|
549
|
-
);
|
|
550
|
-
for (const [from, to] of DISPATCH_WORKSPACE_ROOT_REDIRECTS) {
|
|
551
|
-
routes.push(vercelRedirect(`/${from}`, `/dispatch/${to}`));
|
|
552
|
-
}
|
|
553
|
-
routes.push(vercelRedirect("/apps/(.*)", "/dispatch/apps/$1"));
|
|
554
|
-
} else {
|
|
555
|
-
routes.push(vercelRedirect("/", `/${apps[0]}/`));
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
for (const app of apps) {
|
|
559
|
-
if (app !== "dispatch") {
|
|
560
|
-
routes.push({ src: `/${app}`, dest: `/${app}-server` });
|
|
561
|
-
}
|
|
562
|
-
routes.push({ src: `/${app}/(.*)`, dest: `/${app}-server` });
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const config = {
|
|
566
|
-
version: 3,
|
|
567
|
-
routes,
|
|
568
|
-
};
|
|
569
|
-
fs.writeFileSync(
|
|
570
|
-
path.join(outputDir, "config.json"),
|
|
571
|
-
JSON.stringify(config, null, 2) + "\n",
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function vercelImmutableAssetHeaderRoutes(
|
|
576
|
-
outputDir: string,
|
|
577
|
-
apps: string[],
|
|
578
|
-
): Array<Record<string, any>> {
|
|
579
|
-
return apps.flatMap((app) => {
|
|
580
|
-
const staticDir = path.join(outputDir, "static", app);
|
|
581
|
-
return collectImmutableAssetPaths(staticDir).map((assetPath) => ({
|
|
582
|
-
src: vercelRouteSrc(`/${app}${assetPath}`),
|
|
583
|
-
headers: IMMUTABLE_ASSET_CACHE_HEADERS,
|
|
584
|
-
continue: true,
|
|
585
|
-
}));
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function vercelRouteSrc(pathname: string): string {
|
|
590
|
-
return pathname.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function vercelRedirect(src: string, location: string): Record<string, any> {
|
|
594
|
-
return {
|
|
595
|
-
src,
|
|
596
|
-
status: 302,
|
|
597
|
-
headers: { Location: location },
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function netlifyAssetRedirectsFor(app: string, distDir: string): string[] {
|
|
602
|
-
const from = `/${app}`;
|
|
603
|
-
const to = `/${NETLIFY_WORKSPACE_STATIC_DIR}/${app}`;
|
|
604
|
-
return [
|
|
605
|
-
`${from}/assets/* ${to}/assets/:splat 200`,
|
|
606
|
-
...netlifyPublicRootAssetPaths(
|
|
607
|
-
app,
|
|
608
|
-
path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app),
|
|
609
|
-
).map((assetPath) => {
|
|
610
|
-
const assetName = assetPath.slice(from.length + 1);
|
|
611
|
-
return `${assetPath} ${to}/${assetName} 200`;
|
|
612
|
-
}),
|
|
613
|
-
];
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function dispatchRootFaviconAsset(distDir: string): string | null {
|
|
617
|
-
for (const asset of ["favicon.ico", "favicon.svg", "favicon.png"]) {
|
|
618
|
-
if (workspaceAppAssetExists(distDir, "dispatch", asset)) return asset;
|
|
619
|
-
}
|
|
620
|
-
return null;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function workspaceAppAssetExists(
|
|
624
|
-
distDir: string,
|
|
625
|
-
app: string,
|
|
626
|
-
asset: string,
|
|
627
|
-
): boolean {
|
|
628
|
-
return [
|
|
629
|
-
path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app, asset),
|
|
630
|
-
path.join(distDir, app, app, asset),
|
|
631
|
-
path.join(distDir, app, asset),
|
|
632
|
-
].some((candidate) => fs.existsSync(candidate));
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const RESERVED_WORKSPACE_APP_IDS = new Set([
|
|
636
|
-
"_agent-native",
|
|
637
|
-
"_workspace_static",
|
|
638
|
-
"netlify",
|
|
639
|
-
...DISPATCH_WORKSPACE_ROOT_REDIRECTS.map(([from]) => from),
|
|
640
|
-
]);
|
|
641
|
-
|
|
642
|
-
function assertNoReservedWorkspaceAppIds(apps: string[]): void {
|
|
643
|
-
const conflicts = apps.filter(
|
|
644
|
-
(app) => app !== "dispatch" && RESERVED_WORKSPACE_APP_IDS.has(app),
|
|
645
|
-
);
|
|
646
|
-
if (conflicts.length === 0) return;
|
|
647
|
-
throw new Error(
|
|
648
|
-
`Workspace app id ${conflicts.map((id) => `"${id}"`).join(", ")} conflicts with reserved workspace routes. Choose a different app id.`,
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function copyNetlifyFunctionIntoWorkspace(
|
|
653
|
-
workspaceRoot: string,
|
|
654
|
-
app: string,
|
|
655
|
-
workspaceApps: WorkspaceAppManifestEntry[],
|
|
656
|
-
staticDir: string,
|
|
657
|
-
): void {
|
|
658
|
-
const appDir = path.join(workspaceRoot, "apps", app);
|
|
659
|
-
const src = path.join(appDir, ".netlify", "functions-internal", "server");
|
|
660
|
-
if (!fs.existsSync(src)) {
|
|
661
|
-
throw new Error(
|
|
662
|
-
`Expected Netlify function at ${src} after building ${app}. Check the app's build script and NITRO_PRESET.`,
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const dest = path.join(netlifyFunctionsDir(workspaceRoot), `${app}-server`);
|
|
667
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
668
|
-
copyDir(src, dest);
|
|
669
|
-
patchNetlifyFunctionEntry(dest, app, workspaceApps, staticDir);
|
|
670
|
-
|
|
671
|
-
// Durable background agent runs (default-ON; opt out with a falsy
|
|
672
|
-
// AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: when explicitly opted out
|
|
673
|
-
// this emits nothing and the single-function deploy is unchanged.
|
|
674
|
-
if (isDurableBackgroundDeployEnabled()) {
|
|
675
|
-
emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Deploy-time gate for emitting the second `-background` Netlify function. Reads
|
|
681
|
-
* the same env flag the runtime gate uses (`AGENT_CHAT_DURABLE_BACKGROUND`).
|
|
682
|
-
*
|
|
683
|
-
* DEFAULT-ON, matching the runtime gate (`isFlagEnabled` in
|
|
684
|
-
* durable-background.ts) and the single-template gate
|
|
685
|
-
* (`isDurableBackgroundDeployEnabled` in deploy/build.ts): unset/empty/unknown
|
|
686
|
-
* means enabled; an app opts OUT only with an explicit falsy value
|
|
687
|
-
* (`false`/`0`/`no`/`off`). This emits the per-app 15-min `-background` function
|
|
688
|
-
* so the chat `_process-run` dispatch lands on it with the real long budget.
|
|
689
|
-
*/
|
|
690
|
-
function isDurableBackgroundDeployEnabled(): boolean {
|
|
691
|
-
const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
|
|
692
|
-
if (raw == null) return true;
|
|
693
|
-
const v = raw.trim().toLowerCase();
|
|
694
|
-
return !(v === "0" || v === "false" || v === "no" || v === "off");
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Emit a SECOND Netlify function for `app` whose name ends in `-background`,
|
|
699
|
-
* re-exporting the SAME `main.mjs` handler bundle. Netlify invokes any function
|
|
700
|
-
* with `config.background: true` asynchronously (202 immediately, up to 15-min
|
|
701
|
-
* budget), which is exactly what the durable-background chat dispatch
|
|
702
|
-
* (`fireInternalDispatch` → the function's default url) needs.
|
|
703
|
-
*
|
|
704
|
-
* DOC-CORRECT DEFAULT-URL APPROACH (mirrors the single-template emit in
|
|
705
|
-
* deploy/build.ts): the function declares NO custom `config.path`, so it keeps
|
|
706
|
-
* its DEFAULT url `/.netlify/functions/<app>-agent-background`. The `<app>-server`
|
|
707
|
-
* function's catch-all already excludes `/.netlify/*`, so that default-url
|
|
708
|
-
* namespace is NEVER shadowed by the synchronous function — no overlapping
|
|
709
|
-
* `config.path` and no catch-all patch are needed. The foreground dispatches to
|
|
710
|
-
* that default url (`resolveAgentChatProcessRunDispatchPath` resolves the per-app
|
|
711
|
-
* name from `AGENT_NATIVE_WORKSPACE_APP_ID`); `fireInternalDispatch` strips the
|
|
712
|
-
* app base path for `/.netlify/*` targets so the request reaches the host-root
|
|
713
|
-
* function url. The entry then REWRITES the incoming pathname to the
|
|
714
|
-
* base-path-prefixed `_process-run` route before delegating to the Nitro router.
|
|
715
|
-
*
|
|
716
|
-
* It shares the same bundle (`includedFiles: ["**"]`) so `A2A_SECRET`, the DB
|
|
717
|
-
* URL, and the rest of the env/bundle are present. A prior attempt gave the
|
|
718
|
-
* function a custom `config.path` (the framework route) that overlapped the
|
|
719
|
-
* synchronous `<app>-server` catch-all; that path was not honored as a route in
|
|
720
|
-
* prod (probe → 404). The default url is the doc-correct fix.
|
|
721
|
-
*
|
|
722
|
-
* Safety net: if the dispatch fast-fails the foreground degrades to an inline
|
|
723
|
-
* 40s synchronous run (see production-agent.ts).
|
|
724
|
-
*/
|
|
725
|
-
function emitNetlifyBackgroundFunction(
|
|
726
|
-
workspaceRoot: string,
|
|
727
|
-
app: string,
|
|
728
|
-
srcServerDir: string,
|
|
729
|
-
workspaceApps: WorkspaceAppManifestEntry[],
|
|
730
|
-
): void {
|
|
731
|
-
// Name MUST end in `-background` (Netlify async convention + the runtime guard
|
|
732
|
-
// reads the -background Lambda-name suffix as a fallback). It is reached at its
|
|
733
|
-
// DEFAULT url /.netlify/functions/<app>-agent-background.
|
|
734
|
-
const backgroundName = `${app}-agent-background`;
|
|
735
|
-
const dest = path.join(netlifyFunctionsDir(workspaceRoot), backgroundName);
|
|
736
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
737
|
-
copyDir(srcServerDir, dest);
|
|
738
|
-
|
|
739
|
-
const basePath = `/${app}`;
|
|
740
|
-
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
741
|
-
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
742
|
-
workspaceApps,
|
|
743
|
-
app,
|
|
744
|
-
);
|
|
745
|
-
// The Nitro router for this app expects the base-path-prefixed framework route.
|
|
746
|
-
// The function is reached at its default url, so the entry rewrites the
|
|
747
|
-
// incoming pathname to `/<app>/_agent-native/agent-chat/_process-run`.
|
|
748
|
-
const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
|
|
749
|
-
const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
|
|
750
|
-
// is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
|
|
751
|
-
// function (the deployed Lambda name is not guaranteed to end in -background). A
|
|
752
|
-
// globalThis flag (NOT process.env) avoids the no-env-mutation guard and carries
|
|
753
|
-
// no cross-request state.
|
|
754
|
-
globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
755
|
-
|
|
756
|
-
const basePath = ${JSON.stringify(basePath)};
|
|
757
|
-
// The base-path-prefixed framework route the Nitro router dispatches to.
|
|
758
|
-
const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
|
|
759
|
-
|
|
760
|
-
function setBasePathEnv() {
|
|
761
|
-
const processRef = globalThis.process ??= { env: {} };
|
|
762
|
-
processRef.env ??= {};
|
|
763
|
-
Object.assign(processRef.env, {
|
|
764
|
-
AGENT_NATIVE_WORKSPACE: "1",
|
|
765
|
-
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
766
|
-
APP_BASE_PATH: basePath,
|
|
767
|
-
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
768
|
-
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
769
|
-
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
770
|
-
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
771
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
772
|
-
VITE_APP_BASE_PATH: basePath,
|
|
773
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
774
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
775
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
776
|
-
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
777
|
-
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
setBasePathEnv();
|
|
782
|
-
|
|
783
|
-
let cachedHandler;
|
|
784
|
-
|
|
785
|
-
// Reached at the DEFAULT url /.netlify/functions/${backgroundName}; REWRITE the
|
|
786
|
-
// incoming pathname to the base-path-prefixed _process-run route so the Nitro
|
|
787
|
-
// router runs the plugin. Method, ALL headers (the HMAC Authorization: Bearer
|
|
788
|
-
// MUST survive) and the body are preserved.
|
|
789
|
-
export default async function handler(request) {
|
|
790
|
-
setBasePathEnv();
|
|
791
|
-
cachedHandler ??= (await import("./main.mjs")).default;
|
|
792
|
-
const url = new URL(request.url);
|
|
793
|
-
url.pathname = PROCESS_RUN_PATH;
|
|
794
|
-
const method = request.method || "POST";
|
|
795
|
-
const hasBody = method !== "GET" && method !== "HEAD";
|
|
796
|
-
const body = hasBody ? await request.text() : undefined;
|
|
797
|
-
const rewritten = new Request(url.toString(), {
|
|
798
|
-
method,
|
|
799
|
-
headers: request.headers,
|
|
800
|
-
body,
|
|
801
|
-
});
|
|
802
|
-
return cachedHandler(rewritten);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
export const config = {
|
|
806
|
-
name: ${JSON.stringify(`${app} agent background handler`)},
|
|
807
|
-
generator: "agent-native workspace deploy",
|
|
808
|
-
// background: true → async invoke (202, 15-min budget). NO custom path: the
|
|
809
|
-
// function keeps its default url /.netlify/functions/${backgroundName}, which
|
|
810
|
-
// the <app>-server catch-all never shadows (it excludes /.netlify/*).
|
|
811
|
-
background: true,
|
|
812
|
-
nodeBundler: "none",
|
|
813
|
-
includedFiles: ["**"],
|
|
814
|
-
preferStatic: false,
|
|
815
|
-
};
|
|
816
|
-
`;
|
|
817
|
-
// Remove the original Nitro entry (server.mjs) so only our background entry
|
|
818
|
-
// is the function entrypoint, mirroring patchNetlifyFunctionEntry.
|
|
819
|
-
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
820
|
-
fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), server);
|
|
821
|
-
console.log(
|
|
822
|
-
`[workspace-deploy] Emitted durable-background function "${backgroundName}" ` +
|
|
823
|
-
`for app "${app}" with config { background:true } and NO custom path — ` +
|
|
824
|
-
`reachable at its default url /.netlify/functions/${backgroundName} ` +
|
|
825
|
-
`(rewrites to ${processRunPath}). REQUIRES real-deploy verification of ` +
|
|
826
|
-
`Netlify async (202) invocation — see docs/design/durable-agent-runs.md.`,
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function patchNetlifyFunctionEntry(
|
|
831
|
-
functionDir: string,
|
|
832
|
-
app: string,
|
|
833
|
-
workspaceApps: WorkspaceAppManifestEntry[],
|
|
834
|
-
staticDir: string,
|
|
835
|
-
): void {
|
|
836
|
-
const serverPath = path.join(functionDir, "server.mjs");
|
|
837
|
-
if (!fs.existsSync(serverPath)) return;
|
|
838
|
-
|
|
839
|
-
const basePath = `/${app}`;
|
|
840
|
-
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
841
|
-
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
842
|
-
workspaceApps,
|
|
843
|
-
app,
|
|
844
|
-
);
|
|
845
|
-
const pathConfig =
|
|
846
|
-
app === "dispatch"
|
|
847
|
-
? ["/_agent-native/*", "/.well-known/*", `${basePath}/*`]
|
|
848
|
-
: [basePath, `${basePath}/*`];
|
|
849
|
-
const normalizeBasePathHelper =
|
|
850
|
-
app === "dispatch"
|
|
851
|
-
? ""
|
|
852
|
-
: `
|
|
853
|
-
function normalizeBasePathArgs(args) {
|
|
854
|
-
const request = args[0];
|
|
855
|
-
if (!request || typeof request.url !== "string" || typeof Request !== "function") {
|
|
856
|
-
return args;
|
|
857
|
-
}
|
|
858
|
-
const url = new URL(request.url);
|
|
859
|
-
if (url.pathname === basePath || url.pathname === \`\${basePath}/\`) {
|
|
860
|
-
url.pathname = \`\${basePath}//\`;
|
|
861
|
-
return [new Request(url, request), ...args.slice(1)];
|
|
862
|
-
}
|
|
863
|
-
return args;
|
|
864
|
-
}
|
|
865
|
-
`;
|
|
866
|
-
const handlerArgs =
|
|
867
|
-
app === "dispatch" ? "...args" : "...normalizeBasePathArgs(args)";
|
|
868
|
-
const server = `const basePath = ${JSON.stringify(basePath)};
|
|
869
|
-
|
|
870
|
-
function setBasePathEnv() {
|
|
871
|
-
const processRef = globalThis.process ??= { env: {} };
|
|
872
|
-
processRef.env ??= {};
|
|
873
|
-
Object.assign(processRef.env, {
|
|
874
|
-
AGENT_NATIVE_WORKSPACE: "1",
|
|
875
|
-
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
876
|
-
APP_BASE_PATH: basePath,
|
|
877
|
-
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
878
|
-
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
879
|
-
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
880
|
-
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
881
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
882
|
-
VITE_APP_BASE_PATH: basePath,
|
|
883
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
884
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
885
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
886
|
-
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
887
|
-
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
setBasePathEnv();
|
|
892
|
-
${normalizeBasePathHelper}
|
|
893
|
-
|
|
894
|
-
let cachedHandler;
|
|
895
|
-
|
|
896
|
-
export default async function handler(...args) {
|
|
897
|
-
setBasePathEnv();
|
|
898
|
-
cachedHandler ??= (await import("./main.mjs")).default;
|
|
899
|
-
return cachedHandler(${handlerArgs});
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
export const config = {
|
|
903
|
-
name: ${JSON.stringify(`${app} server handler`)},
|
|
904
|
-
generator: "agent-native workspace deploy",
|
|
905
|
-
path: ${JSON.stringify(pathConfig)},
|
|
906
|
-
nodeBundler: "none",
|
|
907
|
-
includedFiles: ["**"],
|
|
908
|
-
excludedPath: ${JSON.stringify(
|
|
909
|
-
netlifyFunctionExcludedPaths(app, staticDir),
|
|
910
|
-
null,
|
|
911
|
-
2,
|
|
912
|
-
)
|
|
913
|
-
.split("\n")
|
|
914
|
-
.join("\n ")},
|
|
915
|
-
preferStatic: false,
|
|
916
|
-
};
|
|
917
|
-
`;
|
|
918
|
-
fs.rmSync(serverPath, { force: true });
|
|
919
|
-
fs.writeFileSync(path.join(functionDir, `${app}-server.mjs`), server);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function patchVercelFunctionEntry(
|
|
923
|
-
functionDir: string,
|
|
924
|
-
app: string,
|
|
925
|
-
workspaceApps: WorkspaceAppManifestEntry[],
|
|
926
|
-
): void {
|
|
927
|
-
const entryPath = path.join(functionDir, "index.mjs");
|
|
928
|
-
if (!fs.existsSync(entryPath)) return;
|
|
929
|
-
|
|
930
|
-
const mainPath = path.join(functionDir, "main.mjs");
|
|
931
|
-
fs.rmSync(mainPath, { force: true });
|
|
932
|
-
fs.renameSync(entryPath, mainPath);
|
|
933
|
-
|
|
934
|
-
const basePath = `/${app}`;
|
|
935
|
-
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
936
|
-
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
937
|
-
workspaceApps,
|
|
938
|
-
app,
|
|
939
|
-
);
|
|
940
|
-
const entry = `const basePath = ${JSON.stringify(basePath)};
|
|
941
|
-
|
|
942
|
-
function setBasePathEnv() {
|
|
943
|
-
const processRef = globalThis.process ??= { env: {} };
|
|
944
|
-
processRef.env ??= {};
|
|
945
|
-
Object.assign(processRef.env, {
|
|
946
|
-
AGENT_NATIVE_WORKSPACE: "1",
|
|
947
|
-
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
948
|
-
APP_BASE_PATH: basePath,
|
|
949
|
-
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
950
|
-
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
951
|
-
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
952
|
-
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
953
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
954
|
-
VITE_APP_BASE_PATH: basePath,
|
|
955
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
956
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
957
|
-
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
958
|
-
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
959
|
-
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function normalizeBasePathArgs(args) {
|
|
964
|
-
const request = args[0];
|
|
965
|
-
if (!request) return args;
|
|
966
|
-
|
|
967
|
-
if (typeof Request === "function" && request instanceof Request) {
|
|
968
|
-
const url = new URL(request.url);
|
|
969
|
-
if (url.pathname === basePath || url.pathname === \`\${basePath}/\`) {
|
|
970
|
-
url.pathname = \`\${basePath}//\`;
|
|
971
|
-
return [new Request(url, request), ...args.slice(1)];
|
|
972
|
-
}
|
|
973
|
-
return args;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
if (typeof request.url !== "string") return args;
|
|
977
|
-
const url = new URL(request.url, "http://agent-native.local");
|
|
978
|
-
if (url.pathname === basePath || url.pathname === \`\${basePath}/\`) {
|
|
979
|
-
request.url = \`\${basePath}//\${url.search}\`;
|
|
980
|
-
}
|
|
981
|
-
return args;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
setBasePathEnv();
|
|
985
|
-
|
|
986
|
-
let cachedHandler;
|
|
987
|
-
|
|
988
|
-
export default async function handler(...args) {
|
|
989
|
-
setBasePathEnv();
|
|
990
|
-
cachedHandler ??= (await import("./main.mjs")).default;
|
|
991
|
-
return cachedHandler(...normalizeBasePathArgs(args));
|
|
992
|
-
}
|
|
993
|
-
`;
|
|
994
|
-
fs.writeFileSync(entryPath, entry);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function netlifyFunctionExcludedPaths(
|
|
998
|
-
app: string,
|
|
999
|
-
staticDir: string,
|
|
1000
|
-
): string[] {
|
|
1001
|
-
return [
|
|
1002
|
-
"/.netlify/*",
|
|
1003
|
-
`/${app}/assets/*`,
|
|
1004
|
-
...netlifyPublicRootAssetPaths(app, staticDir),
|
|
1005
|
-
];
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function netlifyPublicRootAssetPaths(app: string, staticDir: string): string[] {
|
|
1009
|
-
if (!fs.existsSync(staticDir)) return [];
|
|
1010
|
-
return fs
|
|
1011
|
-
.readdirSync(staticDir, { withFileTypes: true })
|
|
1012
|
-
.filter((entry) => entry.isFile())
|
|
1013
|
-
.map((entry) => entry.name)
|
|
1014
|
-
.filter((name) => {
|
|
1015
|
-
const ext = path.extname(name).slice(1).toLowerCase();
|
|
1016
|
-
return NETLIFY_PUBLIC_ASSET_EXTENSIONS.has(ext);
|
|
1017
|
-
})
|
|
1018
|
-
.sort()
|
|
1019
|
-
.map((name) => `/${app}/${encodeURI(name)}`);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function netlifyFunctionsDir(workspaceRoot: string): string {
|
|
1023
|
-
return path.join(workspaceRoot, ".netlify", "functions-internal");
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
function cleanAppBuildOutputs(appDir: string): void {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
})
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
)
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
)
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
)
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
process.env.
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
normalizeOrigin(process.env.
|
|
1285
|
-
(
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
)
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
if (value
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
app
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `agent-native deploy` — build and deploy every app in a workspace to a
|
|
3
|
+
* single origin. Each app is served from `/<app-name>/*`, so:
|
|
4
|
+
*
|
|
5
|
+
* https://your-agents.com/mail/* → apps/mail
|
|
6
|
+
* https://your-agents.com/calendar/* → apps/calendar
|
|
7
|
+
*
|
|
8
|
+
* Benefits of same-origin deploy:
|
|
9
|
+
* - Shared auth cookie → log in once, every app is signed in
|
|
10
|
+
* - Cross-app A2A is a same-origin fetch (no CORS, no JWT for siblings)
|
|
11
|
+
* - One DNS record, one TLS cert, one CDN cache
|
|
12
|
+
*
|
|
13
|
+
* Per-app independent deploy is still supported — just cd into the app and
|
|
14
|
+
* run `agent-native build` as before. This orchestrator is for teams that
|
|
15
|
+
* want the whole workspace behind one domain.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from "child_process";
|
|
18
|
+
import fs from "fs";
|
|
19
|
+
import path from "path";
|
|
20
|
+
|
|
21
|
+
import { AGENT_CHAT_PROCESS_RUN_PATH } from "../agent/durable-background.js";
|
|
22
|
+
import { findWorkspaceRoot } from "../scripts/utils.js";
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
25
|
+
normalizeWorkspaceAppAudience,
|
|
26
|
+
normalizeWorkspaceAppPathList,
|
|
27
|
+
workspaceAppAudienceFromPackageJson,
|
|
28
|
+
workspaceAppRouteAccessFromPackageJson,
|
|
29
|
+
type WorkspaceAppRouteAccess,
|
|
30
|
+
type WorkspaceAppAudience,
|
|
31
|
+
} from "../shared/workspace-app-audience.js";
|
|
32
|
+
import { DISPATCH_WORKSPACE_ROOT_REDIRECTS } from "../shared/workspace-app-id.js";
|
|
33
|
+
import {
|
|
34
|
+
collectImmutableAssetPaths,
|
|
35
|
+
IMMUTABLE_ASSET_CACHE_HEADERS,
|
|
36
|
+
} from "./immutable-assets.js";
|
|
37
|
+
|
|
38
|
+
export type WorkspaceDeployPreset = "cloudflare_pages" | "netlify" | "vercel";
|
|
39
|
+
|
|
40
|
+
const NETLIFY_WORKSPACE_STATIC_DIR = "_workspace_static";
|
|
41
|
+
const NETLIFY_PUBLIC_ASSET_EXTENSIONS = new Set([
|
|
42
|
+
"avif",
|
|
43
|
+
"css",
|
|
44
|
+
"gif",
|
|
45
|
+
"ico",
|
|
46
|
+
"jpeg",
|
|
47
|
+
"jpg",
|
|
48
|
+
"js",
|
|
49
|
+
"json",
|
|
50
|
+
"map",
|
|
51
|
+
"mp4",
|
|
52
|
+
"pdf",
|
|
53
|
+
"png",
|
|
54
|
+
"svg",
|
|
55
|
+
"txt",
|
|
56
|
+
"wasm",
|
|
57
|
+
"webm",
|
|
58
|
+
"webmanifest",
|
|
59
|
+
"webp",
|
|
60
|
+
"xml",
|
|
61
|
+
]);
|
|
62
|
+
const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
|
|
63
|
+
const WORKSPACE_APPS_MANIFEST_DIR = ".agent-native";
|
|
64
|
+
const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
|
|
65
|
+
const VERCEL_OUTPUT_DIR = ".vercel/output";
|
|
66
|
+
|
|
67
|
+
interface WorkspaceAppManifestEntry {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
description: string;
|
|
71
|
+
path: string;
|
|
72
|
+
url?: string;
|
|
73
|
+
isDispatch: boolean;
|
|
74
|
+
audience: WorkspaceAppAudience;
|
|
75
|
+
publicPaths: string[];
|
|
76
|
+
protectedPaths: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface WorkspaceAppManifestOverride {
|
|
80
|
+
id: string;
|
|
81
|
+
url?: string;
|
|
82
|
+
audience?: WorkspaceAppAudience;
|
|
83
|
+
publicPaths?: string[];
|
|
84
|
+
protectedPaths?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WorkspaceDeployOptions {
|
|
88
|
+
args?: string[];
|
|
89
|
+
/** Override the workspace root (defaults to walking up from cwd). */
|
|
90
|
+
workspaceRoot?: string;
|
|
91
|
+
/** Only build — don't invoke the deploy platform CLI. */
|
|
92
|
+
buildOnly?: boolean;
|
|
93
|
+
/** Target preset. Defaults to `cloudflare_pages`. */
|
|
94
|
+
preset?: WorkspaceDeployPreset;
|
|
95
|
+
/** @internal Override process execution in tests. */
|
|
96
|
+
execFile?: typeof execFileSync;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function runWorkspaceDeploy(
|
|
100
|
+
opts: WorkspaceDeployOptions = {},
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const workspaceRoot =
|
|
103
|
+
opts.workspaceRoot ?? findWorkspaceRoot(process.cwd()) ?? process.cwd();
|
|
104
|
+
const appsDir = path.join(workspaceRoot, "apps");
|
|
105
|
+
if (!fs.existsSync(appsDir)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`No apps/ directory found at ${workspaceRoot}. Run this inside an agent-native workspace.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rawArgs = opts.args ?? [];
|
|
112
|
+
const args = new Set(rawArgs);
|
|
113
|
+
const buildOnly = opts.buildOnly ?? args.has("--build-only");
|
|
114
|
+
|
|
115
|
+
const apps = fs
|
|
116
|
+
.readdirSync(appsDir, { withFileTypes: true })
|
|
117
|
+
.filter((e) => e.isDirectory())
|
|
118
|
+
.map((e) => e.name)
|
|
119
|
+
.filter((n) => fs.existsSync(path.join(appsDir, n, "package.json")))
|
|
120
|
+
.sort(compareWorkspaceAppIds);
|
|
121
|
+
|
|
122
|
+
if (apps.length === 0) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Workspace has no apps. Run \`agent-native add-app\` to add one.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
assertNoReservedWorkspaceAppIds(apps);
|
|
128
|
+
const workspaceApps = readWorkspaceAppManifest(workspaceRoot, apps);
|
|
129
|
+
|
|
130
|
+
const preset = resolvePreset(opts.preset, rawArgs);
|
|
131
|
+
assertWorkspaceDeployProductionEnv({ buildOnly, preset });
|
|
132
|
+
const distDir = path.join(workspaceRoot, "dist");
|
|
133
|
+
const vercelOutputDir = path.join(workspaceRoot, VERCEL_OUTPUT_DIR);
|
|
134
|
+
if (preset === "vercel") {
|
|
135
|
+
fs.rmSync(vercelOutputDir, { recursive: true, force: true });
|
|
136
|
+
fs.mkdirSync(path.join(vercelOutputDir, "static"), { recursive: true });
|
|
137
|
+
fs.mkdirSync(path.join(vercelOutputDir, "functions"), {
|
|
138
|
+
recursive: true,
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
142
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (preset === "netlify") {
|
|
146
|
+
const functionsDir = netlifyFunctionsDir(workspaceRoot);
|
|
147
|
+
fs.rmSync(functionsDir, { recursive: true, force: true });
|
|
148
|
+
fs.mkdirSync(functionsDir, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
`[workspace-deploy] Building ${apps.length} app(s) for preset=${preset}`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const execFile = opts.execFile ?? execFileSync;
|
|
156
|
+
for (const app of apps) {
|
|
157
|
+
buildOneApp(workspaceRoot, app, preset, execFile, workspaceApps);
|
|
158
|
+
moveAppBuildIntoWorkspaceOutput(
|
|
159
|
+
workspaceRoot,
|
|
160
|
+
app,
|
|
161
|
+
preset,
|
|
162
|
+
distDir,
|
|
163
|
+
vercelOutputDir,
|
|
164
|
+
workspaceApps,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
writeWorkspaceAppManifests(
|
|
168
|
+
workspaceRoot,
|
|
169
|
+
distDir,
|
|
170
|
+
apps,
|
|
171
|
+
workspaceApps,
|
|
172
|
+
preset,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (preset === "netlify") {
|
|
176
|
+
writeNetlifyRedirects(distDir, apps);
|
|
177
|
+
writeNetlifyHeaders(distDir, apps);
|
|
178
|
+
} else if (preset === "vercel") {
|
|
179
|
+
writeVercelBuildConfig(vercelOutputDir, apps);
|
|
180
|
+
} else {
|
|
181
|
+
writeCloudflareRoutingManifest(distDir, apps);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (buildOnly) {
|
|
185
|
+
const outputDir = preset === "vercel" ? vercelOutputDir : distDir;
|
|
186
|
+
console.log(
|
|
187
|
+
`\n[workspace-deploy] Build complete at ${outputDir}. Skipping publish (--build-only).`,
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(`\n[workspace-deploy] Build complete. Publish with:\n`);
|
|
193
|
+
console.log(` cd ${path.relative(process.cwd(), workspaceRoot) || "."}`);
|
|
194
|
+
if (preset === "netlify") {
|
|
195
|
+
console.log(
|
|
196
|
+
` netlify deploy --prod --dir=dist --functions=.netlify/functions-internal\n`,
|
|
197
|
+
);
|
|
198
|
+
} else if (preset === "vercel") {
|
|
199
|
+
console.log(` vercel deploy --prebuilt\n`);
|
|
200
|
+
} else {
|
|
201
|
+
console.log(` wrangler pages deploy dist\n`);
|
|
202
|
+
}
|
|
203
|
+
console.log(
|
|
204
|
+
`All apps live at https://<origin>/<app-name>/*. Log in once on any app\nand the session is shared across the workspace.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildOneApp(
|
|
209
|
+
workspaceRoot: string,
|
|
210
|
+
app: string,
|
|
211
|
+
preset: WorkspaceDeployPreset,
|
|
212
|
+
execFile: typeof execFileSync,
|
|
213
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
214
|
+
): void {
|
|
215
|
+
const appDir = path.join(workspaceRoot, "apps", app);
|
|
216
|
+
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
217
|
+
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
218
|
+
workspaceApps,
|
|
219
|
+
app,
|
|
220
|
+
);
|
|
221
|
+
const workspaceGatewayUrl =
|
|
222
|
+
process.env.VITE_WORKSPACE_GATEWAY_URL || workspaceBaseUrl();
|
|
223
|
+
const workspaceOAuthUrl = workspaceOAuthOrigin(workspaceGatewayUrl);
|
|
224
|
+
const env: NodeJS.ProcessEnv = {
|
|
225
|
+
...process.env,
|
|
226
|
+
NITRO_PRESET: preset,
|
|
227
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
228
|
+
AGENT_NATIVE_WORKSPACE_APP_ID: app,
|
|
229
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
230
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: app,
|
|
231
|
+
APP_BASE_PATH: `/${app}`,
|
|
232
|
+
VITE_APP_BASE_PATH: `/${app}`,
|
|
233
|
+
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: workspaceAppAudience,
|
|
234
|
+
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: JSON.stringify(
|
|
235
|
+
workspaceAppRouteAccess.publicPaths,
|
|
236
|
+
),
|
|
237
|
+
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: JSON.stringify(
|
|
238
|
+
workspaceAppRouteAccess.protectedPaths,
|
|
239
|
+
),
|
|
240
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: workspaceAppAudience,
|
|
241
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: JSON.stringify(
|
|
242
|
+
workspaceAppRouteAccess.publicPaths,
|
|
243
|
+
),
|
|
244
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: JSON.stringify(
|
|
245
|
+
workspaceAppRouteAccess.protectedPaths,
|
|
246
|
+
),
|
|
247
|
+
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: JSON.stringify(workspaceApps),
|
|
248
|
+
...(workspaceGatewayUrl
|
|
249
|
+
? {
|
|
250
|
+
WORKSPACE_GATEWAY_URL:
|
|
251
|
+
process.env.WORKSPACE_GATEWAY_URL || workspaceGatewayUrl,
|
|
252
|
+
VITE_WORKSPACE_GATEWAY_URL: workspaceGatewayUrl,
|
|
253
|
+
...(workspaceOAuthUrl
|
|
254
|
+
? { VITE_WORKSPACE_OAUTH_ORIGIN: workspaceOAuthUrl }
|
|
255
|
+
: {}),
|
|
256
|
+
}
|
|
257
|
+
: {}),
|
|
258
|
+
[WORKSPACE_APPS_ENV_KEY]: JSON.stringify(workspaceApps),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (preset === "netlify" && appUsesNetlifyUnpooledDatabaseUrl(appDir)) {
|
|
262
|
+
env.DATABASE_URL =
|
|
263
|
+
process.env.NETLIFY_DATABASE_URL_UNPOOLED ??
|
|
264
|
+
process.env.DATABASE_URL ??
|
|
265
|
+
env.DATABASE_URL;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(
|
|
269
|
+
`[workspace-deploy] Building ${app} (base=/${app}, preset=${preset})`,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
cleanAppBuildOutputs(appDir);
|
|
273
|
+
|
|
274
|
+
execFile("pnpm", ["--filter", app, "build"], {
|
|
275
|
+
cwd: workspaceRoot,
|
|
276
|
+
env,
|
|
277
|
+
stdio: "inherit",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function moveAppBuildIntoWorkspaceOutput(
|
|
282
|
+
workspaceRoot: string,
|
|
283
|
+
app: string,
|
|
284
|
+
preset: WorkspaceDeployPreset,
|
|
285
|
+
distDir: string,
|
|
286
|
+
vercelOutputDir: string,
|
|
287
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
288
|
+
): void {
|
|
289
|
+
const appDir = path.join(workspaceRoot, "apps", app);
|
|
290
|
+
if (preset === "vercel") {
|
|
291
|
+
copyVercelAppBuildIntoWorkspace(
|
|
292
|
+
workspaceRoot,
|
|
293
|
+
app,
|
|
294
|
+
vercelOutputDir,
|
|
295
|
+
workspaceApps,
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Resolve the per-app build output: prefer dist/ (standard), fall back to
|
|
301
|
+
// .output/ (Nitro's default). The Cloudflare preset emits into dist/
|
|
302
|
+
// containing the worker + assets.
|
|
303
|
+
const candidates = ["dist", ".output"];
|
|
304
|
+
const src = candidates
|
|
305
|
+
.map((c) => path.join(appDir, c))
|
|
306
|
+
.find((p) => fs.existsSync(p));
|
|
307
|
+
if (!src) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Expected ${candidates.join(" or ")} under ${appDir} but none existed. Check the app's build script.`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (preset === "netlify") {
|
|
313
|
+
const mountedSrc = path.join(src, app);
|
|
314
|
+
const staticSrc = fs.existsSync(mountedSrc) ? mountedSrc : src;
|
|
315
|
+
const target = path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app);
|
|
316
|
+
fs.mkdirSync(target, { recursive: true });
|
|
317
|
+
copyDir(staticSrc, target);
|
|
318
|
+
// Nitro/Vite mounted builds can contain a nested copy of public assets at
|
|
319
|
+
// dist/<app>/<app>/...; the workspace root already supplies the outer
|
|
320
|
+
// mount path, so keeping it would publish duplicate /<app>/<app> URLs.
|
|
321
|
+
fs.rmSync(path.join(target, app), { recursive: true, force: true });
|
|
322
|
+
copyNetlifyFunctionIntoWorkspace(workspaceRoot, app, workspaceApps, target);
|
|
323
|
+
} else {
|
|
324
|
+
const target = path.join(distDir, app);
|
|
325
|
+
fs.mkdirSync(target, { recursive: true });
|
|
326
|
+
copyDir(src, target);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function copyVercelAppBuildIntoWorkspace(
|
|
331
|
+
workspaceRoot: string,
|
|
332
|
+
app: string,
|
|
333
|
+
vercelOutputDir: string,
|
|
334
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
335
|
+
): void {
|
|
336
|
+
const appDir = path.join(workspaceRoot, "apps", app);
|
|
337
|
+
const src = path.join(appDir, VERCEL_OUTPUT_DIR);
|
|
338
|
+
if (!fs.existsSync(src)) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Expected Vercel output at ${src} after building ${app}. Check the app's build script and NITRO_PRESET.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const staticSrc = path.join(src, "static");
|
|
345
|
+
const staticDest = path.join(vercelOutputDir, "static");
|
|
346
|
+
if (fs.existsSync(staticSrc)) {
|
|
347
|
+
copyDir(staticSrc, staticDest);
|
|
348
|
+
// Nitro's Vercel preset already nests assets under baseURL. The shared
|
|
349
|
+
// deploy build also mirrors client assets under baseURL for other Nitro
|
|
350
|
+
// presets, so mounted apps can contain a duplicate /<app>/<app> copy.
|
|
351
|
+
fs.rmSync(path.join(staticDest, app, app), {
|
|
352
|
+
recursive: true,
|
|
353
|
+
force: true,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const functionSrc = path.join(src, "functions", "__server.func");
|
|
358
|
+
if (!fs.existsSync(functionSrc)) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Expected Vercel function at ${functionSrc} after building ${app}. Check the app's build script and NITRO_PRESET.`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const functionDest = path.join(
|
|
365
|
+
vercelOutputDir,
|
|
366
|
+
"functions",
|
|
367
|
+
`${app}-server.func`,
|
|
368
|
+
);
|
|
369
|
+
fs.rmSync(functionDest, { recursive: true, force: true });
|
|
370
|
+
copyDir(functionSrc, functionDest);
|
|
371
|
+
patchVercelFunctionEntry(functionDest, app, workspaceApps);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Write the Cloudflare Pages `_routes.json` and a dispatcher `_worker.js` at
|
|
376
|
+
* the workspace dist root so each app is reachable under /<app>/*.
|
|
377
|
+
*/
|
|
378
|
+
function writeCloudflareRoutingManifest(distDir: string, apps: string[]): void {
|
|
379
|
+
const dispatchFaviconAsset = apps.includes("dispatch")
|
|
380
|
+
? dispatchRootFaviconAsset(distDir)
|
|
381
|
+
: null;
|
|
382
|
+
// _routes.json tells Cloudflare which paths are dynamic (Functions) vs
|
|
383
|
+
// static. Mark both /<app> and /<app>/* as include so every app's worker
|
|
384
|
+
// handles its root and subtree.
|
|
385
|
+
const include = apps.flatMap((a) => [`/${a}`, `/${a}/*`]).concat(["/"]);
|
|
386
|
+
if (apps.includes("dispatch")) {
|
|
387
|
+
include.push("/_agent-native/*");
|
|
388
|
+
include.push("/.well-known/*");
|
|
389
|
+
include.push(
|
|
390
|
+
...DISPATCH_WORKSPACE_ROOT_REDIRECTS.map(([from]) => `/${from}`),
|
|
391
|
+
);
|
|
392
|
+
include.push("/apps/*");
|
|
393
|
+
if (dispatchFaviconAsset) include.push("/favicon.ico");
|
|
394
|
+
}
|
|
395
|
+
const routes = {
|
|
396
|
+
version: 1,
|
|
397
|
+
include,
|
|
398
|
+
exclude: [],
|
|
399
|
+
};
|
|
400
|
+
fs.writeFileSync(
|
|
401
|
+
path.join(distDir, "_routes.json"),
|
|
402
|
+
JSON.stringify(routes, null, 2) + "\n",
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Dispatcher worker: inspects the path and forwards to the matching
|
|
406
|
+
// per-app worker.
|
|
407
|
+
const imports = apps
|
|
408
|
+
.map((a) => `import ${moduleIdent(a)} from "./${a}/_worker.js";`)
|
|
409
|
+
.join("\n");
|
|
410
|
+
const dispatch = apps
|
|
411
|
+
.map(
|
|
412
|
+
(a) =>
|
|
413
|
+
` if (pathname === "/${a}" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
|
|
414
|
+
)
|
|
415
|
+
.join("\n");
|
|
416
|
+
const dispatchRootFrameworkRoutes = apps.includes("dispatch")
|
|
417
|
+
? ` if (pathname === "/_agent-native" || pathname.startsWith("/_agent-native/") || pathname === "/.well-known" || pathname.startsWith("/.well-known/")) return ${moduleIdent("dispatch")}.fetch(request, env, ctx);
|
|
418
|
+
`
|
|
419
|
+
: "";
|
|
420
|
+
const dispatchRootFaviconRoute = dispatchFaviconAsset
|
|
421
|
+
? ` if (pathname === "/favicon.ico") return Response.redirect(new URL("/dispatch/${dispatchFaviconAsset}", request.url).toString(), 302);
|
|
422
|
+
`
|
|
423
|
+
: "";
|
|
424
|
+
const dispatchRootAliasRoutes = apps.includes("dispatch")
|
|
425
|
+
? DISPATCH_WORKSPACE_ROOT_REDIRECTS.map(
|
|
426
|
+
([from, to]) =>
|
|
427
|
+
` if (pathname === "/${from}") return Response.redirect(new URL("/dispatch/${to}" + search, request.url).toString(), 302);`,
|
|
428
|
+
).join("\n") + "\n"
|
|
429
|
+
: "";
|
|
430
|
+
const dispatchRootDynamicAliasRoutes = apps.includes("dispatch")
|
|
431
|
+
? ` if (pathname.startsWith("/apps/")) return Response.redirect(new URL("/dispatch" + pathname + search, request.url).toString(), 302);
|
|
432
|
+
`
|
|
433
|
+
: "";
|
|
434
|
+
const dispatchMountedRootRedirect = apps.includes("dispatch")
|
|
435
|
+
? ` if (pathname === "/dispatch" || pathname === "/dispatch/") return Response.redirect(new URL("/dispatch/overview" + search, request.url).toString(), 302);
|
|
436
|
+
`
|
|
437
|
+
: "";
|
|
438
|
+
|
|
439
|
+
const worker = `${imports}
|
|
440
|
+
|
|
441
|
+
function requestForMountedApp(request, basePath) {
|
|
442
|
+
const url = new URL(request.url);
|
|
443
|
+
if (url.pathname !== basePath && url.pathname !== \`\${basePath}/\`) {
|
|
444
|
+
return request;
|
|
445
|
+
}
|
|
446
|
+
url.pathname = \`\${basePath}//\`;
|
|
447
|
+
return new Request(url, request);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export default {
|
|
451
|
+
async fetch(request, env, ctx) {
|
|
452
|
+
const { pathname, search } = new URL(request.url);
|
|
453
|
+
${dispatchRootFrameworkRoutes}${dispatchRootFaviconRoute}${dispatchRootAliasRoutes}${dispatchRootDynamicAliasRoutes}${dispatchMountedRootRedirect}${dispatch}
|
|
454
|
+
if (pathname === "/") {
|
|
455
|
+
return Response.redirect(new URL("${cloudflareRootRedirectPath(apps)}", request.url).toString(), 302);
|
|
456
|
+
}
|
|
457
|
+
return new Response("Not found", { status: 404 });
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
`;
|
|
461
|
+
fs.writeFileSync(path.join(distDir, "_worker.js"), worker);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function cloudflareRootRedirectPath(apps: string[]): string {
|
|
465
|
+
return apps.includes("dispatch") ? "/dispatch/overview" : `/${apps[0]}/`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function writeNetlifyRedirects(distDir: string, apps: string[]): void {
|
|
469
|
+
const lines: string[] = [
|
|
470
|
+
"# Generated by agent-native deploy --preset netlify",
|
|
471
|
+
"# Static app assets are stored under a safe namespace; dynamic app routes are handled by function route config.",
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
if (apps.includes("dispatch")) {
|
|
475
|
+
lines.push("/_agent-native/* /.netlify/functions/dispatch-server 200");
|
|
476
|
+
lines.push("/.well-known/* /.netlify/functions/dispatch-server 200");
|
|
477
|
+
const faviconAsset = dispatchRootFaviconAsset(distDir);
|
|
478
|
+
if (faviconAsset) {
|
|
479
|
+
lines.push(`/favicon.ico /dispatch/${faviconAsset} 302`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for (const app of apps) {
|
|
484
|
+
lines.push(...netlifyAssetRedirectsFor(app, distDir));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (apps.includes("dispatch")) {
|
|
488
|
+
lines.push("/ /dispatch/overview 302");
|
|
489
|
+
lines.push("/dispatch /dispatch/overview 302");
|
|
490
|
+
lines.push("/dispatch/ /dispatch/overview 302");
|
|
491
|
+
for (const [from, to] of DISPATCH_WORKSPACE_ROOT_REDIRECTS) {
|
|
492
|
+
lines.push(`/${from} /dispatch/${to} 302`);
|
|
493
|
+
}
|
|
494
|
+
lines.push("/apps/* /dispatch/apps/:splat 302");
|
|
495
|
+
} else {
|
|
496
|
+
lines.push(`/ /${apps[0]}/ 302`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
fs.writeFileSync(path.join(distDir, "_redirects"), lines.join("\n") + "\n");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function writeNetlifyHeaders(distDir: string, apps: string[]): void {
|
|
503
|
+
const blocks = apps.flatMap((app) => {
|
|
504
|
+
const staticDir = path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app);
|
|
505
|
+
return collectImmutableAssetPaths(staticDir).flatMap((assetPath) => [
|
|
506
|
+
netlifyHeaderBlock(`/${app}${assetPath}`),
|
|
507
|
+
netlifyHeaderBlock(`/${NETLIFY_WORKSPACE_STATIC_DIR}/${app}${assetPath}`),
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (blocks.length === 0) return;
|
|
512
|
+
fs.writeFileSync(path.join(distDir, "_headers"), blocks.join("\n\n") + "\n");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function netlifyHeaderBlock(pathname: string): string {
|
|
516
|
+
return [
|
|
517
|
+
pathname,
|
|
518
|
+
...Object.entries(IMMUTABLE_ASSET_CACHE_HEADERS).map(
|
|
519
|
+
([name, value]) => ` ${name}: ${value}`,
|
|
520
|
+
),
|
|
521
|
+
].join("\n");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function writeVercelBuildConfig(outputDir: string, apps: string[]): void {
|
|
525
|
+
const routes: Array<Record<string, any>> = [
|
|
526
|
+
...vercelImmutableAssetHeaderRoutes(outputDir, apps),
|
|
527
|
+
{ handle: "filesystem" },
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
if (apps.includes("dispatch")) {
|
|
531
|
+
routes.push(
|
|
532
|
+
{ src: "/_agent-native", dest: "/dispatch-server" },
|
|
533
|
+
{ src: "/_agent-native/(.*)", dest: "/dispatch-server" },
|
|
534
|
+
{ src: "/\\.well-known", dest: "/dispatch-server" },
|
|
535
|
+
{ src: "/\\.well-known/(.*)", dest: "/dispatch-server" },
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const faviconAsset = dispatchRootFaviconAsset(
|
|
539
|
+
path.join(outputDir, "static"),
|
|
540
|
+
);
|
|
541
|
+
if (faviconAsset) {
|
|
542
|
+
routes.push(vercelRedirect("/favicon.ico", `/dispatch/${faviconAsset}`));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
routes.push(
|
|
546
|
+
vercelRedirect("/", "/dispatch/overview"),
|
|
547
|
+
vercelRedirect("/dispatch", "/dispatch/overview"),
|
|
548
|
+
vercelRedirect("/dispatch/", "/dispatch/overview"),
|
|
549
|
+
);
|
|
550
|
+
for (const [from, to] of DISPATCH_WORKSPACE_ROOT_REDIRECTS) {
|
|
551
|
+
routes.push(vercelRedirect(`/${from}`, `/dispatch/${to}`));
|
|
552
|
+
}
|
|
553
|
+
routes.push(vercelRedirect("/apps/(.*)", "/dispatch/apps/$1"));
|
|
554
|
+
} else {
|
|
555
|
+
routes.push(vercelRedirect("/", `/${apps[0]}/`));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
for (const app of apps) {
|
|
559
|
+
if (app !== "dispatch") {
|
|
560
|
+
routes.push({ src: `/${app}`, dest: `/${app}-server` });
|
|
561
|
+
}
|
|
562
|
+
routes.push({ src: `/${app}/(.*)`, dest: `/${app}-server` });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const config = {
|
|
566
|
+
version: 3,
|
|
567
|
+
routes,
|
|
568
|
+
};
|
|
569
|
+
fs.writeFileSync(
|
|
570
|
+
path.join(outputDir, "config.json"),
|
|
571
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function vercelImmutableAssetHeaderRoutes(
|
|
576
|
+
outputDir: string,
|
|
577
|
+
apps: string[],
|
|
578
|
+
): Array<Record<string, any>> {
|
|
579
|
+
return apps.flatMap((app) => {
|
|
580
|
+
const staticDir = path.join(outputDir, "static", app);
|
|
581
|
+
return collectImmutableAssetPaths(staticDir).map((assetPath) => ({
|
|
582
|
+
src: vercelRouteSrc(`/${app}${assetPath}`),
|
|
583
|
+
headers: IMMUTABLE_ASSET_CACHE_HEADERS,
|
|
584
|
+
continue: true,
|
|
585
|
+
}));
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function vercelRouteSrc(pathname: string): string {
|
|
590
|
+
return pathname.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function vercelRedirect(src: string, location: string): Record<string, any> {
|
|
594
|
+
return {
|
|
595
|
+
src,
|
|
596
|
+
status: 302,
|
|
597
|
+
headers: { Location: location },
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function netlifyAssetRedirectsFor(app: string, distDir: string): string[] {
|
|
602
|
+
const from = `/${app}`;
|
|
603
|
+
const to = `/${NETLIFY_WORKSPACE_STATIC_DIR}/${app}`;
|
|
604
|
+
return [
|
|
605
|
+
`${from}/assets/* ${to}/assets/:splat 200`,
|
|
606
|
+
...netlifyPublicRootAssetPaths(
|
|
607
|
+
app,
|
|
608
|
+
path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app),
|
|
609
|
+
).map((assetPath) => {
|
|
610
|
+
const assetName = assetPath.slice(from.length + 1);
|
|
611
|
+
return `${assetPath} ${to}/${assetName} 200`;
|
|
612
|
+
}),
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function dispatchRootFaviconAsset(distDir: string): string | null {
|
|
617
|
+
for (const asset of ["favicon.ico", "favicon.svg", "favicon.png"]) {
|
|
618
|
+
if (workspaceAppAssetExists(distDir, "dispatch", asset)) return asset;
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function workspaceAppAssetExists(
|
|
624
|
+
distDir: string,
|
|
625
|
+
app: string,
|
|
626
|
+
asset: string,
|
|
627
|
+
): boolean {
|
|
628
|
+
return [
|
|
629
|
+
path.join(distDir, NETLIFY_WORKSPACE_STATIC_DIR, app, asset),
|
|
630
|
+
path.join(distDir, app, app, asset),
|
|
631
|
+
path.join(distDir, app, asset),
|
|
632
|
+
].some((candidate) => fs.existsSync(candidate));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const RESERVED_WORKSPACE_APP_IDS = new Set([
|
|
636
|
+
"_agent-native",
|
|
637
|
+
"_workspace_static",
|
|
638
|
+
"netlify",
|
|
639
|
+
...DISPATCH_WORKSPACE_ROOT_REDIRECTS.map(([from]) => from),
|
|
640
|
+
]);
|
|
641
|
+
|
|
642
|
+
function assertNoReservedWorkspaceAppIds(apps: string[]): void {
|
|
643
|
+
const conflicts = apps.filter(
|
|
644
|
+
(app) => app !== "dispatch" && RESERVED_WORKSPACE_APP_IDS.has(app),
|
|
645
|
+
);
|
|
646
|
+
if (conflicts.length === 0) return;
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Workspace app id ${conflicts.map((id) => `"${id}"`).join(", ")} conflicts with reserved workspace routes. Choose a different app id.`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function copyNetlifyFunctionIntoWorkspace(
|
|
653
|
+
workspaceRoot: string,
|
|
654
|
+
app: string,
|
|
655
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
656
|
+
staticDir: string,
|
|
657
|
+
): void {
|
|
658
|
+
const appDir = path.join(workspaceRoot, "apps", app);
|
|
659
|
+
const src = path.join(appDir, ".netlify", "functions-internal", "server");
|
|
660
|
+
if (!fs.existsSync(src)) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Expected Netlify function at ${src} after building ${app}. Check the app's build script and NITRO_PRESET.`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const dest = path.join(netlifyFunctionsDir(workspaceRoot), `${app}-server`);
|
|
667
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
668
|
+
copyDir(src, dest);
|
|
669
|
+
patchNetlifyFunctionEntry(dest, app, workspaceApps, staticDir);
|
|
670
|
+
|
|
671
|
+
// Durable background agent runs (default-ON; opt out with a falsy
|
|
672
|
+
// AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: when explicitly opted out
|
|
673
|
+
// this emits nothing and the single-function deploy is unchanged.
|
|
674
|
+
if (isDurableBackgroundDeployEnabled()) {
|
|
675
|
+
emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Deploy-time gate for emitting the second `-background` Netlify function. Reads
|
|
681
|
+
* the same env flag the runtime gate uses (`AGENT_CHAT_DURABLE_BACKGROUND`).
|
|
682
|
+
*
|
|
683
|
+
* DEFAULT-ON, matching the runtime gate (`isFlagEnabled` in
|
|
684
|
+
* durable-background.ts) and the single-template gate
|
|
685
|
+
* (`isDurableBackgroundDeployEnabled` in deploy/build.ts): unset/empty/unknown
|
|
686
|
+
* means enabled; an app opts OUT only with an explicit falsy value
|
|
687
|
+
* (`false`/`0`/`no`/`off`). This emits the per-app 15-min `-background` function
|
|
688
|
+
* so the chat `_process-run` dispatch lands on it with the real long budget.
|
|
689
|
+
*/
|
|
690
|
+
function isDurableBackgroundDeployEnabled(): boolean {
|
|
691
|
+
const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
|
|
692
|
+
if (raw == null) return true;
|
|
693
|
+
const v = raw.trim().toLowerCase();
|
|
694
|
+
return !(v === "0" || v === "false" || v === "no" || v === "off");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Emit a SECOND Netlify function for `app` whose name ends in `-background`,
|
|
699
|
+
* re-exporting the SAME `main.mjs` handler bundle. Netlify invokes any function
|
|
700
|
+
* with `config.background: true` asynchronously (202 immediately, up to 15-min
|
|
701
|
+
* budget), which is exactly what the durable-background chat dispatch
|
|
702
|
+
* (`fireInternalDispatch` → the function's default url) needs.
|
|
703
|
+
*
|
|
704
|
+
* DOC-CORRECT DEFAULT-URL APPROACH (mirrors the single-template emit in
|
|
705
|
+
* deploy/build.ts): the function declares NO custom `config.path`, so it keeps
|
|
706
|
+
* its DEFAULT url `/.netlify/functions/<app>-agent-background`. The `<app>-server`
|
|
707
|
+
* function's catch-all already excludes `/.netlify/*`, so that default-url
|
|
708
|
+
* namespace is NEVER shadowed by the synchronous function — no overlapping
|
|
709
|
+
* `config.path` and no catch-all patch are needed. The foreground dispatches to
|
|
710
|
+
* that default url (`resolveAgentChatProcessRunDispatchPath` resolves the per-app
|
|
711
|
+
* name from `AGENT_NATIVE_WORKSPACE_APP_ID`); `fireInternalDispatch` strips the
|
|
712
|
+
* app base path for `/.netlify/*` targets so the request reaches the host-root
|
|
713
|
+
* function url. The entry then REWRITES the incoming pathname to the
|
|
714
|
+
* base-path-prefixed `_process-run` route before delegating to the Nitro router.
|
|
715
|
+
*
|
|
716
|
+
* It shares the same bundle (`includedFiles: ["**"]`) so `A2A_SECRET`, the DB
|
|
717
|
+
* URL, and the rest of the env/bundle are present. A prior attempt gave the
|
|
718
|
+
* function a custom `config.path` (the framework route) that overlapped the
|
|
719
|
+
* synchronous `<app>-server` catch-all; that path was not honored as a route in
|
|
720
|
+
* prod (probe → 404). The default url is the doc-correct fix.
|
|
721
|
+
*
|
|
722
|
+
* Safety net: if the dispatch fast-fails the foreground degrades to an inline
|
|
723
|
+
* 40s synchronous run (see production-agent.ts).
|
|
724
|
+
*/
|
|
725
|
+
function emitNetlifyBackgroundFunction(
|
|
726
|
+
workspaceRoot: string,
|
|
727
|
+
app: string,
|
|
728
|
+
srcServerDir: string,
|
|
729
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
730
|
+
): void {
|
|
731
|
+
// Name MUST end in `-background` (Netlify async convention + the runtime guard
|
|
732
|
+
// reads the -background Lambda-name suffix as a fallback). It is reached at its
|
|
733
|
+
// DEFAULT url /.netlify/functions/<app>-agent-background.
|
|
734
|
+
const backgroundName = `${app}-agent-background`;
|
|
735
|
+
const dest = path.join(netlifyFunctionsDir(workspaceRoot), backgroundName);
|
|
736
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
737
|
+
copyDir(srcServerDir, dest);
|
|
738
|
+
|
|
739
|
+
const basePath = `/${app}`;
|
|
740
|
+
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
741
|
+
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
742
|
+
workspaceApps,
|
|
743
|
+
app,
|
|
744
|
+
);
|
|
745
|
+
// The Nitro router for this app expects the base-path-prefixed framework route.
|
|
746
|
+
// The function is reached at its default url, so the entry rewrites the
|
|
747
|
+
// incoming pathname to `/<app>/_agent-native/agent-chat/_process-run`.
|
|
748
|
+
const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
|
|
749
|
+
const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
|
|
750
|
+
// is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
|
|
751
|
+
// function (the deployed Lambda name is not guaranteed to end in -background). A
|
|
752
|
+
// globalThis flag (NOT process.env) avoids the no-env-mutation guard and carries
|
|
753
|
+
// no cross-request state.
|
|
754
|
+
globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
755
|
+
|
|
756
|
+
const basePath = ${JSON.stringify(basePath)};
|
|
757
|
+
// The base-path-prefixed framework route the Nitro router dispatches to.
|
|
758
|
+
const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
|
|
759
|
+
|
|
760
|
+
function setBasePathEnv() {
|
|
761
|
+
const processRef = globalThis.process ??= { env: {} };
|
|
762
|
+
processRef.env ??= {};
|
|
763
|
+
Object.assign(processRef.env, {
|
|
764
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
765
|
+
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
766
|
+
APP_BASE_PATH: basePath,
|
|
767
|
+
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
768
|
+
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
769
|
+
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
770
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
771
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
772
|
+
VITE_APP_BASE_PATH: basePath,
|
|
773
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
774
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
775
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
776
|
+
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
777
|
+
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
setBasePathEnv();
|
|
782
|
+
|
|
783
|
+
let cachedHandler;
|
|
784
|
+
|
|
785
|
+
// Reached at the DEFAULT url /.netlify/functions/${backgroundName}; REWRITE the
|
|
786
|
+
// incoming pathname to the base-path-prefixed _process-run route so the Nitro
|
|
787
|
+
// router runs the plugin. Method, ALL headers (the HMAC Authorization: Bearer
|
|
788
|
+
// MUST survive) and the body are preserved.
|
|
789
|
+
export default async function handler(request) {
|
|
790
|
+
setBasePathEnv();
|
|
791
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
792
|
+
const url = new URL(request.url);
|
|
793
|
+
url.pathname = PROCESS_RUN_PATH;
|
|
794
|
+
const method = request.method || "POST";
|
|
795
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
796
|
+
const body = hasBody ? await request.text() : undefined;
|
|
797
|
+
const rewritten = new Request(url.toString(), {
|
|
798
|
+
method,
|
|
799
|
+
headers: request.headers,
|
|
800
|
+
body,
|
|
801
|
+
});
|
|
802
|
+
return cachedHandler(rewritten);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export const config = {
|
|
806
|
+
name: ${JSON.stringify(`${app} agent background handler`)},
|
|
807
|
+
generator: "agent-native workspace deploy",
|
|
808
|
+
// background: true → async invoke (202, 15-min budget). NO custom path: the
|
|
809
|
+
// function keeps its default url /.netlify/functions/${backgroundName}, which
|
|
810
|
+
// the <app>-server catch-all never shadows (it excludes /.netlify/*).
|
|
811
|
+
background: true,
|
|
812
|
+
nodeBundler: "none",
|
|
813
|
+
includedFiles: ["**"],
|
|
814
|
+
preferStatic: false,
|
|
815
|
+
};
|
|
816
|
+
`;
|
|
817
|
+
// Remove the original Nitro entry (server.mjs) so only our background entry
|
|
818
|
+
// is the function entrypoint, mirroring patchNetlifyFunctionEntry.
|
|
819
|
+
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
820
|
+
fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), server);
|
|
821
|
+
console.log(
|
|
822
|
+
`[workspace-deploy] Emitted durable-background function "${backgroundName}" ` +
|
|
823
|
+
`for app "${app}" with config { background:true } and NO custom path — ` +
|
|
824
|
+
`reachable at its default url /.netlify/functions/${backgroundName} ` +
|
|
825
|
+
`(rewrites to ${processRunPath}). REQUIRES real-deploy verification of ` +
|
|
826
|
+
`Netlify async (202) invocation — see docs/design/durable-agent-runs.md.`,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function patchNetlifyFunctionEntry(
|
|
831
|
+
functionDir: string,
|
|
832
|
+
app: string,
|
|
833
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
834
|
+
staticDir: string,
|
|
835
|
+
): void {
|
|
836
|
+
const serverPath = path.join(functionDir, "server.mjs");
|
|
837
|
+
if (!fs.existsSync(serverPath)) return;
|
|
838
|
+
|
|
839
|
+
const basePath = `/${app}`;
|
|
840
|
+
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
841
|
+
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
842
|
+
workspaceApps,
|
|
843
|
+
app,
|
|
844
|
+
);
|
|
845
|
+
const pathConfig =
|
|
846
|
+
app === "dispatch"
|
|
847
|
+
? ["/_agent-native/*", "/.well-known/*", `${basePath}/*`]
|
|
848
|
+
: [basePath, `${basePath}/*`];
|
|
849
|
+
const normalizeBasePathHelper =
|
|
850
|
+
app === "dispatch"
|
|
851
|
+
? ""
|
|
852
|
+
: `
|
|
853
|
+
function normalizeBasePathArgs(args) {
|
|
854
|
+
const request = args[0];
|
|
855
|
+
if (!request || typeof request.url !== "string" || typeof Request !== "function") {
|
|
856
|
+
return args;
|
|
857
|
+
}
|
|
858
|
+
const url = new URL(request.url);
|
|
859
|
+
if (url.pathname === basePath || url.pathname === \`\${basePath}/\`) {
|
|
860
|
+
url.pathname = \`\${basePath}//\`;
|
|
861
|
+
return [new Request(url, request), ...args.slice(1)];
|
|
862
|
+
}
|
|
863
|
+
return args;
|
|
864
|
+
}
|
|
865
|
+
`;
|
|
866
|
+
const handlerArgs =
|
|
867
|
+
app === "dispatch" ? "...args" : "...normalizeBasePathArgs(args)";
|
|
868
|
+
const server = `const basePath = ${JSON.stringify(basePath)};
|
|
869
|
+
|
|
870
|
+
function setBasePathEnv() {
|
|
871
|
+
const processRef = globalThis.process ??= { env: {} };
|
|
872
|
+
processRef.env ??= {};
|
|
873
|
+
Object.assign(processRef.env, {
|
|
874
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
875
|
+
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
876
|
+
APP_BASE_PATH: basePath,
|
|
877
|
+
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
878
|
+
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
879
|
+
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
880
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
881
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
882
|
+
VITE_APP_BASE_PATH: basePath,
|
|
883
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
884
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
885
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
886
|
+
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
887
|
+
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
setBasePathEnv();
|
|
892
|
+
${normalizeBasePathHelper}
|
|
893
|
+
|
|
894
|
+
let cachedHandler;
|
|
895
|
+
|
|
896
|
+
export default async function handler(...args) {
|
|
897
|
+
setBasePathEnv();
|
|
898
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
899
|
+
return cachedHandler(${handlerArgs});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export const config = {
|
|
903
|
+
name: ${JSON.stringify(`${app} server handler`)},
|
|
904
|
+
generator: "agent-native workspace deploy",
|
|
905
|
+
path: ${JSON.stringify(pathConfig)},
|
|
906
|
+
nodeBundler: "none",
|
|
907
|
+
includedFiles: ["**"],
|
|
908
|
+
excludedPath: ${JSON.stringify(
|
|
909
|
+
netlifyFunctionExcludedPaths(app, staticDir),
|
|
910
|
+
null,
|
|
911
|
+
2,
|
|
912
|
+
)
|
|
913
|
+
.split("\n")
|
|
914
|
+
.join("\n ")},
|
|
915
|
+
preferStatic: false,
|
|
916
|
+
};
|
|
917
|
+
`;
|
|
918
|
+
fs.rmSync(serverPath, { force: true });
|
|
919
|
+
fs.writeFileSync(path.join(functionDir, `${app}-server.mjs`), server);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function patchVercelFunctionEntry(
|
|
923
|
+
functionDir: string,
|
|
924
|
+
app: string,
|
|
925
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
926
|
+
): void {
|
|
927
|
+
const entryPath = path.join(functionDir, "index.mjs");
|
|
928
|
+
if (!fs.existsSync(entryPath)) return;
|
|
929
|
+
|
|
930
|
+
const mainPath = path.join(functionDir, "main.mjs");
|
|
931
|
+
fs.rmSync(mainPath, { force: true });
|
|
932
|
+
fs.renameSync(entryPath, mainPath);
|
|
933
|
+
|
|
934
|
+
const basePath = `/${app}`;
|
|
935
|
+
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
936
|
+
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
937
|
+
workspaceApps,
|
|
938
|
+
app,
|
|
939
|
+
);
|
|
940
|
+
const entry = `const basePath = ${JSON.stringify(basePath)};
|
|
941
|
+
|
|
942
|
+
function setBasePathEnv() {
|
|
943
|
+
const processRef = globalThis.process ??= { env: {} };
|
|
944
|
+
processRef.env ??= {};
|
|
945
|
+
Object.assign(processRef.env, {
|
|
946
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
947
|
+
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
948
|
+
APP_BASE_PATH: basePath,
|
|
949
|
+
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
950
|
+
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
951
|
+
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
952
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
953
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
954
|
+
VITE_APP_BASE_PATH: basePath,
|
|
955
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
956
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
957
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
958
|
+
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
959
|
+
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function normalizeBasePathArgs(args) {
|
|
964
|
+
const request = args[0];
|
|
965
|
+
if (!request) return args;
|
|
966
|
+
|
|
967
|
+
if (typeof Request === "function" && request instanceof Request) {
|
|
968
|
+
const url = new URL(request.url);
|
|
969
|
+
if (url.pathname === basePath || url.pathname === \`\${basePath}/\`) {
|
|
970
|
+
url.pathname = \`\${basePath}//\`;
|
|
971
|
+
return [new Request(url, request), ...args.slice(1)];
|
|
972
|
+
}
|
|
973
|
+
return args;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (typeof request.url !== "string") return args;
|
|
977
|
+
const url = new URL(request.url, "http://agent-native.local");
|
|
978
|
+
if (url.pathname === basePath || url.pathname === \`\${basePath}/\`) {
|
|
979
|
+
request.url = \`\${basePath}//\${url.search}\`;
|
|
980
|
+
}
|
|
981
|
+
return args;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
setBasePathEnv();
|
|
985
|
+
|
|
986
|
+
let cachedHandler;
|
|
987
|
+
|
|
988
|
+
export default async function handler(...args) {
|
|
989
|
+
setBasePathEnv();
|
|
990
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
991
|
+
return cachedHandler(...normalizeBasePathArgs(args));
|
|
992
|
+
}
|
|
993
|
+
`;
|
|
994
|
+
fs.writeFileSync(entryPath, entry);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function netlifyFunctionExcludedPaths(
|
|
998
|
+
app: string,
|
|
999
|
+
staticDir: string,
|
|
1000
|
+
): string[] {
|
|
1001
|
+
return [
|
|
1002
|
+
"/.netlify/*",
|
|
1003
|
+
`/${app}/assets/*`,
|
|
1004
|
+
...netlifyPublicRootAssetPaths(app, staticDir),
|
|
1005
|
+
];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function netlifyPublicRootAssetPaths(app: string, staticDir: string): string[] {
|
|
1009
|
+
if (!fs.existsSync(staticDir)) return [];
|
|
1010
|
+
return fs
|
|
1011
|
+
.readdirSync(staticDir, { withFileTypes: true })
|
|
1012
|
+
.filter((entry) => entry.isFile())
|
|
1013
|
+
.map((entry) => entry.name)
|
|
1014
|
+
.filter((name) => {
|
|
1015
|
+
const ext = path.extname(name).slice(1).toLowerCase();
|
|
1016
|
+
return NETLIFY_PUBLIC_ASSET_EXTENSIONS.has(ext);
|
|
1017
|
+
})
|
|
1018
|
+
.sort()
|
|
1019
|
+
.map((name) => `/${app}/${encodeURI(name)}`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function netlifyFunctionsDir(workspaceRoot: string): string {
|
|
1023
|
+
return path.join(workspaceRoot, ".netlify", "functions-internal");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function cleanAppBuildOutputs(appDir: string): void {
|
|
1027
|
+
// .deploy-tmp: a crashed prior per-app build leaves partial artifacts that
|
|
1028
|
+
// silently poison the next build for that app (observed on Windows: stale
|
|
1029
|
+
// state made a cloudflare_pages build die after the vite phases with no
|
|
1030
|
+
// error output). The deploy post-build also sweeps it, but clean here too
|
|
1031
|
+
// so the unified build never depends on the child's hygiene.
|
|
1032
|
+
for (const name of ["dist", ".output", "build", ".deploy-tmp"]) {
|
|
1033
|
+
fs.rmSync(path.join(appDir, name), { recursive: true, force: true });
|
|
1034
|
+
}
|
|
1035
|
+
fs.rmSync(path.join(appDir, ".netlify", "functions-internal"), {
|
|
1036
|
+
recursive: true,
|
|
1037
|
+
force: true,
|
|
1038
|
+
});
|
|
1039
|
+
fs.rmSync(path.join(appDir, ".vercel", "output"), {
|
|
1040
|
+
recursive: true,
|
|
1041
|
+
force: true,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function appUsesNetlifyUnpooledDatabaseUrl(appDir: string): boolean {
|
|
1046
|
+
const netlifyPath = path.join(appDir, "netlify.toml");
|
|
1047
|
+
if (!fs.existsSync(netlifyPath)) return false;
|
|
1048
|
+
try {
|
|
1049
|
+
return fs
|
|
1050
|
+
.readFileSync(netlifyPath, "utf-8")
|
|
1051
|
+
.includes("NETLIFY_DATABASE_URL_UNPOOLED");
|
|
1052
|
+
} catch {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function writeWorkspaceAppManifests(
|
|
1058
|
+
workspaceRoot: string,
|
|
1059
|
+
distDir: string,
|
|
1060
|
+
apps: string[],
|
|
1061
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
1062
|
+
preset: WorkspaceDeployPreset,
|
|
1063
|
+
): void {
|
|
1064
|
+
const manifest = JSON.stringify(
|
|
1065
|
+
{
|
|
1066
|
+
version: 1,
|
|
1067
|
+
apps: workspaceApps,
|
|
1068
|
+
},
|
|
1069
|
+
null,
|
|
1070
|
+
2,
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
const targets =
|
|
1074
|
+
preset === "netlify"
|
|
1075
|
+
? apps.map((app) =>
|
|
1076
|
+
path.join(
|
|
1077
|
+
netlifyFunctionsDir(workspaceRoot),
|
|
1078
|
+
`${app}-server`,
|
|
1079
|
+
WORKSPACE_APPS_MANIFEST_DIR,
|
|
1080
|
+
WORKSPACE_APPS_MANIFEST_FILE,
|
|
1081
|
+
),
|
|
1082
|
+
)
|
|
1083
|
+
: preset === "vercel"
|
|
1084
|
+
? apps.map((app) =>
|
|
1085
|
+
path.join(
|
|
1086
|
+
workspaceRoot,
|
|
1087
|
+
VERCEL_OUTPUT_DIR,
|
|
1088
|
+
"functions",
|
|
1089
|
+
`${app}-server.func`,
|
|
1090
|
+
WORKSPACE_APPS_MANIFEST_DIR,
|
|
1091
|
+
WORKSPACE_APPS_MANIFEST_FILE,
|
|
1092
|
+
),
|
|
1093
|
+
)
|
|
1094
|
+
: apps.map((app) =>
|
|
1095
|
+
path.join(
|
|
1096
|
+
distDir,
|
|
1097
|
+
app,
|
|
1098
|
+
WORKSPACE_APPS_MANIFEST_DIR,
|
|
1099
|
+
WORKSPACE_APPS_MANIFEST_FILE,
|
|
1100
|
+
),
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
for (const target of targets) {
|
|
1104
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1105
|
+
fs.writeFileSync(target, `${manifest}\n`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function readWorkspaceAppManifest(
|
|
1110
|
+
workspaceRoot: string,
|
|
1111
|
+
apps: string[],
|
|
1112
|
+
): WorkspaceAppManifestEntry[] {
|
|
1113
|
+
const explicitApps = readExistingWorkspaceAppManifest(workspaceRoot);
|
|
1114
|
+
|
|
1115
|
+
return apps
|
|
1116
|
+
.map((app) => {
|
|
1117
|
+
const appDir = path.join(workspaceRoot, "apps", app);
|
|
1118
|
+
const pkg = readPackageJson(path.join(appDir, "package.json"));
|
|
1119
|
+
const appPath = `/${app}`;
|
|
1120
|
+
const explicit = explicitApps.get(app);
|
|
1121
|
+
const url =
|
|
1122
|
+
normalizeWorkspaceAppUrl(explicit?.url) ?? workspaceAppUrl(appPath);
|
|
1123
|
+
const audience =
|
|
1124
|
+
workspaceAppAudienceFromPackageJson(pkg) ??
|
|
1125
|
+
explicit?.audience ??
|
|
1126
|
+
DEFAULT_WORKSPACE_APP_AUDIENCE;
|
|
1127
|
+
const packageRouteAccess = workspaceAppRouteAccessFromPackageJson(pkg);
|
|
1128
|
+
// Prefer the package.json value whenever the field was set — including
|
|
1129
|
+
// an explicit empty array, which is how a per-app package.json signals
|
|
1130
|
+
// "clear any previously-published manifest override." Falling back on
|
|
1131
|
+
// length > 0 would silently keep the explicit override even after the
|
|
1132
|
+
// app owner blanked their list.
|
|
1133
|
+
const publicPaths =
|
|
1134
|
+
packageRouteAccess.publicPaths ?? explicit?.publicPaths ?? [];
|
|
1135
|
+
const protectedPaths =
|
|
1136
|
+
packageRouteAccess.protectedPaths ?? explicit?.protectedPaths ?? [];
|
|
1137
|
+
return {
|
|
1138
|
+
id: app,
|
|
1139
|
+
name: pkg?.displayName || titleCase(app),
|
|
1140
|
+
description: pkg?.description || "",
|
|
1141
|
+
path: appPath,
|
|
1142
|
+
...(url ? { url } : {}),
|
|
1143
|
+
isDispatch: app === "dispatch",
|
|
1144
|
+
audience,
|
|
1145
|
+
publicPaths,
|
|
1146
|
+
protectedPaths,
|
|
1147
|
+
};
|
|
1148
|
+
})
|
|
1149
|
+
.sort((a, b) => {
|
|
1150
|
+
if (a.id === "dispatch") return -1;
|
|
1151
|
+
if (b.id === "dispatch") return 1;
|
|
1152
|
+
return a.name.localeCompare(b.name);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function readExistingWorkspaceAppManifest(
|
|
1157
|
+
workspaceRoot: string,
|
|
1158
|
+
): Map<string, WorkspaceAppManifestOverride> {
|
|
1159
|
+
const fromEnv = parseWorkspaceAppsJson(process.env[WORKSPACE_APPS_ENV_KEY]);
|
|
1160
|
+
const fromFile =
|
|
1161
|
+
readWorkspaceAppsFromFile(
|
|
1162
|
+
path.join(
|
|
1163
|
+
workspaceRoot,
|
|
1164
|
+
WORKSPACE_APPS_MANIFEST_DIR,
|
|
1165
|
+
WORKSPACE_APPS_MANIFEST_FILE,
|
|
1166
|
+
),
|
|
1167
|
+
) ??
|
|
1168
|
+
readWorkspaceAppsFromFile(
|
|
1169
|
+
path.join(workspaceRoot, WORKSPACE_APPS_MANIFEST_FILE),
|
|
1170
|
+
);
|
|
1171
|
+
const apps = fromEnv ?? fromFile ?? [];
|
|
1172
|
+
return new Map(apps.map((app) => [app.id, app]));
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function parseWorkspaceAppsJson(
|
|
1176
|
+
raw: string | undefined,
|
|
1177
|
+
): WorkspaceAppManifestOverride[] | null {
|
|
1178
|
+
if (!raw) return null;
|
|
1179
|
+
try {
|
|
1180
|
+
return parseWorkspaceAppsManifest(JSON.parse(raw));
|
|
1181
|
+
} catch {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function readWorkspaceAppsFromFile(
|
|
1187
|
+
file: string,
|
|
1188
|
+
): WorkspaceAppManifestOverride[] | null {
|
|
1189
|
+
if (!fs.existsSync(file)) return null;
|
|
1190
|
+
return parseWorkspaceAppsManifest(readPackageJson(file));
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function parseWorkspaceAppsManifest(
|
|
1194
|
+
parsed: any,
|
|
1195
|
+
): WorkspaceAppManifestOverride[] | null {
|
|
1196
|
+
const rawApps = Array.isArray(parsed?.apps)
|
|
1197
|
+
? parsed.apps
|
|
1198
|
+
: Array.isArray(parsed)
|
|
1199
|
+
? parsed
|
|
1200
|
+
: null;
|
|
1201
|
+
if (!rawApps) return null;
|
|
1202
|
+
|
|
1203
|
+
const apps = (rawApps as unknown[])
|
|
1204
|
+
.map((entry) => {
|
|
1205
|
+
if (!entry || typeof entry !== "object") return null;
|
|
1206
|
+
const e = entry as Record<string, unknown>;
|
|
1207
|
+
const id = typeof e.id === "string" ? e.id.trim() : "";
|
|
1208
|
+
if (!id) return null;
|
|
1209
|
+
const url = normalizeWorkspaceAppUrl(e.url);
|
|
1210
|
+
const audience =
|
|
1211
|
+
e.audience === undefined
|
|
1212
|
+
? undefined
|
|
1213
|
+
: normalizeWorkspaceAppAudience(e.audience);
|
|
1214
|
+
const publicPaths = normalizeWorkspaceAppPathList(e.publicPaths);
|
|
1215
|
+
const protectedPaths = normalizeWorkspaceAppPathList(e.protectedPaths);
|
|
1216
|
+
return {
|
|
1217
|
+
id,
|
|
1218
|
+
...(url ? { url } : {}),
|
|
1219
|
+
...(audience ? { audience } : {}),
|
|
1220
|
+
...(publicPaths.length > 0 ? { publicPaths } : {}),
|
|
1221
|
+
...(protectedPaths.length > 0 ? { protectedPaths } : {}),
|
|
1222
|
+
};
|
|
1223
|
+
})
|
|
1224
|
+
.filter((app): app is NonNullable<typeof app> => !!app);
|
|
1225
|
+
|
|
1226
|
+
return apps.length ? apps : null;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function workspaceBaseUrl(): string | null {
|
|
1230
|
+
const gatewayOrigin =
|
|
1231
|
+
process.env.WORKSPACE_GATEWAY_URL || process.env.VITE_WORKSPACE_GATEWAY_URL;
|
|
1232
|
+
const publicGatewayOrigin = normalizeOrigin(gatewayOrigin);
|
|
1233
|
+
const gatewayFallback =
|
|
1234
|
+
publicGatewayOrigin && !isLoopbackOrigin(publicGatewayOrigin)
|
|
1235
|
+
? gatewayOrigin
|
|
1236
|
+
: null;
|
|
1237
|
+
return (
|
|
1238
|
+
process.env.APP_URL ||
|
|
1239
|
+
process.env.WORKSPACE_OAUTH_ORIGIN ||
|
|
1240
|
+
process.env.VITE_WORKSPACE_OAUTH_ORIGIN ||
|
|
1241
|
+
process.env.URL ||
|
|
1242
|
+
process.env.DEPLOY_URL ||
|
|
1243
|
+
process.env.BETTER_AUTH_URL ||
|
|
1244
|
+
gatewayFallback ||
|
|
1245
|
+
gatewayOrigin ||
|
|
1246
|
+
null
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function normalizeOrigin(value: string | null | undefined): string | undefined {
|
|
1251
|
+
if (!value) return undefined;
|
|
1252
|
+
try {
|
|
1253
|
+
return new URL(value).origin;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return undefined;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function isLoopbackOrigin(origin: string | undefined): boolean {
|
|
1260
|
+
if (!origin) return false;
|
|
1261
|
+
try {
|
|
1262
|
+
const host = new URL(origin).hostname.toLowerCase();
|
|
1263
|
+
return (
|
|
1264
|
+
host === "localhost" ||
|
|
1265
|
+
host === "127.0.0.1" ||
|
|
1266
|
+
host === "[::1]" ||
|
|
1267
|
+
host === "::1"
|
|
1268
|
+
);
|
|
1269
|
+
} catch {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function workspaceOAuthOrigin(
|
|
1275
|
+
workspaceGatewayUrl: string | null,
|
|
1276
|
+
): string | undefined {
|
|
1277
|
+
// Explicit overrides (env vars set by the operator) win even if they happen
|
|
1278
|
+
// to be loopback — that's a deliberate dev-against-prod choice.
|
|
1279
|
+
// The gateway-URL fallback, however, is auto-resolved and would silently
|
|
1280
|
+
// send users to localhost if the configured gateway is loopback in a
|
|
1281
|
+
// production deploy. Strip loopback there so OAuth fails loudly instead.
|
|
1282
|
+
const gatewayFallback = normalizeOrigin(workspaceGatewayUrl);
|
|
1283
|
+
return (
|
|
1284
|
+
normalizeOrigin(process.env.VITE_WORKSPACE_OAUTH_ORIGIN) ||
|
|
1285
|
+
normalizeOrigin(process.env.WORKSPACE_OAUTH_ORIGIN) ||
|
|
1286
|
+
normalizeOrigin(process.env.APP_URL) ||
|
|
1287
|
+
normalizeOrigin(process.env.BETTER_AUTH_URL) ||
|
|
1288
|
+
normalizeOrigin(process.env.URL) ||
|
|
1289
|
+
normalizeOrigin(process.env.DEPLOY_URL) ||
|
|
1290
|
+
(isLoopbackOrigin(gatewayFallback) ? undefined : gatewayFallback)
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function workspaceAppUrl(appPath: string): string | undefined {
|
|
1295
|
+
const base = workspaceBaseUrl();
|
|
1296
|
+
if (!base) return undefined;
|
|
1297
|
+
try {
|
|
1298
|
+
return new URL(appPath, `${base.replace(/\/$/, "")}/`).toString();
|
|
1299
|
+
} catch {
|
|
1300
|
+
return undefined;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function normalizeWorkspaceAppUrl(value: unknown): string | undefined {
|
|
1305
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
1306
|
+
try {
|
|
1307
|
+
return new URL(value.trim()).toString().replace(/\/$/, "");
|
|
1308
|
+
} catch {
|
|
1309
|
+
return undefined;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function readPackageJson(file: string): Record<string, any> | null {
|
|
1314
|
+
try {
|
|
1315
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1316
|
+
} catch {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function titleCase(value: string): string {
|
|
1322
|
+
return value
|
|
1323
|
+
.split(/[-_\s]+/)
|
|
1324
|
+
.filter(Boolean)
|
|
1325
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1326
|
+
.join(" ");
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function parsePresetArg(args: string[]): WorkspaceDeployPreset | null {
|
|
1330
|
+
for (let i = 0; i < args.length; i++) {
|
|
1331
|
+
const arg = args[i];
|
|
1332
|
+
if (arg === "--preset" && args[i + 1]) {
|
|
1333
|
+
return normalizePreset(args[i + 1]);
|
|
1334
|
+
}
|
|
1335
|
+
if (arg.startsWith("--preset=")) {
|
|
1336
|
+
return normalizePreset(arg.slice("--preset=".length));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function resolvePreset(
|
|
1343
|
+
optionPreset: WorkspaceDeployPreset | undefined,
|
|
1344
|
+
args: string[],
|
|
1345
|
+
): WorkspaceDeployPreset {
|
|
1346
|
+
return (
|
|
1347
|
+
optionPreset ??
|
|
1348
|
+
parsePresetArg(args) ??
|
|
1349
|
+
normalizePreset(process.env.NITRO_PRESET) ??
|
|
1350
|
+
"cloudflare_pages"
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function assertWorkspaceDeployProductionEnv(opts: {
|
|
1355
|
+
buildOnly: boolean;
|
|
1356
|
+
preset: WorkspaceDeployPreset;
|
|
1357
|
+
}): void {
|
|
1358
|
+
if (!isProductionWorkspaceDeploy(opts)) return;
|
|
1359
|
+
if (process.env.A2A_SECRET?.trim()) return;
|
|
1360
|
+
const providerHint =
|
|
1361
|
+
opts.preset === "netlify"
|
|
1362
|
+
? ' For Netlify, one option is: netlify env:set A2A_SECRET "$(openssl rand -hex 32)".'
|
|
1363
|
+
: "";
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
[
|
|
1366
|
+
"A2A_SECRET is required for production workspace deploys.",
|
|
1367
|
+
"Workspace Slack, webhook, and cross-app A2A work resumes through signed background processors; without A2A_SECRET those production routes return 503.",
|
|
1368
|
+
`Set A2A_SECRET in your deploy provider and redeploy.${providerHint}`,
|
|
1369
|
+
"For local artifact checks, run agent-native deploy --build-only outside the deploy provider environment.",
|
|
1370
|
+
].join(" "),
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function isProductionWorkspaceDeploy(opts: {
|
|
1375
|
+
buildOnly: boolean;
|
|
1376
|
+
preset: WorkspaceDeployPreset;
|
|
1377
|
+
}): boolean {
|
|
1378
|
+
if (!opts.buildOnly) return true;
|
|
1379
|
+
if (
|
|
1380
|
+
opts.preset === "netlify" &&
|
|
1381
|
+
process.env.NETLIFY === "true" &&
|
|
1382
|
+
process.env.NETLIFY_LOCAL !== "true"
|
|
1383
|
+
) {
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
if (opts.preset === "cloudflare_pages" && process.env.CF_PAGES === "1") {
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
if (opts.preset === "vercel" && process.env.VERCEL === "1") {
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function normalizePreset(
|
|
1396
|
+
value: string | undefined,
|
|
1397
|
+
): WorkspaceDeployPreset | null {
|
|
1398
|
+
if (!value) return null;
|
|
1399
|
+
if (value === "cloudflare_pages" || value === "cloudflare-pages") {
|
|
1400
|
+
return "cloudflare_pages";
|
|
1401
|
+
}
|
|
1402
|
+
if (value === "netlify") return "netlify";
|
|
1403
|
+
if (value === "vercel") return "vercel";
|
|
1404
|
+
throw new Error(
|
|
1405
|
+
`Unsupported workspace deploy preset "${value}". Supported presets: cloudflare_pages, netlify, vercel.`,
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function moduleIdent(app: string): string {
|
|
1410
|
+
return "app_" + app.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function workspaceAppAudienceForApp(
|
|
1414
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
1415
|
+
app: string,
|
|
1416
|
+
): WorkspaceAppAudience {
|
|
1417
|
+
return (
|
|
1418
|
+
workspaceApps.find((entry) => entry.id === app)?.audience ??
|
|
1419
|
+
DEFAULT_WORKSPACE_APP_AUDIENCE
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function workspaceAppRouteAccessForApp(
|
|
1424
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
1425
|
+
app: string,
|
|
1426
|
+
): WorkspaceAppRouteAccess {
|
|
1427
|
+
const entry = workspaceApps.find((candidate) => candidate.id === app);
|
|
1428
|
+
return {
|
|
1429
|
+
publicPaths: entry?.publicPaths ?? [],
|
|
1430
|
+
protectedPaths: entry?.protectedPaths ?? [],
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function compareWorkspaceAppIds(a: string, b: string): number {
|
|
1435
|
+
if (a === "dispatch") return -1;
|
|
1436
|
+
if (b === "dispatch") return 1;
|
|
1437
|
+
return a.localeCompare(b);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function copyDir(src: string, dest: string): void {
|
|
1441
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
1442
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
1443
|
+
const s = path.join(src, entry.name);
|
|
1444
|
+
const d = path.join(dest, entry.name);
|
|
1445
|
+
if (entry.isSymbolicLink()) {
|
|
1446
|
+
try {
|
|
1447
|
+
const target = fs.readlinkSync(s);
|
|
1448
|
+
fs.symlinkSync(target, d);
|
|
1449
|
+
} catch {
|
|
1450
|
+
fs.copyFileSync(s, d);
|
|
1451
|
+
}
|
|
1452
|
+
} else if (entry.isDirectory()) {
|
|
1453
|
+
copyDir(s, d);
|
|
1454
|
+
} else {
|
|
1455
|
+
fs.copyFileSync(s, d);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|