experimental-ash 0.43.0 → 0.45.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 (54) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/bin/ash.js +1 -0
  3. package/dist/docs/internals/mechanical-invariants.md +16 -0
  4. package/dist/docs/public/advanced/instrumentation.md +71 -21
  5. package/dist/docs/public/advanced/typescript-api.md +1 -1
  6. package/dist/docs/public/sandbox.md +38 -0
  7. package/dist/src/channel/compiled-channel.d.ts +4 -1
  8. package/dist/src/channel/compiled-channel.js +1 -1
  9. package/dist/src/channel/routes.d.ts +8 -10
  10. package/dist/src/compiled/.vendor-stamp.json +2 -2
  11. package/dist/src/compiled/@vercel/sandbox/index.d.ts +24 -19
  12. package/dist/src/compiled/@vercel/sandbox/index.js +5 -5
  13. package/dist/src/compiled/@vercel/sandbox/network-policy.d.ts +161 -0
  14. package/dist/src/compiled/@vercel/sandbox/package.json +1 -1
  15. package/dist/src/compiled/@workflow/core/runtime.js +13 -13
  16. package/dist/src/compiled/_chunks/node/{auth-CVVvWjaK.js → auth-BsyzphzW.js} +1 -1
  17. package/dist/src/compiled/_chunks/node/{version-nR4RSpFw.js → version-BGue04qw.js} +1 -1
  18. package/dist/src/compiled/just-bash/index.d.ts +23 -2
  19. package/dist/src/compiled/just-bash/network/types.d.ts +155 -0
  20. package/dist/src/compiler/artifacts.d.ts +1 -0
  21. package/dist/src/compiler/artifacts.js +1 -1
  22. package/dist/src/compiler/channel-instrumentation-types.d.ts +8 -0
  23. package/dist/src/compiler/channel-instrumentation-types.js +2 -0
  24. package/dist/src/context/dynamic-skill-lifecycle.d.ts +4 -3
  25. package/dist/src/context/dynamic-skill-lifecycle.js +1 -1
  26. package/dist/src/context/keys.d.ts +11 -4
  27. package/dist/src/execution/sandbox/bindings/local.js +1 -1
  28. package/dist/src/execution/sandbox/bindings/vercel.js +1 -1
  29. package/dist/src/execution/sandbox/session.d.ts +6 -1
  30. package/dist/src/execution/sandbox/session.js +1 -1
  31. package/dist/src/harness/tool-loop.js +1 -1
  32. package/dist/src/internal/application/package.js +1 -1
  33. package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
  34. package/dist/src/packages/ash-scaffold/src/web-template.js +1 -0
  35. package/dist/src/public/channels/discord/discordChannel.d.ts +5 -2
  36. package/dist/src/public/channels/index.d.ts +1 -1
  37. package/dist/src/public/channels/slack/slackChannel.d.ts +6 -8
  38. package/dist/src/public/channels/teams/teamsChannel.d.ts +5 -2
  39. package/dist/src/public/channels/telegram/telegramChannel.d.ts +5 -2
  40. package/dist/src/public/channels/twilio/twilioChannel.d.ts +6 -3
  41. package/dist/src/public/definitions/defineChannel.d.ts +6 -3
  42. package/dist/src/public/definitions/instrumentation.d.ts +1 -152
  43. package/dist/src/public/definitions/instrumentation.js +1 -1
  44. package/dist/src/public/definitions/sandbox.d.ts +1 -1
  45. package/dist/src/public/instrumentation/index.d.ts +178 -1
  46. package/dist/src/public/instrumentation/index.js +1 -1
  47. package/dist/src/public/sandbox/index.d.ts +1 -0
  48. package/dist/src/runtime/resolve-channel.js +1 -1
  49. package/dist/src/shared/sandbox-network-policy.d.ts +23 -0
  50. package/dist/src/shared/sandbox-network-policy.js +1 -0
  51. package/dist/src/shared/sandbox-session.d.ts +36 -0
  52. package/dist/src/shared/skill-package.d.ts +7 -0
  53. package/dist/src/shared/skill-package.js +1 -1
  54. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.45.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8672b13: Dynamic skills now clear stale model announcements when removed, reject duplicate dynamic names before materializing, and remove stale skill package directories through the Ash sandbox file API. Sandbox sessions also expose `removePath()` for deleting files or directory trees.
8
+
9
+ ## 0.44.0
10
+
11
+ ### Minor Changes
12
+
13
+ - ed89a1b: Generate channel instrumentation metadata typings from authored `agent/channels/*` files so `input.channel.metadata` narrows after checking `input.channel.kind` or calling `isChannel(input.channel, channel)`.
14
+ - 5faaf2b: Add `sandbox.setNetworkPolicy(policy)` to the live sandbox handle returned by `ctx.getSandbox()`, so any tool, hook, or channel event can apply a firewall network policy at run time — for cases where the policy must change _during_ a turn (e.g. brokering a credential resolved mid-turn). Configuring a policy known at session start in the backend factory or `onSession`'s `use()` remains valid and is often preferable; both accept the same `transform` shape for credential brokering, which injects headers at the firewall so secrets authenticate egress without entering the sandbox process. The local backend rejects `setNetworkPolicy` (just-bash applies its network policy only at creation and runs no binaries to govern). The `@vercel/sandbox` and `just-bash` network-policy types are now copied verbatim from the installed packages at vendor time (instead of hand-written stubs), so the brokering surface (`match`/`transform`/`forwardURL`/matchers, and just-bash's `allowedUrlPrefixes` transforms) never drifts.
15
+
3
16
  ## 0.43.0
4
17
 
5
18
  ### Minor Changes
package/bin/ash.js CHANGED
@@ -18,6 +18,7 @@ function createBootstrapOptions(overrides = {}) {
18
18
  overrides.cliEntrypointPath ?? resolve(packageRoot, "dist", "src", "cli", "run.js"),
19
19
  packageRoot,
20
20
  postBuildScriptPaths: overrides.postBuildScriptPaths ?? [
21
+ resolve(packageRoot, "scripts", "copy-compiled-assets.mjs"),
21
22
  resolve(packageRoot, "scripts", "copy-docs.mjs"),
22
23
  resolve(packageRoot, "scripts", "stamp-version-tokens.mjs"),
23
24
  ],
@@ -49,6 +49,22 @@ This is enforced on three layers:
49
49
  Markdown frontmatter is treated the same way: `name:` is no longer a
50
50
  recognized field on system or skill markdown.
51
51
 
52
+ ## Release Metadata
53
+
54
+ ### Changeset Package Keys Match Workspace Names
55
+
56
+ Changeset frontmatter must name the package exactly as it appears in that
57
+ workspace package's `package.json`. Directory names, binary names, and
58
+ marketing names are not valid substitutes; for example, the package under
59
+ `packages/ash` is named `experimental-ash`.
60
+
61
+ Enforcement: rule 29 in `scripts/guard-agents-rules.mjs` parses changeset
62
+ YAML frontmatter with `gray-matter`, reads the package patterns from
63
+ `pnpm-workspace.yaml`, resolves workspace package names from their
64
+ `package.json` files, and fails if any `.changeset/*.md` package key does not
65
+ match one of those names. This keeps invalid release metadata from reaching
66
+ the post-merge `changesets/action` workflow.
67
+
52
68
  ## Runtime Boundaries
53
69
 
54
70
  ### Workflow APIs Stay In Runtime Code
@@ -61,32 +61,88 @@ Use `metadata["step.started"]` when the metadata depends on the current session,
61
61
  channel, or model input:
62
62
 
63
63
  ```ts
64
- import type { SlackInstrumentationMetadata } from "experimental-ash/channels/slack";
64
+ import { defineInstrumentation } from "experimental-ash/instrumentation";
65
+
66
+ export default defineInstrumentation({
67
+ metadata: {
68
+ "step.started"(input) {
69
+ if (input.channel.kind !== "channel:support") {
70
+ return {};
71
+ }
72
+
73
+ return {
74
+ "slack.channel_id": input.channel.metadata.channelId ?? "",
75
+ "slack.user_id": input.channel.metadata.triggeringUserId ?? "",
76
+ };
77
+ },
78
+ },
79
+ });
80
+ ```
81
+
82
+ For authored channels, TypeScript gets channel metadata types from a compiler-owned declaration
83
+ file. When Ash compiles `agent/channels/support.ts`, it writes
84
+ `.ash/compile/channel-instrumentation-types.d.ts` with a `ChannelMetadataMap` entry for the
85
+ filename-derived kind and a `ChannelReferenceMap` entry for `isChannel`:
86
+
87
+ ```ts
88
+ import type { InferChannelMetadata } from "experimental-ash/channels";
65
89
 
66
- // A Slack-backed channel authored at `agent/channels/support.ts` has kind
67
- // `channel:support` (its filename).
68
90
  declare module "experimental-ash/instrumentation" {
69
91
  interface ChannelMetadataMap {
70
- readonly "channel:support": SlackInstrumentationMetadata;
92
+ readonly "channel:support": InferChannelMetadata<
93
+ typeof import("../../agent/channels/support.js").default
94
+ >;
95
+ }
96
+ interface ChannelReferenceMap {
97
+ readonly "channel:support": typeof import("../../agent/channels/support.js").default;
71
98
  }
72
99
  }
100
+ ```
101
+
102
+ Keep `.ash/**/*.d.ts` in your app's `tsconfig.json` `include` list so TypeScript sees that file.
103
+ Then either `input.channel.kind === "channel:support"` or `isChannel(input.channel, supportChannel)`
104
+ narrows `input.channel.metadata` to the return type of the channel's `metadata(state)` function:
105
+
106
+ ```ts
107
+ // agent/channels/support.ts
108
+ import { defineChannel, POST } from "experimental-ash/channels";
109
+
110
+ export default defineChannel({
111
+ state: { ticketId: null as string | null },
112
+ // Ash uses this function's runtime value for instrumentation metadata and
113
+ // makes its return type available to instrumentation.ts.
114
+ metadata(state) {
115
+ return { ticketId: state.ticketId };
116
+ },
117
+ routes: [POST("/support", async () => new Response("ok"))],
118
+ });
119
+ ```
120
+
121
+ ```ts
122
+ // agent/instrumentation.ts
123
+ import { defineInstrumentation, isChannel } from "experimental-ash/instrumentation";
124
+
125
+ import supportChannel from "./channels/support.js";
73
126
 
74
127
  export default defineInstrumentation({
75
128
  metadata: {
76
129
  "step.started"(input) {
77
- if (input.channel.kind !== "channel:support") {
130
+ if (!isChannel(input.channel, supportChannel)) {
78
131
  return {};
79
132
  }
80
133
 
81
134
  return {
82
- "slack.channel_id": input.channel.metadata.channelId ?? "",
83
- "slack.user_id": input.channel.metadata.triggeringUserId ?? "",
135
+ "support.ticket_id": input.channel.metadata.ticketId ?? "",
84
136
  };
85
137
  },
86
138
  },
87
139
  });
88
140
  ```
89
141
 
142
+ Built-in wrapper return types expose their concrete metadata too. For example, a file named
143
+ `agent/channels/support.ts` that default-exports `slackChannel()` still narrows under
144
+ `input.channel.kind === "channel:support"` to Slack's projected metadata shape.
145
+
90
146
  The callback runs after Ash has assembled the model input for the attempt and before constructing
91
147
  the AI SDK call. That timing lets the returned values attach to the AI SDK model-call span and its
92
148
  child spans. It runs for each model-call attempt, including a retry of the same logical step when
@@ -109,31 +165,25 @@ channels it is `channel:<name>`, where `<name>` is the channel's filename under
109
165
  Channel metadata is channel-owned. Built-in channels expose only the fields they choose to make
110
166
  observable; for example, Slack projects `channelId`, `teamId`, `threadTs`, and `triggeringUserId`
111
167
  from its durable channel state. User-authored channels expose their own projection by returning
112
- `metadata(state)` from `defineChannel`. To make TypeScript narrow `input.channel.metadata`,
113
- declaration-merge the same `channel:<name>` kind into `ChannelMetadataMap`:
168
+ `metadata(state)` from `defineChannel`. Runtime instrumentation never falls back to raw channel
169
+ state.
114
170
 
115
171
  `metadata(state)` must return an object composed of JSON primitives, arrays, and plain objects.
116
172
  Ash omits `undefined` object properties and drops projections containing values such as `Date` or
117
173
  `Map` with a warning.
118
174
 
175
+ Manual `ChannelMetadataMap` declaration merging is only the escape hatch for unusual setups where
176
+ the generated `.ash/**/*.d.ts` file is not available to the TypeScript program, or where a channel
177
+ is typed outside the normal `agent/channels/<name>` compile path:
178
+
119
179
  ```ts
120
- import { defineChannel } from "experimental-ash/channels";
180
+ import type { SlackInstrumentationMetadata } from "experimental-ash/channels/slack";
121
181
 
122
182
  declare module "experimental-ash/instrumentation" {
123
183
  interface ChannelMetadataMap {
124
- readonly "channel:support": {
125
- readonly triggeringUserId: string | null;
126
- };
184
+ readonly "channel:support": SlackInstrumentationMetadata;
127
185
  }
128
186
  }
129
-
130
- export default defineChannel({
131
- state: { triggeringUserId: null as string | null },
132
- metadata(state) {
133
- return { triggeringUserId: state.triggeringUserId };
134
- },
135
- routes: [],
136
- });
137
187
  ```
138
188
 
139
189
  Metadata failures are non-destructive. If the callback throws, returns a non-record, or returns
@@ -58,7 +58,7 @@ Session metadata, sandbox access, and skill access are available through the `ct
58
58
  passed to tool `execute`, hook handlers, and channel event handlers:
59
59
 
60
60
  - `ctx.session` - current session, turn, auth, and optional parent lineage
61
- - `ctx.getSandbox()` - live sandbox handle for the current agent
61
+ - `ctx.getSandbox()` - live sandbox handle for the current agent; `sandbox.setNetworkPolicy(policy)` applies a firewall network policy at run time (egress control, credential brokering) without an `onSession` hook
62
62
  - `ctx.getSkill(identifier)` - handle for a named skill visible to the current agent
63
63
  - `defineState(name, initial)` - typed durable state with `get()` and `update()` (`experimental-ash/context`)
64
64
 
@@ -359,6 +359,44 @@ or to apply it only after the template has been built, set it in `onSession`'s `
359
359
  common pattern: leave the factory open so `bootstrap` can `git clone` and `npm install`, then
360
360
  lock down via `onSession`.
361
361
 
362
+ `onSession`'s `use()` (and the factory) accept the full policy shape, including per-domain
363
+ `transform`s for **credential brokering** — injecting headers at the firewall so a secret
364
+ authenticates egress without ever entering the sandbox process. When the brokered credential is
365
+ known at session start, configuring it up front in `onSession` is the right choice:
366
+
367
+ ```ts
368
+ onSession: async ({ use }) => {
369
+ await use({
370
+ networkPolicy: {
371
+ allow: {
372
+ "github.com": [{ transform: [{ headers: { authorization: "Basic …" } }] }],
373
+ "*": [],
374
+ },
375
+ },
376
+ });
377
+ };
378
+ ```
379
+
380
+ When the policy must change **during a turn** — to broker a credential resolved mid-turn, or to
381
+ tighten egress after fetching data — call `setNetworkPolicy` on the live sandbox handle from a
382
+ tool, hook, or channel event. It accepts the same policy shape:
383
+
384
+ ```ts
385
+ const sandbox = await ctx.getSandbox();
386
+ await sandbox.setNetworkPolicy({
387
+ allow: {
388
+ "github.com": [{ transform: [{ headers: { authorization: "Basic …" } }] }],
389
+ "*": [],
390
+ },
391
+ });
392
+ ```
393
+
394
+ The `"*": []` catch-all keeps general egress open while the `transform` applies only to
395
+ `github.com`. The policy takes effect from the time the call resolves, so await it before the
396
+ egress you want governed. The local backend rejects `setNetworkPolicy` — just-bash takes its
397
+ network policy only at sandbox creation and runs no binaries to govern — so run-time updates are
398
+ a Vercel-backend capability.
399
+
362
400
  ### Timeout
363
401
 
364
402
  Sandbox VMs shut down after a configurable timeout. Ash defaults to **30 minutes**. Set a different
@@ -3,7 +3,7 @@ import type { RouteHandler, SendFn } from "#channel/routes.js";
3
3
  import type { Session } from "#channel/session.js";
4
4
  import type { SessionAuthContext } from "#channel/types.js";
5
5
  export declare const CHANNEL_SENTINEL: "ash:channel";
6
- export interface CompiledChannel<TState = undefined, TReceiveArgs = Record<string, unknown>> {
6
+ export interface CompiledChannel<TState = undefined, TReceiveArgs = Record<string, unknown>, TMetadata extends Record<string, unknown> = Record<string, unknown>> {
7
7
  readonly __kind: typeof CHANNEL_SENTINEL;
8
8
  readonly routes: readonly {
9
9
  method: string;
@@ -11,6 +11,7 @@ export interface CompiledChannel<TState = undefined, TReceiveArgs = Record<strin
11
11
  handler: RouteHandler<TState>;
12
12
  }[];
13
13
  readonly adapter: ChannelAdapter<any>;
14
+ readonly __metadata?: TMetadata;
14
15
  readonly receive?: (input: {
15
16
  readonly message: string;
16
17
  readonly args: Readonly<TReceiveArgs>;
@@ -20,3 +21,5 @@ export interface CompiledChannel<TState = undefined, TReceiveArgs = Record<strin
20
21
  }) => Promise<Session>;
21
22
  }
22
23
  export declare function isCompiledChannel(value: unknown): value is CompiledChannel;
24
+ export declare function getChannelInstrumentationKind(value: unknown): string | undefined;
25
+ export declare function setChannelInstrumentationKind(channel: CompiledChannel, kind: string): void;
@@ -1 +1 @@
1
- const CHANNEL_SENTINEL=`ash:channel`;function isCompiledChannel(e){return typeof e==`object`&&!!e&&e.__kind===`ash:channel`}export{CHANNEL_SENTINEL,isCompiledChannel};
1
+ const CHANNEL_SENTINEL=`ash:channel`,CHANNEL_INSTRUMENTATION_KIND=Symbol.for(`ash.channel.instrumentationKind`);function isCompiledChannel(e){return typeof e==`object`&&!!e&&e.__kind===`ash:channel`}function getChannelInstrumentationKind(e){if(!isCompiledChannel(e))return;let t=Reflect.get(e,CHANNEL_INSTRUMENTATION_KIND);if(typeof t==`string`&&t.length>0)return t;let n=e.adapter.kind;return typeof n==`string`&&n.startsWith(`channel:`)?n:void 0}function setChannelInstrumentationKind(e,t){Object.defineProperty(e,CHANNEL_INSTRUMENTATION_KIND,{configurable:!0,enumerable:!1,value:t})}export{CHANNEL_SENTINEL,getChannelInstrumentationKind,isCompiledChannel,setChannelInstrumentationKind};
@@ -32,6 +32,12 @@ export interface SendPayload {
32
32
  readonly modelContext?: readonly ModelMessage[];
33
33
  }
34
34
  export type SendFn<TState = undefined> = (input: string | UserContent | SendPayload, options: SendOptions<TState>) => Promise<Session>;
35
+ type BaseSendOptions = {
36
+ auth: SessionAuthContext | null;
37
+ callback?: SessionCallback;
38
+ continuationToken: string;
39
+ mode?: RunMode;
40
+ };
35
41
  /**
36
42
  * Options for {@link SendFn}. The channel owns its continuation-token
37
43
  * format — pass the channel-local raw token (the framework prepends
@@ -39,16 +45,7 @@ export type SendFn<TState = undefined> = (input: string | UserContent | SendPayl
39
45
  * state via {@link state}, which becomes the new session's `state`
40
46
  * on first `runtime.run()` and is ignored on subsequent `deliver`s.
41
47
  */
42
- export type SendOptions<TState = undefined> = TState extends undefined ? {
43
- auth: SessionAuthContext | null;
44
- callback?: SessionCallback;
45
- continuationToken: string;
46
- mode?: RunMode;
47
- } : {
48
- auth: SessionAuthContext | null;
49
- callback?: SessionCallback;
50
- continuationToken: string;
51
- mode?: RunMode;
48
+ export type SendOptions<TState = undefined> = [TState] extends [undefined] ? BaseSendOptions : BaseSendOptions & {
52
49
  state: TState;
53
50
  };
54
51
  export type GetSessionFn = (sessionId: string) => Session;
@@ -63,3 +60,4 @@ export declare function POST<TState = undefined>(path: string, handler: RouteHan
63
60
  export declare function PUT<TState = undefined>(path: string, handler: RouteHandler<TState>): RouteDefinition<TState>;
64
61
  export declare function PATCH<TState = undefined>(path: string, handler: RouteHandler<TState>): RouteDefinition<TState>;
65
62
  export declare function DELETE<TState = undefined>(path: string, handler: RouteHandler<TState>): RouteDefinition<TState>;
63
+ export {};
@@ -21,11 +21,11 @@
21
21
  "@standard-schema/spec": "1.1.0",
22
22
  "turndown": "7.2.4",
23
23
  "@vercel/oidc": "3.5.0",
24
- "@vercel/sandbox": "2.0.1",
24
+ "@vercel/sandbox": "2.1.0",
25
25
  "@workflow/core": "5.0.0-beta.10",
26
26
  "@workflow/errors": "5.0.0-beta.6",
27
27
  "zod": "4.4.3",
28
28
  "zod-validation-error": "5.0.0"
29
29
  },
30
- "scriptHash": "27a236d633a4d9d7191418c0c9eb03158389a9de5ca9a6b54bd35b754147c7f6"
30
+ "scriptHash": "3ca25d8480d76331751f15b85150bcb6cea056aa28c4fc04012dddcdaccae3e5"
31
31
  }
@@ -1,23 +1,16 @@
1
- export interface NetworkTransformer {
2
- headers?: Record<string, string> | undefined;
3
- }
4
-
5
- export interface NetworkPolicyRule {
6
- transform?: NetworkTransformer[] | undefined;
7
- }
1
+ // The firewall network-policy types are copied verbatim from the installed
2
+ // `@vercel/sandbox` at vendor time (see scripts/vendor-compiled/@vercel/sandbox.mjs)
3
+ // so the credential-brokering surface never drifts from the SDK.
4
+ import type { NetworkPolicy } from "./network-policy.js";
8
5
 
9
- export type NetworkPolicy =
10
- | "allow-all"
11
- | "deny-all"
12
- | {
13
- allow?: string[] | Record<string, NetworkPolicyRule[]> | undefined;
14
- subnets?:
15
- | {
16
- allow?: string[] | undefined;
17
- deny?: string[] | undefined;
18
- }
19
- | undefined;
20
- };
6
+ export type {
7
+ NetworkPolicy,
8
+ NetworkPolicyKeyValueMatcher,
9
+ NetworkPolicyMatch,
10
+ NetworkPolicyMatcher,
11
+ NetworkPolicyRule,
12
+ NetworkTransformer,
13
+ } from "./network-policy.js";
21
14
 
22
15
  export interface SandboxKeepLastSnapshotsConfig {
23
16
  count: number;
@@ -70,6 +63,17 @@ export interface SandboxRunCommandParams {
70
63
  sudo?: boolean | undefined;
71
64
  }
72
65
 
66
+ export interface SandboxRmOptions {
67
+ force?: boolean | undefined;
68
+ recursive?: boolean | undefined;
69
+ signal?: AbortSignal | undefined;
70
+ }
71
+
72
+ export declare class FileSystem {
73
+ rm(path: string, options?: SandboxRmOptions): Promise<void>;
74
+ unlink(path: string, options?: { signal?: AbortSignal | undefined }): Promise<void>;
75
+ }
76
+
73
77
  export interface SandboxCommandLogMessage {
74
78
  data: string;
75
79
  stream: "stdout" | "stderr";
@@ -96,6 +100,7 @@ export type SandboxCommandResult = SandboxCommandFinished;
96
100
 
97
101
  export declare class Sandbox {
98
102
  currentSnapshotId?: string | undefined;
103
+ readonly fs: FileSystem;
99
104
  id: string;
100
105
  name: string;
101
106
  networkPolicy?: NetworkPolicy | undefined;