fix(eve): start - resolve prewarm modules after app relocation (#3093)

Signed-off-by: Andrew Barba <barba@hey.com>
This commit is contained in:
Andrew Barba
2026-09-07 11:16:56 -04:00
committed by GitHub
parent 14af96f69c
commit 4cff7dc5db
5 changed files with 132 additions and 4 deletions
@@ -0,0 +1,5 @@
---
"eve": patch
---
Fix `eve start` failing when an app built on another machine is deployed to a different directory. Sandbox prewarming now resolves authored modules from the deployed app while preserving TypeScript aliases and workspace package resolution.
+2
View File
@@ -203,6 +203,8 @@ eve start [--host <host>] [--port <port>]
Serves the previously built output. Prints the listening URL.
For self-hosted deployments, copy the app source, `.output/`, and installed dependencies together. The deployment directory can differ from the build directory. Preserve the relative layout of any workspace packages used by the app; startup resolves sandbox prewarm modules from the deployed source.
## `eve dev`
```bash
@@ -206,6 +206,7 @@ export async function prewarmBuiltAppSandboxes(input: {
compiledArtifactsSource: builtArtifactsSource,
}),
loadCompiledModuleMapFromAuthoredSource({
authoredAppRoot: input.appRoot,
compiledArtifactsSource: builtArtifactsSource,
}),
]);
@@ -1,4 +1,4 @@
import { join } from "node:path";
import { join, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type {
@@ -23,9 +23,11 @@ import { formatValidationError } from "#runtime/validation.js";
const EXT_CONFIG_SCOPE = Symbol.for("eve.ext-config-scope");
/** Hydrates the compiled module map from the exact physical bindings in the manifest. */
/** Hydrates the compiled module map from the manifests authored bindings. */
export async function loadCompiledModuleMapFromAuthoredSource(input: {
readonly compiledArtifactsSource: RuntimeDiskCompiledArtifactsSource;
/** Current location of a built application deployed with its source and dependencies. */
readonly authoredAppRoot?: string;
}): Promise<CompiledModuleMap> {
const manifest = await loadCompiledManifest({
compiledArtifactsSource: input.compiledArtifactsSource,
@@ -33,12 +35,14 @@ export async function loadCompiledModuleMapFromAuthoredSource(input: {
return await hydrateCompiledModuleMapFromManifest(
manifest,
input.compiledArtifactsSource.appRoot,
input.authoredAppRoot,
);
}
async function hydrateCompiledModuleMapFromManifest(
manifest: CompiledAgentManifest,
runtimeAppRoot: string,
authoredAppRoot: string = manifest.appRoot,
): Promise<CompiledModuleMap> {
const materializedIndex = await readMaterializedAuthoredModuleIndex(runtimeAppRoot);
if (materializedIndex !== undefined) {
@@ -60,7 +64,9 @@ async function hydrateCompiledModuleMapFromManifest(
];
for (const node of nodeManifests) {
nodes[node.nodeId] = {
modules: await hydrateCompiledNodeScope(node.manifest),
modules: await hydrateCompiledNodeScope(node.manifest, (sourcePath) =>
resolve(authoredAppRoot, relative(manifest.appRoot, sourcePath)),
),
};
}
return { nodes };
@@ -68,6 +74,7 @@ async function hydrateCompiledModuleMapFromManifest(
async function hydrateCompiledNodeScope(
manifest: CompiledAgentNodeManifest | CompiledAgentResources,
resolveSourcePath: (sourcePath: string) => string,
): Promise<CompiledModuleMap["nodes"][string]["modules"]> {
const mountScopes = new Map(
manifest.extensionMounts.map((mount) => [mount.mountSourceId, mount.packageNamespace]),
@@ -89,7 +96,7 @@ async function hydrateCompiledNodeScope(
),
)
: memoizeModuleNamespaceFactories(
await loadAuthoredModuleNamespace(binding.backing.sourcePath, {
await loadAuthoredModuleNamespace(resolveSourcePath(binding.backing.sourcePath), {
externalDependencies: binding.backing.externalDependencies,
extensionScopeNamespace: resolveCompiledModuleExtensionScopeNamespace(binding),
}),
@@ -0,0 +1,113 @@
import { access, mkdir, realpath, rename } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { prewarmBuiltAppSandboxes } from "#execution/sandbox/prewarm.js";
import { useScenarioApp } from "#internal/testing/scenario-app.js";
import { buildApplication } from "./build-application.js";
import { startProductionServer } from "./start-production-server.js";
describe("relocated production applications", () => {
const scenarioApp = useScenarioApp();
it("prewarms and boots a moved workspace with app and extension TypeScript aliases", async () => {
const workspace = await scenarioApp({
name: "relocated-production",
installDependencies: true,
dependencies: { "just-bash": "3.1.0" },
files: {
"pnpm-workspace.yaml": "packages:\n - apps/*\n - packages/*\n",
"apps/service/package.json": JSON.stringify({
name: "relocated-service",
type: "module",
dependencies: { "@acme/relocated": "workspace:*" },
}),
"apps/service/tsconfig.json": JSON.stringify({
compilerOptions: { baseUrl: ".", paths: { "@/*": ["./lib/*"] } },
}),
"apps/service/lib/marker.ts": 'export const marker = "app-bootstrap";',
"apps/service/agent/agent.ts": 'export default { model: "openai/gpt-5.4" };',
"apps/service/agent/instructions.md": "Use the available tools.",
"apps/service/agent/skills/probe.md":
"---\ndescription: Probe the sandbox.\n---\nProbe content.",
"apps/service/agent/sandbox/sandbox.ts": [
'import { justbash } from "eve/sandbox/just-bash";',
'import { marker } from "@/marker";',
"export default {",
" backend: justbash(),",
' revalidationKey: () => "relocated-v1",',
" async bootstrap({ use }) {",
" const sandbox = await use();",
" const result = await sandbox.run({ command: `echo ${marker}` });",
' if (result.stdout.trim() !== "app-bootstrap") throw new Error("Wrong app alias");',
" },",
"};",
].join("\n"),
"apps/service/agent/extensions/acme.ts":
'import extension from "@acme/relocated"; export default extension();',
"packages/extension/package.json": JSON.stringify({
name: "@acme/relocated",
type: "module",
exports: "./extension/extension.ts",
eve: { extension: { source: "source", dist: "extension" } },
}),
"packages/extension/tsconfig.json": JSON.stringify({
compilerOptions: { baseUrl: ".", paths: { "@/*": ["./lib/*"] } },
}),
"packages/extension/lib/marker.ts": 'export const marker = "extension-tool";',
"packages/extension/extension/extension.ts":
'import { defineExtension } from "eve/extension"; export default defineExtension();',
"packages/extension/extension/tools/probe.ts": [
'import { marker } from "@/marker";',
'if (marker !== "extension-tool") throw new Error("Wrong extension alias");',
'export default { description: "Probe the extension.", execute: () => marker };',
].join("\n"),
"packages/extension/extension/_manifest.json": JSON.stringify({
kind: "eve-extension",
formatVersion: 1,
builtWithEve: "0.0.0-test",
requires: { extension: 1, tool: 1 },
}),
},
});
const root = await realpath(workspace.appRoot);
const buildRoot = join(root, "build-machine");
const runtimeRoot = join(root, "runtime-machine");
await mkdir(buildRoot);
for (const entry of [
"apps",
"packages",
"node_modules",
"pnpm-workspace.yaml",
"package.json",
]) {
await rename(join(root, entry), join(buildRoot, entry));
}
const appRoot = join(buildRoot, "apps", "service");
await buildApplication(appRoot, { skipVercelSandboxPrewarm: false });
await rename(buildRoot, runtimeRoot);
await expect(access(buildRoot)).rejects.toMatchObject({ code: "ENOENT" });
const runtimeAppRoot = join(runtimeRoot, "apps", "service");
const prewarmedRoots: string[] = [];
const seededPaths: string[] = [];
await prewarmBuiltAppSandboxes({
appRoot: runtimeAppRoot,
dispatch: async ({ backend, input }) => {
prewarmedRoots.push(input.runtimeContext.appRoot);
seededPaths.push(...(input.seedFiles ?? []).map((file) => file.path));
return await backend.prewarm(input);
},
});
expect(prewarmedRoots).toEqual([runtimeAppRoot]);
expect(seededPaths).toContain("$HOME/.agents/skills/probe/SKILL.md");
const server = await startProductionServer(runtimeAppRoot, { host: "127.0.0.1", port: 0 });
try {
expect((await fetch(new URL("/eve/v1/health", server.url))).status).toBe(200);
} finally {
await server.close();
}
});
});