experimental-ash 0.22.0 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/docs/public/sandbox.md +25 -0
- package/dist/src/chunks/{dev-authored-source-watcher-DKDaaPea.js → dev-authored-source-watcher-BLzYWh05.js} +1 -1
- package/dist/src/chunks/host-DREC8e8Z.js +65 -0
- package/dist/src/chunks/{paths-DZTgjrW-.js → paths-C6sp4T2U.js} +25 -25
- package/dist/src/chunks/{prewarm-BELT37PI.js → prewarm-hz8p2jlZ.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.js +38 -6
- package/dist/src/harness/action-result-helpers.d.ts +9 -6
- package/dist/src/harness/action-result-helpers.js +23 -16
- package/dist/src/harness/model-call-error.d.ts +16 -0
- package/dist/src/harness/model-call-error.js +71 -0
- package/dist/src/harness/provider-tools.d.ts +33 -2
- package/dist/src/harness/provider-tools.js +81 -0
- package/dist/src/harness/step-hooks.d.ts +21 -0
- package/dist/src/harness/step-hooks.js +7 -2
- package/dist/src/harness/tool-loop.js +284 -143
- package/dist/src/harness/tools.d.ts +12 -0
- package/dist/src/harness/tools.js +23 -5
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/nitro/host/build-application.js +67 -1
- package/dist/src/internal/workflow-bundle/ash-service-route-output.d.ts +4 -0
- package/dist/src/internal/workflow-bundle/ash-service-route-output.js +134 -0
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.d.ts +17 -0
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +141 -1
- package/dist/src/public/definitions/connections/mcp.js +2 -0
- package/dist/src/public/definitions/tool.js +2 -0
- package/dist/src/public/next/index.js +7 -2
- package/dist/src/public/sandbox/backends/vercel.d.ts +7 -0
- package/dist/src/public/sandbox/backends/vercel.js +7 -0
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +14 -4
- package/dist/src/public/tool-result-narrowing.d.ts +10 -7
- package/dist/src/public/tool-result-narrowing.js +42 -13
- package/dist/src/runtime/resolve-connection.js +5 -2
- package/dist/src/runtime/resolve-tool.js +5 -2
- package/package.json +1 -1
- package/dist/src/chunks/host-Btr4S69C.js +0 -22
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { build as buildNitro, copyPublicAssets, prepare, prerender } from "nitro/builder";
|
|
4
4
|
import { resolvePackageRoot } from "#internal/application/package.js";
|
|
5
5
|
import { prepareAshVersionedCacheDirectory, writeAshVersionedCacheMetadata, } from "#internal/application/cache-metadata.js";
|
|
6
6
|
import { resolveNitroSurfaceOutputDirectory } from "#internal/application/paths.js";
|
|
7
7
|
import { WorkflowBundleBuilder } from "#internal/workflow-bundle/builder.js";
|
|
8
|
+
import { normalizeAshVercelFunctionOutput } from "#internal/workflow-bundle/vercel-workflow-output.js";
|
|
8
9
|
import { createApplicationNitro } from "#internal/nitro/host/create-application-nitro.js";
|
|
9
10
|
import { emitVercelAgentSummary } from "#internal/nitro/host/build-vercel-agent-summary.js";
|
|
10
11
|
import { prepareApplicationHost } from "#internal/nitro/host/prepare-application-host.js";
|
|
@@ -12,6 +13,67 @@ import { runVercelBuildPrewarm } from "#internal/nitro/host/vercel-build-prewarm
|
|
|
12
13
|
function trimTrailingSlash(path) {
|
|
13
14
|
return path.replace(/[\\/]+$/, "");
|
|
14
15
|
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function normalizeEntrypoint(rootDir, entrypoint) {
|
|
20
|
+
if (typeof entrypoint !== "string" || entrypoint.trim().length === 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return resolve(rootDir, entrypoint);
|
|
24
|
+
}
|
|
25
|
+
function resolveNextAshServicePrefix(input) {
|
|
26
|
+
if (!isRecord(input.config) || !isRecord(input.config.experimentalServices)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
let hasNextService = false;
|
|
30
|
+
let servicePrefix;
|
|
31
|
+
for (const service of Object.values(input.config.experimentalServices)) {
|
|
32
|
+
if (!isRecord(service)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (service.framework === "nextjs") {
|
|
36
|
+
hasNextService = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (service.framework !== "ash") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const ashEntrypoint = normalizeEntrypoint(input.configRoot, service.entrypoint);
|
|
43
|
+
const routePrefix = typeof service.routePrefix === "string" ? service.routePrefix.trim() : "";
|
|
44
|
+
if (ashEntrypoint === input.appRoot && routePrefix.length > 0 && routePrefix !== "/") {
|
|
45
|
+
servicePrefix = routePrefix;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return hasNextService ? servicePrefix : undefined;
|
|
49
|
+
}
|
|
50
|
+
async function resolveNextAshServicePrefixForVercelFunctionOutput(appRoot) {
|
|
51
|
+
let currentDir = appRoot;
|
|
52
|
+
while (true) {
|
|
53
|
+
try {
|
|
54
|
+
const configPath = join(currentDir, "vercel.json");
|
|
55
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
56
|
+
const servicePrefix = resolveNextAshServicePrefix({
|
|
57
|
+
appRoot,
|
|
58
|
+
configRoot: currentDir,
|
|
59
|
+
config,
|
|
60
|
+
});
|
|
61
|
+
if (servicePrefix !== undefined) {
|
|
62
|
+
return servicePrefix;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const parentDir = dirname(currentDir);
|
|
71
|
+
if (parentDir === currentDir) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
currentDir = parentDir;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
15
77
|
async function readVercelServerRuntime(outputDir) {
|
|
16
78
|
try {
|
|
17
79
|
const config = JSON.parse(await readFile(join(outputDir, "functions", "__server.func", ".vc-config.json"), "utf8"));
|
|
@@ -99,6 +161,10 @@ export async function buildApplication(rootDir) {
|
|
|
99
161
|
outputDir: outputDirectory,
|
|
100
162
|
workflowBuildDir: preparedHost.workflowBuildDir,
|
|
101
163
|
});
|
|
164
|
+
const servicePrefix = await resolveNextAshServicePrefixForVercelFunctionOutput(preparedHost.appRoot);
|
|
165
|
+
if (servicePrefix !== undefined) {
|
|
166
|
+
await normalizeAshVercelFunctionOutput(outputDirectory, { servicePrefix });
|
|
167
|
+
}
|
|
102
168
|
await emitVercelAgentSummary({
|
|
103
169
|
manifest: preparedHost.compileResult.manifest,
|
|
104
170
|
appRoot: preparedHost.appRoot,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const ASH_SHARED_SERVER_FUNCTION_PATH = "ash/__server.func";
|
|
2
|
+
export declare function isAshVercelFunctionPath(path: string): boolean;
|
|
3
|
+
export declare function normalizeAshVercelRoutes(routes: readonly unknown[], servicePrefix: string | undefined): unknown[];
|
|
4
|
+
export declare function applyAshServiceRoutePrefixWrapper(functionPath: string, servicePrefix: string): Promise<void>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const ASH_SHARED_SERVER_FUNCTION_PATH = "ash/__server.func";
|
|
4
|
+
const ASH_SHARED_SERVER_ROUTE_DESTINATION = "/ash/__server";
|
|
5
|
+
const ASH_SERVICE_ROUTE_PREFIX_WRAPPER = "index.__ash_service_route_prefix.mjs";
|
|
6
|
+
const ASH_VERCEL_FUNCTION_PREFIXES = ["ash/", ".well-known/workflow/"];
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
export function isAshVercelFunctionPath(path) {
|
|
11
|
+
return ASH_VERCEL_FUNCTION_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
12
|
+
}
|
|
13
|
+
export function normalizeAshVercelRoutes(routes, servicePrefix) {
|
|
14
|
+
return routes
|
|
15
|
+
.filter(isAshVercelRoute)
|
|
16
|
+
.map((route) => normalizeAshVercelRoute(route, servicePrefix));
|
|
17
|
+
}
|
|
18
|
+
function isAshVercelRoute(route) {
|
|
19
|
+
if (!isRecord(route)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if ("handle" in route) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const src = typeof route.src === "string" ? route.src : "";
|
|
26
|
+
const dest = typeof route.dest === "string" ? route.dest : "";
|
|
27
|
+
return isAshVercelRoutePath(src) || isAshVercelRoutePath(dest);
|
|
28
|
+
}
|
|
29
|
+
function isAshVercelRoutePath(path) {
|
|
30
|
+
return path.includes("/ash/v1") || path.includes("/.well-known/workflow/");
|
|
31
|
+
}
|
|
32
|
+
function isAshProtocolRoutePath(path) {
|
|
33
|
+
return path.includes("/ash/v1");
|
|
34
|
+
}
|
|
35
|
+
function normalizeAshVercelRoute(route, servicePrefix) {
|
|
36
|
+
if (!isRecord(route) || "handle" in route || typeof route.src !== "string") {
|
|
37
|
+
return route;
|
|
38
|
+
}
|
|
39
|
+
const shouldUseSharedServerFunction = isAshProtocolRoutePath(route.src) ||
|
|
40
|
+
(typeof route.dest === "string" && isAshProtocolRoutePath(route.dest));
|
|
41
|
+
const nextRoute = {
|
|
42
|
+
...route,
|
|
43
|
+
src: prefixAshVercelRoutePath(route.src, servicePrefix),
|
|
44
|
+
};
|
|
45
|
+
if (shouldUseSharedServerFunction) {
|
|
46
|
+
nextRoute.dest = ASH_SHARED_SERVER_ROUTE_DESTINATION;
|
|
47
|
+
}
|
|
48
|
+
return nextRoute;
|
|
49
|
+
}
|
|
50
|
+
function prefixAshVercelRoutePath(path, servicePrefix) {
|
|
51
|
+
if (servicePrefix === undefined ||
|
|
52
|
+
servicePrefix === "/" ||
|
|
53
|
+
!isAshVercelRoutePath(path) ||
|
|
54
|
+
path.includes(servicePrefix)) {
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
const normalizedPrefix = servicePrefix.endsWith("/") ? servicePrefix.slice(0, -1) : servicePrefix;
|
|
58
|
+
const escapedPrefix = normalizedPrefix.replaceAll("/", "\\/");
|
|
59
|
+
if (path.includes(escapedPrefix)) {
|
|
60
|
+
return path;
|
|
61
|
+
}
|
|
62
|
+
if (path.startsWith("^(?:/")) {
|
|
63
|
+
return `^(?:${normalizedPrefix}${path.slice(4)}`;
|
|
64
|
+
}
|
|
65
|
+
if (path.startsWith("^/")) {
|
|
66
|
+
return `^${normalizedPrefix}${path.slice(1)}`;
|
|
67
|
+
}
|
|
68
|
+
if (path.startsWith("/")) {
|
|
69
|
+
return `${normalizedPrefix}${path}`;
|
|
70
|
+
}
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
export async function applyAshServiceRoutePrefixWrapper(functionPath, servicePrefix) {
|
|
74
|
+
const configPath = join(functionPath, ".vc-config.json");
|
|
75
|
+
const wrapperPath = join(functionPath, ASH_SERVICE_ROUTE_PREFIX_WRAPPER);
|
|
76
|
+
const rawConfig = JSON.parse(await readFile(configPath, "utf8"));
|
|
77
|
+
const config = isRecord(rawConfig) ? rawConfig : {};
|
|
78
|
+
await writeFile(wrapperPath, createAshServiceRoutePrefixWrapper(servicePrefix));
|
|
79
|
+
await writeFile(configPath, `${JSON.stringify({
|
|
80
|
+
...config,
|
|
81
|
+
handler: ASH_SERVICE_ROUTE_PREFIX_WRAPPER,
|
|
82
|
+
}, null, 2)}\n`);
|
|
83
|
+
}
|
|
84
|
+
function createAshServiceRoutePrefixWrapper(servicePrefix) {
|
|
85
|
+
return `
|
|
86
|
+
import { Server } from "node:http";
|
|
87
|
+
|
|
88
|
+
const SERVICE_PREFIX = ${JSON.stringify(normalizeServiceRoutePrefix(servicePrefix))};
|
|
89
|
+
const PATCH_SYMBOL = Symbol.for("ash.service.route-prefix-strip.patch");
|
|
90
|
+
|
|
91
|
+
function stripServiceRoutePrefix(requestUrl) {
|
|
92
|
+
if (typeof requestUrl !== "string" || requestUrl === "*") {
|
|
93
|
+
return requestUrl;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const queryIndex = requestUrl.indexOf("?");
|
|
97
|
+
const rawPath = queryIndex === -1 ? requestUrl : requestUrl.slice(0, queryIndex);
|
|
98
|
+
const query = queryIndex === -1 ? "" : requestUrl.slice(queryIndex);
|
|
99
|
+
const path = rawPath.startsWith("/") ? rawPath : \`/\${rawPath}\`;
|
|
100
|
+
|
|
101
|
+
if (path === SERVICE_PREFIX) {
|
|
102
|
+
return \`/\${query}\`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (path.startsWith(\`\${SERVICE_PREFIX}/\`)) {
|
|
106
|
+
return path.slice(SERVICE_PREFIX.length) + query;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return path + query;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!globalThis[PATCH_SYMBOL]) {
|
|
113
|
+
globalThis[PATCH_SYMBOL] = true;
|
|
114
|
+
const originalEmit = Server.prototype.emit;
|
|
115
|
+
Server.prototype.emit = function patchedEmit(event, request, ...args) {
|
|
116
|
+
if (event === "request" && request && typeof request.url === "string") {
|
|
117
|
+
request.url = stripServiceRoutePrefix(request.url);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return originalEmit.call(this, event, request, ...args);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const originalModule = await import("./index.mjs");
|
|
125
|
+
const entrypoint = originalModule?.default ?? originalModule;
|
|
126
|
+
|
|
127
|
+
export default entrypoint;
|
|
128
|
+
`.trimStart();
|
|
129
|
+
}
|
|
130
|
+
function normalizeServiceRoutePrefix(servicePrefix) {
|
|
131
|
+
const prefixed = servicePrefix.startsWith("/") ? servicePrefix : `/${servicePrefix}`;
|
|
132
|
+
const normalized = prefixed.replace(/\/+$/, "");
|
|
133
|
+
return normalized.length === 0 ? "/" : normalized;
|
|
134
|
+
}
|
|
@@ -26,6 +26,23 @@ export declare function copyNitroFunctionDirectory(input: {
|
|
|
26
26
|
sourcePath: string;
|
|
27
27
|
targetPath: string;
|
|
28
28
|
}): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Keeps only Ash-owned Vercel function output and rewrites Ash route function
|
|
31
|
+
* symlinks to a shared Ash-owned server function.
|
|
32
|
+
*
|
|
33
|
+
* Nitro emits generic app routes such as `index.func -> ./__server.func` for
|
|
34
|
+
* Ash's standalone landing page. In a multi-service Next.js deployment those
|
|
35
|
+
* root aliases collide with Next's own functions. The Next integration only
|
|
36
|
+
* proxies Ash's `/ash/v1/**` transport routes, so Vercel output should expose
|
|
37
|
+
* those route functions and workflow trigger functions, not Ash's root page.
|
|
38
|
+
*
|
|
39
|
+
* Nitro also dedupes every route function through `__server.func`. Preserve
|
|
40
|
+
* that model by copying the shared target once into the Ash-owned tree and
|
|
41
|
+
* repointing Ash route aliases at it before pruning the root target.
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizeAshVercelFunctionOutput(outputDir: string, options?: {
|
|
44
|
+
readonly servicePrefix?: string;
|
|
45
|
+
}): Promise<void>;
|
|
29
46
|
/**
|
|
30
47
|
* Bundles one package-owned workflow handler into a standalone Vercel function
|
|
31
48
|
* directory without inheriting Nitro's hosted server chunks.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { cp, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { cp, mkdir, readdir, readFile, realpath, rename, rm, symlink, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, extname, join, relative } from "node:path";
|
|
3
3
|
import { buildWithNitroRolldown, getSingleRolldownChunk, } from "#internal/bundler/nitro-rolldown.js";
|
|
4
|
+
import { applyAshServiceRoutePrefixWrapper, ASH_SHARED_SERVER_FUNCTION_PATH, isAshVercelFunctionPath, normalizeAshVercelRoutes, } from "#internal/workflow-bundle/ash-service-route-output.js";
|
|
4
5
|
// just-bash exposes native optional codecs for xz/zstd support. Those packages
|
|
5
6
|
// are feature-gated at runtime and should stay external so workflow step
|
|
6
7
|
// bundles do not try to inline platform-specific `.node` artifacts.
|
|
@@ -63,6 +64,145 @@ export async function copyNitroFunctionDirectory(input) {
|
|
|
63
64
|
recursive: true,
|
|
64
65
|
});
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Keeps only Ash-owned Vercel function output and rewrites Ash route function
|
|
69
|
+
* symlinks to a shared Ash-owned server function.
|
|
70
|
+
*
|
|
71
|
+
* Nitro emits generic app routes such as `index.func -> ./__server.func` for
|
|
72
|
+
* Ash's standalone landing page. In a multi-service Next.js deployment those
|
|
73
|
+
* root aliases collide with Next's own functions. The Next integration only
|
|
74
|
+
* proxies Ash's `/ash/v1/**` transport routes, so Vercel output should expose
|
|
75
|
+
* those route functions and workflow trigger functions, not Ash's root page.
|
|
76
|
+
*
|
|
77
|
+
* Nitro also dedupes every route function through `__server.func`. Preserve
|
|
78
|
+
* that model by copying the shared target once into the Ash-owned tree and
|
|
79
|
+
* repointing Ash route aliases at it before pruning the root target.
|
|
80
|
+
*/
|
|
81
|
+
export async function normalizeAshVercelFunctionOutput(outputDir, options = {}) {
|
|
82
|
+
const functionsDir = join(outputDir, "functions");
|
|
83
|
+
const sharedFunctionPath = await prepareSharedAshServerFunction(functionsDir);
|
|
84
|
+
if (sharedFunctionPath !== null) {
|
|
85
|
+
if (options.servicePrefix !== undefined) {
|
|
86
|
+
await applyAshServiceRoutePrefixWrapper(sharedFunctionPath, options.servicePrefix);
|
|
87
|
+
}
|
|
88
|
+
await repointAshFunctionSymlinksInDirectory(functionsDir, sharedFunctionPath);
|
|
89
|
+
}
|
|
90
|
+
await pruneNonAshFunctionEntries(functionsDir, functionsDir);
|
|
91
|
+
await pruneNonAshVercelRoutes(outputDir, options.servicePrefix);
|
|
92
|
+
}
|
|
93
|
+
async function prepareSharedAshServerFunction(functionsDir) {
|
|
94
|
+
const rootServerFunctionPath = join(functionsDir, "__server.func");
|
|
95
|
+
const sharedFunctionPath = join(functionsDir, ASH_SHARED_SERVER_FUNCTION_PATH);
|
|
96
|
+
let sourcePath;
|
|
97
|
+
try {
|
|
98
|
+
sourcePath = await realpath(rootServerFunctionPath);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const stagingPath = `${sharedFunctionPath}.ash-staging`;
|
|
107
|
+
await mkdir(dirname(sharedFunctionPath), {
|
|
108
|
+
recursive: true,
|
|
109
|
+
});
|
|
110
|
+
await rm(stagingPath, {
|
|
111
|
+
force: true,
|
|
112
|
+
recursive: true,
|
|
113
|
+
});
|
|
114
|
+
await cp(sourcePath, stagingPath, {
|
|
115
|
+
dereference: true,
|
|
116
|
+
recursive: true,
|
|
117
|
+
});
|
|
118
|
+
await rm(sharedFunctionPath, {
|
|
119
|
+
force: true,
|
|
120
|
+
recursive: true,
|
|
121
|
+
});
|
|
122
|
+
await rename(stagingPath, sharedFunctionPath);
|
|
123
|
+
return sharedFunctionPath;
|
|
124
|
+
}
|
|
125
|
+
async function repointAshFunctionSymlinksInDirectory(directoryPath, sharedFunctionPath, functionsDir = directoryPath) {
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = await readdir(directoryPath, { withFileTypes: true });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
await Promise.all(entries.map(async (entry) => {
|
|
137
|
+
const entryPath = join(directoryPath, entry.name);
|
|
138
|
+
const relativeFunctionPath = normalizeVercelOutputPath(relative(functionsDir, entryPath));
|
|
139
|
+
if (entry.isSymbolicLink()) {
|
|
140
|
+
if (entry.name.endsWith(".func") && isAshVercelFunctionPath(relativeFunctionPath)) {
|
|
141
|
+
await repointFunctionSymlink(entryPath, sharedFunctionPath);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (entry.isDirectory() && !entry.name.endsWith(".func")) {
|
|
146
|
+
await repointAshFunctionSymlinksInDirectory(entryPath, sharedFunctionPath, functionsDir);
|
|
147
|
+
}
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
async function repointFunctionSymlink(functionPath, sharedFunctionPath) {
|
|
151
|
+
await rm(functionPath, {
|
|
152
|
+
force: true,
|
|
153
|
+
recursive: true,
|
|
154
|
+
});
|
|
155
|
+
await symlink(normalizeVercelOutputPath(relative(dirname(functionPath), sharedFunctionPath)), functionPath, "dir");
|
|
156
|
+
}
|
|
157
|
+
async function pruneNonAshFunctionEntries(functionsDir, directoryPath) {
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = await readdir(directoryPath, { withFileTypes: true });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
await Promise.all(entries.map(async (entry) => {
|
|
169
|
+
const entryPath = join(directoryPath, entry.name);
|
|
170
|
+
const relativeFunctionPath = normalizeVercelOutputPath(relative(functionsDir, entryPath));
|
|
171
|
+
if (entry.name.endsWith(".func")) {
|
|
172
|
+
if (!isAshVercelFunctionPath(relativeFunctionPath)) {
|
|
173
|
+
await rm(entryPath, {
|
|
174
|
+
force: true,
|
|
175
|
+
recursive: true,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
await pruneNonAshFunctionEntries(functionsDir, entryPath);
|
|
182
|
+
}
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
async function pruneNonAshVercelRoutes(outputDir, servicePrefix) {
|
|
186
|
+
const configPath = join(outputDir, "config.json");
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.routes)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
parsed.routes = normalizeAshVercelRoutes(parsed.routes, servicePrefix);
|
|
201
|
+
await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
202
|
+
}
|
|
203
|
+
function normalizeVercelOutputPath(path) {
|
|
204
|
+
return path.replaceAll("\\", "/");
|
|
205
|
+
}
|
|
66
206
|
/**
|
|
67
207
|
* Bundles one package-owned workflow handler into a standalone Vercel function
|
|
68
208
|
* directory without inheriting Nitro's hosted server chunks.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeAuthorizationSpec } from "#runtime/connections/validate-authorization.js";
|
|
2
|
+
import { stampDefinitionKey } from "#public/tool-result-narrowing.js";
|
|
2
3
|
/**
|
|
3
4
|
* Defines an MCP client connection.
|
|
4
5
|
*
|
|
@@ -14,5 +15,6 @@ export function defineMcpClientConnection(definition) {
|
|
|
14
15
|
if (definition.auth !== undefined) {
|
|
15
16
|
definition.auth = normalizeAuthorizationSpec(definition.auth, "defineMcpClientConnection:");
|
|
16
17
|
}
|
|
18
|
+
stampDefinitionKey(definition, `connection:${definition.url}`);
|
|
17
19
|
return definition;
|
|
18
20
|
}
|
|
@@ -83,10 +83,15 @@ async function resolveExistingRewrites(rewrites) {
|
|
|
83
83
|
}
|
|
84
84
|
function mergeRewriteRules(existing, ashRules) {
|
|
85
85
|
if (existing === undefined) {
|
|
86
|
-
return
|
|
86
|
+
return {
|
|
87
|
+
beforeFiles: ashRules,
|
|
88
|
+
};
|
|
87
89
|
}
|
|
88
90
|
if (!isRewriteSections(existing)) {
|
|
89
|
-
return
|
|
91
|
+
return {
|
|
92
|
+
afterFiles: existing,
|
|
93
|
+
beforeFiles: ashRules,
|
|
94
|
+
};
|
|
90
95
|
}
|
|
91
96
|
return {
|
|
92
97
|
...existing,
|
|
@@ -8,6 +8,13 @@ import type { VercelSandboxBootstrapUseOptions, VercelSandboxCreateOptions, Verc
|
|
|
8
8
|
* (template at prewarm, session at first-time create). On resume
|
|
9
9
|
* (`Sandbox.get`), no create happens, so opts are not re-applied.
|
|
10
10
|
*
|
|
11
|
+
* `opts.source`, if supplied, is used only on the template create —
|
|
12
|
+
* the author's snapshot, git revision, or tarball becomes the base
|
|
13
|
+
* layer of the template. Bootstrap, seed files, and framework setup
|
|
14
|
+
* still run on top, and every session derives from the resulting
|
|
15
|
+
* Ash-owned snapshot. `source` is stripped from session creates so the
|
|
16
|
+
* framework's snapshot always wins.
|
|
17
|
+
*
|
|
11
18
|
* `bootstrap({ use })` applies its options to the template via
|
|
12
19
|
* `sandbox.update(...)`; those settings persist into the snapshot.
|
|
13
20
|
* `onSession({ use })` applies its options to the live session via the
|
|
@@ -7,6 +7,13 @@ import { createVercelSandboxBackend } from "#execution/sandbox/bindings/vercel.j
|
|
|
7
7
|
* (template at prewarm, session at first-time create). On resume
|
|
8
8
|
* (`Sandbox.get`), no create happens, so opts are not re-applied.
|
|
9
9
|
*
|
|
10
|
+
* `opts.source`, if supplied, is used only on the template create —
|
|
11
|
+
* the author's snapshot, git revision, or tarball becomes the base
|
|
12
|
+
* layer of the template. Bootstrap, seed files, and framework setup
|
|
13
|
+
* still run on top, and every session derives from the resulting
|
|
14
|
+
* Ash-owned snapshot. `source` is stripped from session creates so the
|
|
15
|
+
* framework's snapshot always wins.
|
|
16
|
+
*
|
|
10
17
|
* `bootstrap({ use })` applies its options to the template via
|
|
11
18
|
* `sandbox.update(...)`; those settings persist into the snapshot.
|
|
12
19
|
* `onSession({ use })` applies its options to the live session via the
|
|
@@ -6,11 +6,21 @@ import type { Sandbox as SdkSandbox, SandboxUpdateParams } from "#compiled/@verc
|
|
|
6
6
|
* session-create). Skipped on resume (`Sandbox.get`) since no create
|
|
7
7
|
* happens there.
|
|
8
8
|
*
|
|
9
|
-
* Framework-injected fields (`name`, `persistent`, `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Framework-injected fields (`name`, `persistent`, `signal`) are
|
|
10
|
+
* excluded — the framework owns those and overrides any author-supplied
|
|
11
|
+
* values.
|
|
12
|
+
*
|
|
13
|
+
* `source` is honored only on the template create at prewarm time, so
|
|
14
|
+
* an author-supplied snapshot, git revision, or tarball becomes the
|
|
15
|
+
* base layer for the template. Bootstrap, seed files, and framework
|
|
16
|
+
* setup (workspace dir, ripgrep) all run on top, and the resulting
|
|
17
|
+
* framework-owned snapshot is what every later session derives from —
|
|
18
|
+
* so `source` is stripped from the session-create path. Ash does not
|
|
19
|
+
* detect external snapshot changes; to pick up a rebuilt external
|
|
20
|
+
* snapshot, force a template rebuild (e.g. by changing the sandbox
|
|
21
|
+
* definition so its template key changes).
|
|
12
22
|
*/
|
|
13
|
-
export type VercelSandboxCreateOptions = Omit<NonNullable<Parameters<typeof SdkSandbox.create>[0]>, "name" | "persistent" | "
|
|
23
|
+
export type VercelSandboxCreateOptions = Omit<NonNullable<Parameters<typeof SdkSandbox.create>[0]>, "name" | "persistent" | "signal">;
|
|
14
24
|
/**
|
|
15
25
|
* Options accepted by the Vercel backend's `bootstrap({ use })` hook.
|
|
16
26
|
* Aliases the Vercel SDK's `SandboxUpdateParams` because bootstrap
|
|
@@ -39,14 +39,17 @@ export type DefinitionSourceEntry = {
|
|
|
39
39
|
readonly name: string;
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
|
-
* Stamps
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
* Stamps a stable content-derived key on the definition so it can be
|
|
43
|
+
* identified across module instances. Called by `defineTool` and
|
|
44
|
+
* `defineMcpClientConnection` at definition time.
|
|
45
|
+
*/
|
|
46
|
+
export declare function stampDefinitionKey(definition: object, key: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Registers a definition key → source entry mapping in the global
|
|
49
|
+
* registry. Called by the resolution pipeline after loading the
|
|
50
|
+
* authored module.
|
|
48
51
|
*/
|
|
49
|
-
export declare function
|
|
52
|
+
export declare function registerDefinitionSource(key: string, entry: DefinitionSourceEntry): void;
|
|
50
53
|
/**
|
|
51
54
|
* Overloaded signature for {@link toolResultFrom}.
|
|
52
55
|
*/
|
|
@@ -1,21 +1,47 @@
|
|
|
1
|
-
const DEFINITION_SOURCE = Symbol("ash.definitionSource");
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
* Cross-instance symbol for reading the stable definition key stamped
|
|
3
|
+
* by `defineTool` / `defineMcpClientConnection`. `Symbol.for` ensures
|
|
4
|
+
* both the resolution pipeline's module copy and the user's import
|
|
5
|
+
* share the same property key.
|
|
6
|
+
*/
|
|
7
|
+
const DEFINITION_KEY = Symbol.for("experimental-ash.definition-source-key");
|
|
8
|
+
/**
|
|
9
|
+
* Global registry mapping definition keys to their resolved runtime
|
|
10
|
+
* names. Rooted on `globalThis` via `Symbol.for` so every module
|
|
11
|
+
* copy shares one registry — same pattern as `context/key.ts`
|
|
12
|
+
* (`KEY_REGISTRY_GLOBAL_KEY`).
|
|
13
|
+
*/
|
|
14
|
+
const REGISTRY_SYMBOL = Symbol.for("experimental-ash.definition-source-registry");
|
|
15
|
+
const registryContainer = globalThis;
|
|
16
|
+
if (registryContainer[REGISTRY_SYMBOL] === undefined) {
|
|
17
|
+
registryContainer[REGISTRY_SYMBOL] = new Map();
|
|
18
|
+
}
|
|
19
|
+
const definitionSourceRegistry = registryContainer[REGISTRY_SYMBOL];
|
|
20
|
+
/**
|
|
21
|
+
* Stamps a stable content-derived key on the definition so it can be
|
|
22
|
+
* identified across module instances. Called by `defineTool` and
|
|
23
|
+
* `defineMcpClientConnection` at definition time.
|
|
9
24
|
*/
|
|
10
|
-
export function
|
|
11
|
-
Object.defineProperty(definition,
|
|
25
|
+
export function stampDefinitionKey(definition, key) {
|
|
26
|
+
Object.defineProperty(definition, DEFINITION_KEY, { value: key });
|
|
12
27
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Reads the stable key from a definition, or `undefined` if unstamped.
|
|
30
|
+
*/
|
|
31
|
+
function readDefinitionKey(definition) {
|
|
32
|
+
if (DEFINITION_KEY in definition) {
|
|
33
|
+
return definition[DEFINITION_KEY];
|
|
16
34
|
}
|
|
17
35
|
return undefined;
|
|
18
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Registers a definition key → source entry mapping in the global
|
|
39
|
+
* registry. Called by the resolution pipeline after loading the
|
|
40
|
+
* authored module.
|
|
41
|
+
*/
|
|
42
|
+
export function registerDefinitionSource(key, entry) {
|
|
43
|
+
definitionSourceRegistry.set(key, entry);
|
|
44
|
+
}
|
|
19
45
|
const CONNECTION_TOOL_SEPARATOR = "__";
|
|
20
46
|
/**
|
|
21
47
|
* Narrows a {@link RuntimeActionResult} to a typed tool or connection
|
|
@@ -34,7 +60,10 @@ function toolResultFromImpl(result, source) {
|
|
|
34
60
|
return undefined;
|
|
35
61
|
if (result.isError === true)
|
|
36
62
|
return undefined;
|
|
37
|
-
const
|
|
63
|
+
const key = readDefinitionKey(source);
|
|
64
|
+
if (key === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const entry = definitionSourceRegistry.get(key);
|
|
38
67
|
if (entry === undefined)
|
|
39
68
|
return undefined;
|
|
40
69
|
if (entry.kind === "tool") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expectObjectRecord } from "#internal/authored-module.js";
|
|
2
|
-
import {
|
|
2
|
+
import { registerDefinitionSource } from "#public/tool-result-narrowing.js";
|
|
3
3
|
import { toErrorMessage } from "#shared/errors.js";
|
|
4
4
|
import { normalizeAuthorizationSpec } from "#runtime/connections/validate-authorization.js";
|
|
5
5
|
import { loadResolvedModuleExport, ResolveAgentError } from "#runtime/resolve-helpers.js";
|
|
@@ -23,7 +23,10 @@ export async function resolveConnectionDefinition(definition, moduleMap, nodeId)
|
|
|
23
23
|
nodeId,
|
|
24
24
|
});
|
|
25
25
|
const resolvedRecord = expectObjectRecord(resolvedExportValue, `Expected the connection export "${definition.exportName ?? "default"}" from "${definition.logicalPath}" to return an object.`);
|
|
26
|
-
|
|
26
|
+
registerDefinitionSource(`connection:${resolvedRecord.url}`, {
|
|
27
|
+
kind: "connection",
|
|
28
|
+
name: definition.connectionName,
|
|
29
|
+
});
|
|
27
30
|
const hasAuth = resolvedRecord.auth !== undefined;
|
|
28
31
|
const hasHeaders = resolvedRecord.headers !== undefined;
|
|
29
32
|
const result = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expectFunction, expectObjectRecord } from "#internal/authored-module.js";
|
|
2
|
-
import {
|
|
2
|
+
import { registerDefinitionSource } from "#public/tool-result-narrowing.js";
|
|
3
3
|
import { toErrorMessage } from "#shared/errors.js";
|
|
4
4
|
import { loadResolvedModuleExport, ResolveAgentError } from "#runtime/resolve-helpers.js";
|
|
5
5
|
/**
|
|
@@ -21,7 +21,10 @@ export async function resolveToolDefinition(definition, moduleMap, nodeId) {
|
|
|
21
21
|
nodeId,
|
|
22
22
|
});
|
|
23
23
|
const resolvedRecord = expectObjectRecord(resolvedExportValue, describe(definition, "to return an object"));
|
|
24
|
-
|
|
24
|
+
registerDefinitionSource(`tool:${resolvedRecord.description}`, {
|
|
25
|
+
kind: "tool",
|
|
26
|
+
name: definition.name,
|
|
27
|
+
});
|
|
25
28
|
const execute = expectFunction(resolvedRecord.execute, describe(definition, "to provide an execute function"));
|
|
26
29
|
return {
|
|
27
30
|
description: definition.description,
|