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.
Files changed (40) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/docs/public/sandbox.md +25 -0
  3. package/dist/src/chunks/{dev-authored-source-watcher-DKDaaPea.js → dev-authored-source-watcher-BLzYWh05.js} +1 -1
  4. package/dist/src/chunks/host-DREC8e8Z.js +65 -0
  5. package/dist/src/chunks/{paths-DZTgjrW-.js → paths-C6sp4T2U.js} +25 -25
  6. package/dist/src/chunks/{prewarm-BELT37PI.js → prewarm-hz8p2jlZ.js} +1 -1
  7. package/dist/src/cli/commands/info.js +1 -1
  8. package/dist/src/cli/run.js +1 -1
  9. package/dist/src/evals/cli/eval.js +1 -1
  10. package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
  11. package/dist/src/execution/sandbox/bindings/vercel.js +38 -6
  12. package/dist/src/harness/action-result-helpers.d.ts +9 -6
  13. package/dist/src/harness/action-result-helpers.js +23 -16
  14. package/dist/src/harness/model-call-error.d.ts +16 -0
  15. package/dist/src/harness/model-call-error.js +71 -0
  16. package/dist/src/harness/provider-tools.d.ts +33 -2
  17. package/dist/src/harness/provider-tools.js +81 -0
  18. package/dist/src/harness/step-hooks.d.ts +21 -0
  19. package/dist/src/harness/step-hooks.js +7 -2
  20. package/dist/src/harness/tool-loop.js +284 -143
  21. package/dist/src/harness/tools.d.ts +12 -0
  22. package/dist/src/harness/tools.js +23 -5
  23. package/dist/src/internal/application/package.js +1 -1
  24. package/dist/src/internal/nitro/host/build-application.js +67 -1
  25. package/dist/src/internal/workflow-bundle/ash-service-route-output.d.ts +4 -0
  26. package/dist/src/internal/workflow-bundle/ash-service-route-output.js +134 -0
  27. package/dist/src/internal/workflow-bundle/vercel-workflow-output.d.ts +17 -0
  28. package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +141 -1
  29. package/dist/src/public/definitions/connections/mcp.js +2 -0
  30. package/dist/src/public/definitions/tool.js +2 -0
  31. package/dist/src/public/next/index.js +7 -2
  32. package/dist/src/public/sandbox/backends/vercel.d.ts +7 -0
  33. package/dist/src/public/sandbox/backends/vercel.js +7 -0
  34. package/dist/src/public/sandbox/vercel-sandbox.d.ts +14 -4
  35. package/dist/src/public/tool-result-narrowing.d.ts +10 -7
  36. package/dist/src/public/tool-result-narrowing.js +42 -13
  37. package/dist/src/runtime/resolve-connection.js +5 -2
  38. package/dist/src/runtime/resolve-tool.js +5 -2
  39. package/package.json +1 -1
  40. 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
  }
@@ -1,4 +1,6 @@
1
+ import { stampDefinitionKey } from "#public/tool-result-narrowing.js";
1
2
  export function defineTool(definition) {
3
+ stampDefinitionKey(definition, `tool:${definition.description}`);
2
4
  return definition;
3
5
  }
4
6
  /**
@@ -83,10 +83,15 @@ async function resolveExistingRewrites(rewrites) {
83
83
  }
84
84
  function mergeRewriteRules(existing, ashRules) {
85
85
  if (existing === undefined) {
86
- return ashRules;
86
+ return {
87
+ beforeFiles: ashRules,
88
+ };
87
89
  }
88
90
  if (!isRewriteSections(existing)) {
89
- return [...ashRules, ...existing];
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`, `source`, `signal`)
10
- * are excluded — the framework owns those and overrides any author-
11
- * supplied values.
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" | "source" | "signal">;
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 the path-derived runtime name onto an authored definition
43
- * object as a non-enumerable symbol property.
44
- *
45
- * Called by the resolution pipeline (`resolveToolDefinition`,
46
- * `resolveConnectionDefinition`) so {@link toolResultFrom} can read
47
- * the name back without a separate lookup structure.
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 stampDefinitionSource(definition: object, entry: DefinitionSourceEntry): void;
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
- * Stamps the path-derived runtime name onto an authored definition
4
- * object as a non-enumerable symbol property.
5
- *
6
- * Called by the resolution pipeline (`resolveToolDefinition`,
7
- * `resolveConnectionDefinition`) so {@link toolResultFrom} can read
8
- * the name back without a separate lookup structure.
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 stampDefinitionSource(definition, entry) {
11
- Object.defineProperty(definition, DEFINITION_SOURCE, { value: entry });
25
+ export function stampDefinitionKey(definition, key) {
26
+ Object.defineProperty(definition, DEFINITION_KEY, { value: key });
12
27
  }
13
- function readDefinitionSource(definition) {
14
- if (DEFINITION_SOURCE in definition) {
15
- return definition[DEFINITION_SOURCE];
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 entry = readDefinitionSource(source);
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 { stampDefinitionSource } from "#public/tool-result-narrowing.js";
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
- stampDefinitionSource(resolvedRecord, { kind: "connection", name: definition.connectionName });
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 { stampDefinitionSource } from "#public/tool-result-narrowing.js";
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
- stampDefinitionSource(resolvedRecord, { kind: "tool", name: definition.name });
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"