experimental-ash 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +79 -56
  3. package/dist/docs/public/channels/README.md +33 -23
  4. package/dist/docs/public/channels/attachments.md +42 -29
  5. package/dist/src/channel/adapter.d.ts +21 -28
  6. package/dist/src/channel/compiled-channel.d.ts +1 -1
  7. package/dist/src/channel/http.d.ts +29 -0
  8. package/dist/src/channel/http.js +30 -0
  9. package/dist/src/channel/schedule.d.ts +20 -0
  10. package/dist/src/channel/schedule.js +22 -1
  11. package/dist/src/channel/send.d.ts +1 -1
  12. package/dist/src/channel/send.js +22 -1
  13. package/dist/src/channel/types.d.ts +1 -1
  14. package/dist/src/chunks/{client-DBMG7iuf.js → client-BeZ_W7vl.js} +2 -2
  15. package/dist/src/chunks/{dev-authored-source-watcher-BcN7BUDE.js → dev-authored-source-watcher-BFC_yNcP.js} +1 -1
  16. package/dist/src/chunks/host-DMccRKcz.js +22 -0
  17. package/dist/src/chunks/{paths-BYIdCNw9.js → paths-B-aiDznc.js} +26 -26
  18. package/dist/src/chunks/{prewarm-DXhyk7i9.js → prewarm-CCbReSNm.js} +1 -1
  19. package/dist/src/chunks/types-MZUhN0Zy.js +1 -0
  20. package/dist/src/cli/commands/info.js +1 -1
  21. package/dist/src/cli/dev/environment.d.ts +3 -2
  22. package/dist/src/cli/dev/repl.js +1 -1
  23. package/dist/src/cli/run.js +1 -1
  24. package/dist/src/compiled/.vendor-stamp.json +1 -1
  25. package/dist/src/compiled/@vercel/sandbox/index.d.ts +37 -3
  26. package/dist/src/evals/cli/eval.js +1 -1
  27. package/dist/src/execution/sandbox/bindings/local.d.ts +0 -2
  28. package/dist/src/execution/sandbox/bindings/local.js +1 -20
  29. package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
  30. package/dist/src/execution/sandbox/bindings/vercel.js +1 -12
  31. package/dist/src/harness/attachment-staging.js +54 -50
  32. package/dist/src/harness/emission.d.ts +14 -1
  33. package/dist/src/harness/emission.js +15 -2
  34. package/dist/src/harness/tool-loop.js +58 -15
  35. package/dist/src/internal/application/package.js +1 -1
  36. package/dist/src/internal/attachments/url-refs.d.ts +14 -0
  37. package/dist/src/internal/attachments/url-refs.js +20 -0
  38. package/dist/src/internal/nitro/host/configure-nitro-routes.d.ts +0 -1
  39. package/dist/src/internal/nitro/host/configure-nitro-routes.js +24 -17
  40. package/dist/src/internal/nitro/host/create-application-nitro.js +1 -16
  41. package/dist/src/internal/nitro/routes/agent-info/build-agent-info-response.d.ts +87 -0
  42. package/dist/src/internal/nitro/routes/{home-page/build-home-page-response.js → agent-info/build-agent-info-response.js} +6 -6
  43. package/dist/src/internal/nitro/routes/{home-page/load-home-page-data.d.ts → agent-info/load-agent-info-data.d.ts} +8 -8
  44. package/dist/src/internal/nitro/routes/{home-page/load-home-page-data.js → agent-info/load-agent-info-data.js} +7 -8
  45. package/dist/src/internal/nitro/routes/index.d.ts +10 -5
  46. package/dist/src/internal/nitro/routes/index.js +225 -18
  47. package/dist/src/internal/nitro/routes/info.d.ts +14 -0
  48. package/dist/src/internal/nitro/routes/info.js +50 -0
  49. package/dist/src/protocol/routes.d.ts +8 -6
  50. package/dist/src/protocol/routes.js +8 -6
  51. package/dist/src/public/channels/ash.js +1 -6
  52. package/dist/src/public/channels/index.d.ts +1 -1
  53. package/dist/src/public/channels/slack/attachments.d.ts +14 -18
  54. package/dist/src/public/channels/slack/attachments.js +30 -36
  55. package/dist/src/public/channels/slack/index.d.ts +0 -1
  56. package/dist/src/public/channels/slack/slackChannel.js +3 -3
  57. package/dist/src/public/definitions/defineChannel.d.ts +9 -7
  58. package/dist/src/public/definitions/defineChannel.js +5 -11
  59. package/dist/src/public/definitions/sandbox.d.ts +3 -3
  60. package/dist/src/public/sandbox/backends/vercel.d.ts +2 -2
  61. package/dist/src/public/sandbox/index.d.ts +2 -2
  62. package/dist/src/public/sandbox/vercel-sandbox.d.ts +11 -10
  63. package/dist/src/runtime/channels/registry.js +9 -3
  64. package/dist/src/runtime/resolve-channel.js +2 -1
  65. package/dist/src/shared/sandbox-backend.d.ts +4 -4
  66. package/dist/src/shared/sandbox-definition.d.ts +6 -36
  67. package/package.json +1 -1
  68. package/dist/src/chunks/host-33-Sb6vq.js +0 -22
  69. package/dist/src/chunks/types-D9Uv7nU4.js +0 -1
  70. package/dist/src/internal/nitro/host/load-home-page-web-assets.d.ts +0 -12
  71. package/dist/src/internal/nitro/host/load-home-page-web-assets.js +0 -34
  72. package/dist/src/internal/nitro/routes/home-page/build-home-page-response.d.ts +0 -87
  73. package/dist/src/internal/nitro/routes/home.d.ts +0 -6
  74. package/dist/src/internal/nitro/routes/home.js +0 -21
  75. package/dist/src/internal/nitro/routes/web-ui/assets/index-BQa8fbHJ.js +0 -11
  76. package/dist/src/internal/nitro/routes/web-ui/assets/style-Kqb6YxTP.css +0 -2
  77. package/dist/src/internal/nitro/routes/web-ui/index.html +0 -17
  78. package/dist/src/public/sandboxes/vercel-sandbox.d.ts +0 -41
  79. package/dist/src/public/sandboxes/vercel-sandbox.js +0 -1
@@ -0,0 +1,50 @@
1
+ import { buildAgentInfoResponse } from "#internal/nitro/routes/agent-info/build-agent-info-response.js";
2
+ import { loadAgentInfoData, resolveAgentInfoCompiledArtifactsSource, } from "#internal/nitro/routes/agent-info/load-agent-info-data.js";
3
+ import { createUnauthorizedResponse, none, vercelOidc, } from "#public/channels/auth.js";
4
+ /**
5
+ * Resolves the default auth strategy for the agent info endpoint.
6
+ *
7
+ * Mirrors {@link resolveFrameworkAshAuth} (session/stream routes): on
8
+ * Vercel deployments require a valid Vercel OIDC bearer (the always-on
9
+ * same-project bypass lets internal callers in automatically); off
10
+ * Vercel (`ash dev`) accept anonymous requests so local inspection
11
+ * tools and curl-based probes work without setup.
12
+ */
13
+ function resolveAgentInfoAuth() {
14
+ if (process.env.VERCEL) {
15
+ return vercelOidc();
16
+ }
17
+ return none();
18
+ }
19
+ async function createAgentInfoPayload(input) {
20
+ const data = await loadAgentInfoData({
21
+ compiledArtifactsSource: resolveAgentInfoCompiledArtifactsSource({
22
+ appRoot: input.appRoot,
23
+ }),
24
+ });
25
+ return buildAgentInfoResponse(data);
26
+ }
27
+ /**
28
+ * Builds the package-owned JSON inspection response for the current
29
+ * agent (model id, instructions preview, skills, schedules, subagents,
30
+ * tools, sandbox, endpoint catalog).
31
+ *
32
+ * The route is gated by {@link resolveAgentInfoAuth} so the deployed URL
33
+ * does not expose agent metadata to anonymous callers. The same-project
34
+ * bypass on `vercelOidc()` keeps the route reachable to in-deployment
35
+ * callers (`apps/nextjs-ash`-style consumers) without any additional
36
+ * setup.
37
+ */
38
+ export async function handleAgentInfoRequest(input, request) {
39
+ const auth = resolveAgentInfoAuth();
40
+ const result = await auth(request);
41
+ if (result === null) {
42
+ return createUnauthorizedResponse({ challenges: [{ scheme: "Bearer" }] });
43
+ }
44
+ return new Response(JSON.stringify(await createAgentInfoPayload(input)), {
45
+ headers: {
46
+ "cache-control": "no-store",
47
+ "content-type": "application/json; charset=utf-8",
48
+ },
49
+ });
50
+ }
@@ -8,9 +8,15 @@ export declare const ASH_ROUTE_PREFIX = "/ash/v1";
8
8
  */
9
9
  export declare const ASH_HEALTH_ROUTE_PATH = "/ash/v1/health";
10
10
  /**
11
- * Stable framework-owned route exposing home-page inspection JSON.
11
+ * Stable framework-owned route exposing the JSON inspection payload for
12
+ * the current agent (model id, instructions preview, skills, schedules,
13
+ * subagents, tools, sandbox, and the runtime's own endpoint catalog).
14
+ *
15
+ * Default auth on this route is `vercelOidc()` on Vercel deployments
16
+ * (with the same-project bypass for in-deployment callers) and `none()`
17
+ * off Vercel. See `routes/info.ts` for the handler.
12
18
  */
13
- export declare const ASH_HOME_ROUTE_PATH = "/ash/v1/home";
19
+ export declare const ASH_INFO_ROUTE_PATH = "/ash/v1/info";
14
20
  /**
15
21
  * Stable framework-owned route for creating a new session.
16
22
  */
@@ -24,10 +30,6 @@ export declare const ASH_CONTINUE_SESSION_ROUTE_PATTERN = "/ash/v1/session/:sess
24
30
  * Stable framework-owned message stream route pattern.
25
31
  */
26
32
  export declare const ASH_MESSAGE_STREAM_ROUTE_PATTERN = "/ash/v1/session/:sessionId/stream";
27
- /**
28
- * Stable framework-owned route prefix for static home-page UI assets.
29
- */
30
- export declare const ASH_HOME_PAGE_UI_ROUTE_PREFIX = "/ash/v1/ui";
31
33
  /**
32
34
  * Stable framework-owned route pattern for receiving inbound IdP redirects
33
35
  * during in-turn interactive connection authorization.
@@ -8,9 +8,15 @@ export const ASH_ROUTE_PREFIX = "/ash/v1";
8
8
  */
9
9
  export const ASH_HEALTH_ROUTE_PATH = `${ASH_ROUTE_PREFIX}/health`;
10
10
  /**
11
- * Stable framework-owned route exposing home-page inspection JSON.
11
+ * Stable framework-owned route exposing the JSON inspection payload for
12
+ * the current agent (model id, instructions preview, skills, schedules,
13
+ * subagents, tools, sandbox, and the runtime's own endpoint catalog).
14
+ *
15
+ * Default auth on this route is `vercelOidc()` on Vercel deployments
16
+ * (with the same-project bypass for in-deployment callers) and `none()`
17
+ * off Vercel. See `routes/info.ts` for the handler.
12
18
  */
13
- export const ASH_HOME_ROUTE_PATH = `${ASH_ROUTE_PREFIX}/home`;
19
+ export const ASH_INFO_ROUTE_PATH = `${ASH_ROUTE_PREFIX}/info`;
14
20
  /**
15
21
  * Stable framework-owned route for creating a new session.
16
22
  */
@@ -24,10 +30,6 @@ export const ASH_CONTINUE_SESSION_ROUTE_PATTERN = `${ASH_ROUTE_PREFIX}/session/:
24
30
  * Stable framework-owned message stream route pattern.
25
31
  */
26
32
  export const ASH_MESSAGE_STREAM_ROUTE_PATTERN = `${ASH_ROUTE_PREFIX}/session/:sessionId/stream`;
27
- /**
28
- * Stable framework-owned route prefix for static home-page UI assets.
29
- */
30
- export const ASH_HOME_PAGE_UI_ROUTE_PREFIX = `${ASH_ROUTE_PREFIX}/ui`;
31
33
  /**
32
34
  * Stable framework-owned route pattern for receiving inbound IdP redirects
33
35
  * during in-turn interactive connection authorization.
@@ -29,12 +29,7 @@ export function ashChannel(input) {
29
29
  if (policyRejection !== null)
30
30
  return policyRejection;
31
31
  const token = `ash:${crypto.randomUUID()}`;
32
- const messageText = typeof body.message === "string"
33
- ? body.message
34
- : body.message
35
- ? JSON.stringify(body.message)
36
- : "";
37
- const session = await send(messageText, {
32
+ const session = await send(body.message, {
38
33
  auth: sessionAuth,
39
34
  continuationToken: token,
40
35
  });
@@ -1 +1 @@
1
- export { defineChannel, POST, GET, PUT, DELETE, type AttachmentResolver, type Channel, type ChannelConfig, type ChannelEvents, type Session, type SessionHandle, type RouteDefinition, type RouteHandlerArgs, type SendFn, type SendOptions, type SendPayload, type GetSessionFn, } from "#public/definitions/defineChannel.js";
1
+ export { defineChannel, POST, GET, PUT, DELETE, type Channel, type ChannelConfig, type ChannelEvents, type Session, type SessionHandle, type RouteDefinition, type RouteHandlerArgs, type SendFn, type SendOptions, type SendPayload, type GetSessionFn, } from "#public/definitions/defineChannel.js";
@@ -1,21 +1,16 @@
1
1
  import type { FilePart, UserContent } from "ai";
2
2
  import type { SlackBotToken } from "#compiled/@chat-adapter/slack/index.js";
3
3
  import type { Message } from "#compiled/chat/index.js";
4
- import type { AttachmentResolver } from "#channel/adapter.js";
4
+ import type { FetchFileResult } from "#channel/adapter.js";
5
5
  import type { UploadPolicy } from "#public/channels/upload-policy.js";
6
- /**
7
- * Payload an `ash-attachment:` ref carries for Slack file uploads.
8
- * The Slack resolver reads `params.url` and fetches it with the bot
9
- * token.
10
- */
11
- export interface SlackAttachmentParams {
12
- readonly url: string;
13
- }
14
6
  /**
15
7
  * Emits one {@link FilePart} per supported attachment in the inbound
16
- * message, with `data` set to an `ash-attachment:` ref URL. Audio,
17
- * video, url-less, and policy-violating attachments are dropped so a
18
- * single bad upload never blocks the text portion of the mention.
8
+ * message, with `data` set to a `URL` object pointing at the Slack
9
+ * file. Audio, video, URL-less, and policy-violating attachments are
10
+ * dropped so a single bad upload never blocks the text portion.
11
+ *
12
+ * The `URL` object in `data` is resolved by the channel's `fetchFile`
13
+ * function at staging time inside the workflow step.
19
14
  */
20
15
  export declare function collectSlackFileParts(message: Pick<Message, "attachments">, policy: UploadPolicy): FilePart[];
21
16
  /**
@@ -25,11 +20,12 @@ export declare function collectSlackFileParts(message: Pick<Message, "attachment
25
20
  */
26
21
  export declare function buildSlackTurnMessage(text: string, fileParts: readonly FilePart[]): string | UserContent;
27
22
  /**
28
- * Slack-channel {@link AttachmentResolver}: fetches the upstream
29
- * Slack file URL with the bot token and returns the bytes plus any
30
- * advertised media-type. Staging writes bytes to the sandbox and
31
- * rewrites the file part as an `ash-sandbox:` ref.
23
+ * Creates a `fetchFile` function for the Slack channel.
24
+ *
25
+ * Returns `null` for URLs that don't belong to Slack so they pass
26
+ * through to the model provider unchanged. Fetches Slack file URLs
27
+ * with the bot token.
32
28
  */
33
- export declare function createSlackAttachmentResolver(input: {
29
+ export declare function createSlackFetchFile(input: {
34
30
  readonly botToken?: SlackBotToken;
35
- }): AttachmentResolver<SlackAttachmentParams>;
31
+ }): (url: string) => Promise<FetchFileResult | null>;
@@ -1,13 +1,14 @@
1
- import { AshAttachmentError } from "#internal/attachments/errors.js";
2
- import { encodeAttachmentRef } from "#internal/attachments/refs.js";
3
1
  import { createLogger } from "#internal/logging.js";
4
2
  import { evaluateFilePart, formatUploadPolicyViolation } from "#public/channels/upload-policy.js";
5
3
  const log = createLogger("slack.attachments");
6
4
  /**
7
5
  * Emits one {@link FilePart} per supported attachment in the inbound
8
- * message, with `data` set to an `ash-attachment:` ref URL. Audio,
9
- * video, url-less, and policy-violating attachments are dropped so a
10
- * single bad upload never blocks the text portion of the mention.
6
+ * message, with `data` set to a `URL` object pointing at the Slack
7
+ * file. Audio, video, URL-less, and policy-violating attachments are
8
+ * dropped so a single bad upload never blocks the text portion.
9
+ *
10
+ * The `URL` object in `data` is resolved by the channel's `fetchFile`
11
+ * function at staging time inside the workflow step.
11
12
  */
12
13
  export function collectSlackFileParts(message, policy) {
13
14
  const parts = [];
@@ -31,7 +32,7 @@ function toSlackFilePart(attachment, index) {
31
32
  return null;
32
33
  }
33
34
  if (!attachment.url) {
34
- log.warn("dropped attachment — no url available for ref construction", {
35
+ log.warn("dropped attachment — no url available", {
35
36
  name: attachment.name,
36
37
  });
37
38
  return null;
@@ -40,10 +41,7 @@ function toSlackFilePart(attachment, index) {
40
41
  type: "file",
41
42
  mediaType: attachment.mimeType ?? "application/octet-stream",
42
43
  filename: attachment.name ?? `attachment-${index}`,
43
- data: encodeAttachmentRef({
44
- params: { url: attachment.url },
45
- size: attachment.size,
46
- }),
44
+ data: new URL(attachment.url),
47
45
  };
48
46
  }
49
47
  /**
@@ -62,39 +60,35 @@ export function buildSlackTurnMessage(text, fileParts) {
62
60
  const textPart = { type: "text", text };
63
61
  return [textPart, ...fileParts];
64
62
  }
65
- /** Materializes a {@link SlackBotToken} to a string at fetch time
66
- * so callers can rotate (function-shaped credentials) or read lazily. */
67
63
  async function resolveSlackBotToken(token) {
68
64
  const source = token ?? process.env.SLACK_BOT_TOKEN;
69
65
  if (!source) {
70
- throw new Error("SLACK_BOT_TOKEN is required to resolve Slack attachment refs.");
66
+ throw new Error("SLACK_BOT_TOKEN is required to fetch Slack files.");
71
67
  }
72
68
  return typeof source === "function" ? await source() : source;
73
69
  }
74
70
  /**
75
- * Slack-channel {@link AttachmentResolver}: fetches the upstream
76
- * Slack file URL with the bot token and returns the bytes plus any
77
- * advertised media-type. Staging writes bytes to the sandbox and
78
- * rewrites the file part as an `ash-sandbox:` ref.
71
+ * Creates a `fetchFile` function for the Slack channel.
72
+ *
73
+ * Returns `null` for URLs that don't belong to Slack so they pass
74
+ * through to the model provider unchanged. Fetches Slack file URLs
75
+ * with the bot token.
79
76
  */
80
- export function createSlackAttachmentResolver(input) {
81
- return {
82
- async resolve(ref) {
83
- const token = await resolveSlackBotToken(input.botToken);
84
- const response = await fetch(ref.params.url, {
85
- headers: { authorization: `Bearer ${token}` },
86
- });
87
- if (!response.ok) {
88
- throw new AshAttachmentError({
89
- adapterKind: "slack",
90
- kind: "fetch-failed",
91
- message: `Slack attachment fetch returned HTTP ${response.status} for ${ref.params.url}.`,
92
- });
93
- }
94
- return {
95
- bytes: Buffer.from(await response.arrayBuffer()),
96
- mediaType: response.headers.get("content-type") ?? undefined,
97
- };
98
- },
77
+ export function createSlackFetchFile(input) {
78
+ return async (url) => {
79
+ if (!url.startsWith("https://files.slack.com/")) {
80
+ return null;
81
+ }
82
+ const token = await resolveSlackBotToken(input.botToken);
83
+ const response = await fetch(url, {
84
+ headers: { authorization: `Bearer ${token}` },
85
+ });
86
+ if (!response.ok) {
87
+ throw new Error(`Slack file fetch returned HTTP ${response.status} for ${url}.`);
88
+ }
89
+ return {
90
+ bytes: Buffer.from(await response.arrayBuffer()),
91
+ mediaType: response.headers.get("content-type") ?? undefined,
92
+ };
99
93
  };
100
94
  }
@@ -1,5 +1,4 @@
1
1
  export { slack, type SlackOptions } from "#public/channels/slack/slack.js";
2
2
  export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackInteractionAction, type SlackReceiveArgs, } from "#public/channels/slack/slackChannel.js";
3
- export type { SlackAttachmentParams } from "#public/channels/slack/attachments.js";
4
3
  export { Actions, Button, Card, CardText, Divider, Fields, Image, LinkButton, Modal, RadioSelect, Section, Select, SelectOption, Table, TextInput, } from "#compiled/chat/index.js";
5
4
  export type { AdapterPostableMessage, Attachment, Author, CardElement, FileUpload, Message, PostableMessage, SentMessage, Thread, } from "#compiled/chat/index.js";
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "#internal/logging.js";
2
- import { buildSlackTurnMessage, collectSlackFileParts, createSlackAttachmentResolver, } from "#public/channels/slack/attachments.js";
2
+ import { buildSlackTurnMessage, collectSlackFileParts, createSlackFetchFile, } from "#public/channels/slack/attachments.js";
3
3
  import { deriveHitlResponse, isHitlAction, renderInputRequestBlocks, } from "#public/channels/slack/hitl.js";
4
4
  import { mergeUploadPolicy } from "#public/channels/upload-policy.js";
5
5
  import { defineChannel, POST, } from "#public/definitions/defineChannel.js";
@@ -109,7 +109,7 @@ function defaultInputRequestedHandler() {
109
109
  }
110
110
  export function slackChannel(config = {}) {
111
111
  const uploadPolicy = mergeUploadPolicy(config.uploadPolicy);
112
- const slackAttachments = createSlackAttachmentResolver({
112
+ const slackFetchFile = createSlackFetchFile({
113
113
  botToken: config.credentials?.botToken,
114
114
  });
115
115
  let activeSend = null;
@@ -183,7 +183,7 @@ export function slackChannel(config = {}) {
183
183
  const inputHandler = config.events?.["input.requested"] ?? defaultInputRequestedHandler();
184
184
  return defineChannel({
185
185
  state: { serializedThread: null, teamId: null },
186
- attachments: slackAttachments,
186
+ fetchFile: slackFetchFile,
187
187
  context(state) {
188
188
  return rebuildSlackContext(state, config.credentials?.botToken);
189
189
  },
@@ -1,4 +1,4 @@
1
- import type { AttachmentResolver } from "#channel/adapter.js";
1
+ import type { FetchFileResult } from "#channel/adapter.js";
2
2
  import { CHANNEL_SENTINEL } from "#channel/compiled-channel.js";
3
3
  import type { SessionAuthContext } from "#channel/types.js";
4
4
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
@@ -7,7 +7,6 @@ import type { Session, SessionHandle } from "#channel/session.js";
7
7
  export type { Session, SessionHandle } from "#channel/session.js";
8
8
  export { POST, GET, PUT, DELETE } from "#channel/routes.js";
9
9
  export type { RouteDefinition, RouteHandlerArgs, SendFn, SendOptions, SendPayload, GetSessionFn, } from "#channel/routes.js";
10
- export type { AttachmentResolver } from "#channel/adapter.js";
11
10
  type EventData<T extends HandleMessageStreamEvent["type"]> = Extract<HandleMessageStreamEvent, {
12
11
  type: T;
13
12
  }> extends {
@@ -43,12 +42,15 @@ export interface ChannelConfig<TState = undefined, TCtx = void> {
43
42
  }): Promise<Session>;
44
43
  readonly events?: ChannelEvents<TCtx>;
45
44
  /**
46
- * Resolver for `ash-attachment:` URLs this channel mints during
47
- * inbound message handling. The framework's staging layer dispatches
48
- * to `resolve(ref, ctx)` before each model call so the bytes can be
49
- * written to the sandbox and rewritten as `ash-sandbox:` refs.
45
+ * Fetches bytes for a URL encountered in `FilePart.data`.
46
+ *
47
+ * Called by the staging pipeline when it encounters a `URL` object
48
+ * on a `FilePart` (e.g. a Slack file URL placed there by the route
49
+ * handler). Return `null` to let the URL pass through to the model
50
+ * provider unchanged. Return bytes or {@link FetchFileResult} to
51
+ * stage the file to the sandbox.
50
52
  */
51
- readonly attachments?: AttachmentResolver;
53
+ readonly fetchFile?: (url: string) => Promise<Buffer | FetchFileResult | null>;
52
54
  }
53
55
  export interface Channel<TState = undefined> {
54
56
  readonly __kind: typeof CHANNEL_SENTINEL;
@@ -1,5 +1,6 @@
1
1
  import { CHANNEL_SENTINEL } from "#channel/compiled-channel.js";
2
2
  import { defaultDeliverResult } from "#channel/adapter.js";
3
+ import { HTTP_ADAPTER_KIND } from "#channel/http.js";
3
4
  export { POST, GET, PUT, DELETE } from "#channel/routes.js";
4
5
  export function defineChannel(config) {
5
6
  const adapter = buildAdapter(config);
@@ -14,7 +15,7 @@ export function defineChannel(config) {
14
15
  function buildAdapter(config) {
15
16
  const hasState = config.state != null;
16
17
  const hasContext = config.context != null;
17
- const hasAttachments = config.attachments !== undefined;
18
+ const hasFetchFile = config.fetchFile !== undefined;
18
19
  const hasBehavior = hasState || hasContext;
19
20
  const eventHandlers = {};
20
21
  let hasEventHandlers = false;
@@ -42,20 +43,13 @@ function buildAdapter(config) {
42
43
  };
43
44
  }
44
45
  }
45
- // When the channel carries no behavior at all, return a bare
46
- // pass-through adapter with framework kind "http". This avoids the
47
- // registry conflict when the adapter would otherwise reserve a
48
- // framework kind with no behavior to register against it.
49
- if (!hasBehavior && !hasEventHandlers && !hasAttachments) {
50
- return { kind: "http" };
46
+ if (!hasBehavior && !hasEventHandlers && !hasFetchFile) {
47
+ return { kind: HTTP_ADAPTER_KIND };
51
48
  }
52
- // Channels with any authored behavior use a dedicated adapter kind so
53
- // the registry can rehydrate it across step boundaries without
54
- // conflicting with framework-reserved kinds.
55
49
  return {
56
50
  kind: "defineChannel",
57
51
  state: hasState ? { ...config.state } : {},
58
- attachments: config.attachments,
52
+ fetchFile: config.fetchFile,
59
53
  createAdapterContext(base) {
60
54
  const state = base.state;
61
55
  const channelCtx = hasContext ? config.context(state) : {};
@@ -2,9 +2,9 @@ import type { Optional } from "#shared/optional.js";
2
2
  import type { SandboxSession } from "#shared/sandbox-session.js";
3
3
  import type { SandboxDefinition as SharedSandboxDefinition } from "#shared/sandbox-definition.js";
4
4
  export type { SandboxCommandOptions, SandboxCommandResult, SandboxReadFileOptions, SandboxSession, } from "#shared/sandbox-session.js";
5
- export type { SandboxNetworkPolicy, SandboxBootstrapUseOptions, SandboxSessionUseOptions, SandboxBootstrapUseFn, SandboxSessionUseFn, SandboxBootstrapContext, SandboxSessionContext, } from "#shared/sandbox-definition.js";
6
- export type SandboxDefinition<S extends SandboxSession = SandboxSession> = Optional<SharedSandboxDefinition<S>, "backend">;
5
+ export type { SandboxBootstrapUseOptions, SandboxBootstrapUseFn, SandboxSessionUseFn, SandboxBootstrapContext, SandboxSessionContext, } from "#shared/sandbox-definition.js";
6
+ export type SandboxDefinition<S extends SandboxSession = SandboxSession, O = Record<string, never>> = Optional<SharedSandboxDefinition<S, O>, "backend">;
7
7
  /**
8
8
  * Defines a sandbox configuration.
9
9
  */
10
- export declare function defineSandbox<S extends SandboxSession = SandboxSession>(definition: SandboxDefinition<S>): SandboxDefinition<S>;
10
+ export declare function defineSandbox<S extends SandboxSession = SandboxSession, O = Record<string, never>>(definition: SandboxDefinition<S, O>): SandboxDefinition<S, O>;
@@ -1,5 +1,5 @@
1
1
  import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
2
- import type { VercelSandbox } from "#public/sandbox/vercel-sandbox.js";
2
+ import type { VercelSandbox, VercelSandboxSessionUseOptions } from "#public/sandbox/vercel-sandbox.js";
3
3
  /**
4
4
  * Constructs the built-in Vercel sandbox backend.
5
5
  *
@@ -8,4 +8,4 @@ import type { VercelSandbox } from "#public/sandbox/vercel-sandbox.js";
8
8
  * timeout) lives in `onSession`'s `create()` call or
9
9
  * `VercelSandbox.update()`.
10
10
  */
11
- export declare function vercelBackend(): SandboxBackend<VercelSandbox>;
11
+ export declare function vercelBackend(): SandboxBackend<VercelSandbox, VercelSandboxSessionUseOptions>;
@@ -3,10 +3,10 @@
3
3
  * `agent/sandbox/sandbox.ts` when paired with a `workspace/` folder).
4
4
  */
5
5
  export { getSandbox } from "#context/accessors.js";
6
- export { defineSandbox, type SandboxBootstrapContext, type SandboxBootstrapUseFn, type SandboxBootstrapUseOptions, type SandboxCommandOptions, type SandboxCommandResult, type SandboxDefinition, type SandboxNetworkPolicy, type SandboxReadFileOptions, type SandboxSession, type SandboxSessionContext, type SandboxSessionUseFn, type SandboxSessionUseOptions, } from "#public/definitions/sandbox.js";
6
+ export { defineSandbox, type SandboxBootstrapContext, type SandboxBootstrapUseFn, type SandboxBootstrapUseOptions, type SandboxCommandOptions, type SandboxCommandResult, type SandboxDefinition, type SandboxReadFileOptions, type SandboxSession, type SandboxSessionContext, type SandboxSessionUseFn, } from "#public/definitions/sandbox.js";
7
7
  export type { SandboxBackend, SandboxBackendCreateInput, SandboxBackendHandle, SandboxBackendPrewarmInput, SandboxBackendRuntimeContext, SandboxBackendSessionState, SandboxSeedFile, } from "#public/definitions/sandbox-backend.js";
8
8
  export { SandboxTemplateNotProvisionedError } from "#public/definitions/sandbox-backend.js";
9
9
  export { defaultBackend } from "#public/sandbox/backends/default.js";
10
10
  export { localBackend } from "#public/sandbox/backends/local.js";
11
11
  export { vercelBackend } from "#public/sandbox/backends/vercel.js";
12
- export type { VercelSandbox } from "#public/sandbox/vercel-sandbox.js";
12
+ export type { VercelSandbox, VercelSandboxSessionUseOptions, } from "#public/sandbox/vercel-sandbox.js";
@@ -1,4 +1,12 @@
1
- import type { SandboxNetworkPolicy, SandboxSession } from "#public/definitions/sandbox.js";
1
+ import type { SandboxUpdateParams } from "#compiled/@vercel/sandbox/index.js";
2
+ import type { SandboxSession } from "#public/definitions/sandbox.js";
3
+ /**
4
+ * Options accepted by the Vercel backend's `onSession({ use })` hook
5
+ * and `VercelSandbox.update()`. Aliases the Vercel SDK's
6
+ * `SandboxUpdateParams` so the full live-session configuration surface
7
+ * is available to authors.
8
+ */
9
+ export type VercelSandboxSessionUseOptions = SandboxUpdateParams;
2
10
  /**
3
11
  * Ash-owned session type returned by the Vercel backend's
4
12
  * `onSession({ use })` hook. Extends the narrow
@@ -9,16 +17,9 @@ export interface VercelSandbox extends SandboxSession {
9
17
  readonly name: string;
10
18
  readonly persistent: boolean;
11
19
  readonly status: string;
12
- readonly networkPolicy: SandboxNetworkPolicy | undefined;
20
+ readonly networkPolicy: SandboxUpdateParams["networkPolicy"];
13
21
  readonly tags: Record<string, string> | undefined;
14
- update(params: {
15
- networkPolicy?: SandboxNetworkPolicy;
16
- resources?: {
17
- vcpus?: number;
18
- };
19
- timeout?: number;
20
- tags?: Record<string, string>;
21
- }): Promise<void>;
22
+ update(params: VercelSandboxSessionUseOptions): Promise<void>;
22
23
  stop(opts?: {
23
24
  blocking?: boolean;
24
25
  }): Promise<void>;
@@ -1,4 +1,6 @@
1
1
  import { getAdapterKind } from "#channel/adapter.js";
2
+ import { HTTP_ADAPTER } from "#channel/http.js";
3
+ import { SCHEDULE_ADAPTER } from "#channel/schedule.js";
2
4
  import { SUBAGENT_ADAPTER } from "#execution/subagent-adapter.js";
3
5
  import { RuntimeRegistryError } from "#internal/runtime-registry.js";
4
6
  /**
@@ -6,7 +8,11 @@ import { RuntimeRegistryError } from "#internal/runtime-registry.js";
6
8
  *
7
9
  * Framework kinds cannot be shadowed with route-authored behavior.
8
10
  */
9
- const FRAMEWORK_ADAPTERS = [{ kind: "http" }, SUBAGENT_ADAPTER];
11
+ const FRAMEWORK_ADAPTERS = [
12
+ HTTP_ADAPTER,
13
+ SUBAGENT_ADAPTER,
14
+ SCHEDULE_ADAPTER,
15
+ ];
10
16
  /**
11
17
  * Non-event-handler fields on a {@link ChannelAdapter}. Any other
12
18
  * key on the adapter object corresponds to a stream event handler
@@ -20,7 +26,7 @@ const ADAPTER_NON_EVENT_FIELDS = new Set([
20
26
  "deliver",
21
27
  "contextProviders",
22
28
  "createAdapterContext",
23
- "attachments",
29
+ "fetchFile",
24
30
  ]);
25
31
  /**
26
32
  * Builds the runtime-owned adapter registry from framework adapters plus any
@@ -109,7 +115,7 @@ function carriesAdapterBehavior(adapter) {
109
115
  if (adapter.deliver !== undefined) {
110
116
  return true;
111
117
  }
112
- if (adapter.attachments !== undefined) {
118
+ if (adapter.fetchFile !== undefined) {
113
119
  return true;
114
120
  }
115
121
  if (adapter.createAdapterContext !== undefined) {
@@ -1,3 +1,4 @@
1
+ import { HTTP_ADAPTER_KIND } from "#channel/http.js";
1
2
  import { normalizeChannelDefinition } from "#internal/authored-definition/channel.js";
2
3
  import { toErrorMessage } from "#shared/errors.js";
3
4
  import { createResolvedModuleSourceRef, loadResolvedModuleExport, ResolveAgentError, } from "#runtime/resolve-helpers.js";
@@ -30,7 +31,7 @@ export async function resolveChannelDefinition(definition, moduleMap, nodeId) {
30
31
  });
31
32
  const matchedRoute = channelDefinition.routes.find((r) => r.method.toUpperCase() === definition.method.toUpperCase() && r.path === definition.urlPath);
32
33
  const adapter = channelDefinition.adapter;
33
- if (adapter && adapter.kind !== "http") {
34
+ if (adapter && adapter.kind !== HTTP_ADAPTER_KIND) {
34
35
  adapter.kind = `channel:${definition.name}`;
35
36
  }
36
37
  return {
@@ -7,9 +7,9 @@ import type { SandboxSession } from "#shared/sandbox-session.js";
7
7
  * runtime orchestrator can persist reconnect metadata and release
8
8
  * resources.
9
9
  */
10
- export interface SandboxBackendHandle<S extends SandboxSession = SandboxSession> {
10
+ export interface SandboxBackendHandle<S extends SandboxSession = SandboxSession, O = Record<string, never>> {
11
11
  readonly session: SandboxSession;
12
- readonly useSessionFn: SandboxSessionUseFn<S>;
12
+ readonly useSessionFn: SandboxSessionUseFn<S, O>;
13
13
  captureState(): Promise<SandboxBackendSessionState>;
14
14
  dispose(): Promise<void>;
15
15
  }
@@ -97,7 +97,7 @@ export interface SandboxBackendPrewarmInput {
97
97
  * Each backend owns the full template-then-session lifecycle internally;
98
98
  * callers only need a single `create` call.
99
99
  */
100
- export interface SandboxBackend<S extends SandboxSession = SandboxSession> {
100
+ export interface SandboxBackend<S extends SandboxSession = SandboxSession, O = Record<string, never>> {
101
101
  /**
102
102
  * Stable identifier for this backend implementation.
103
103
  *
@@ -113,7 +113,7 @@ export interface SandboxBackend<S extends SandboxSession = SandboxSession> {
113
113
  * {@link SandboxTemplateNotProvisionedError} when the requested
114
114
  * template is missing.
115
115
  */
116
- create(input: SandboxBackendCreateInput): Promise<SandboxBackendHandle<S>>;
116
+ create(input: SandboxBackendCreateInput): Promise<SandboxBackendHandle<S, O>>;
117
117
  /**
118
118
  * Build-time prewarm hook. Ash invokes this for every authored
119
119
  * sandbox in the compiled graph before serving traffic so the backend
@@ -1,47 +1,17 @@
1
1
  import type { SandboxSession } from "#shared/sandbox-session.js";
2
2
  import type { SandboxBackend } from "#shared/sandbox-backend.js";
3
- /**
4
- * A transform applied to network requests matching a domain rule.
5
- */
6
- export interface SandboxNetworkTransformer {
7
- readonly headers?: Record<string, string>;
8
- }
9
- /**
10
- * A rule applied to requests matching a domain in the network policy.
11
- */
12
- export interface SandboxNetworkPolicyRule {
13
- readonly transform?: SandboxNetworkTransformer[];
14
- }
15
- /**
16
- * Network policy to define network restrictions for the sandbox.
17
- *
18
- * - `"allow-all"`: Full internet access (default). All traffic is allowed.
19
- * - `"deny-all"`: No internet access. All traffic is denied.
20
- * - Object: Custom access with explicit allow/deny lists.
21
- */
22
- export type SandboxNetworkPolicy = "allow-all" | "deny-all" | {
23
- readonly allow?: string[] | Readonly<Record<string, SandboxNetworkPolicyRule[]>>;
24
- };
25
3
  export interface SandboxBootstrapUseOptions {
26
4
  readonly runtime?: string;
27
5
  readonly ports?: number[];
28
6
  readonly env?: Record<string, string>;
29
7
  }
30
- export interface SandboxSessionUseOptions {
31
- readonly networkPolicy?: SandboxNetworkPolicy;
32
- readonly resources?: {
33
- vcpus?: number;
34
- };
35
- readonly timeout?: number;
36
- readonly tags?: Record<string, string>;
37
- }
38
8
  export type SandboxBootstrapUseFn = (options?: SandboxBootstrapUseOptions) => Promise<SandboxSession>;
39
- export type SandboxSessionUseFn<S extends SandboxSession = SandboxSession> = (options?: SandboxSessionUseOptions) => Promise<S>;
9
+ export type SandboxSessionUseFn<S extends SandboxSession = SandboxSession, O = Record<string, never>> = (options?: O) => Promise<S>;
40
10
  export interface SandboxBootstrapContext {
41
11
  readonly use: SandboxBootstrapUseFn;
42
12
  }
43
- export interface SandboxSessionContext<S extends SandboxSession = SandboxSession> {
44
- readonly use: SandboxSessionUseFn<S>;
13
+ export interface SandboxSessionContext<S extends SandboxSession = SandboxSession, O = Record<string, never>> {
14
+ readonly use: SandboxSessionUseFn<S, O>;
45
15
  }
46
16
  /**
47
17
  * Public sandbox definition authored in `agent/sandbox.ts` (shorthand)
@@ -56,7 +26,7 @@ export interface SandboxSessionContext<S extends SandboxSession = SandboxSession
56
26
  * `subagents/<name>/sandbox.ts` (or the folder form) and do not inherit
57
27
  * their parent's sandbox (skill seeds differ per agent).
58
28
  */
59
- export interface SandboxDefinition<S extends SandboxSession = SandboxSession> {
29
+ export interface SandboxDefinition<S extends SandboxSession = SandboxSession, O = Record<string, never>> {
60
30
  /**
61
31
  * Backend that runs this sandbox.
62
32
  *
@@ -66,7 +36,7 @@ export interface SandboxDefinition<S extends SandboxSession = SandboxSession> {
66
36
  * everywhere else. Set `backend` explicitly to pin the sandbox to a
67
37
  * specific backend regardless of environment.
68
38
  */
69
- backend: SandboxBackend<S>;
39
+ backend: SandboxBackend<S, O>;
70
40
  bootstrap?(input: SandboxBootstrapContext): Promise<void> | void;
71
- onSession?(input: SandboxSessionContext<S>): Promise<void> | void;
41
+ onSession?(input: SandboxSessionContext<S, O>): Promise<void> | void;
72
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.7.5",
3
+ "version": "0.8.0",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"