@hatchet-dev/sandbox 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Eac
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.1] - 2026-10-29
10
+
11
+ ### Added
12
+
13
+ - `create({ from, inheritParentMetadata: true })` copies the parent sandbox's metadata onto the fork; tags given in `metadata` take precedence.
14
+ - `hatchet.sandbox.delete({ sandboxId?, metadata? })` and `sandbox.delete()`: remove sandboxes together with every snapshot and file stored for them. `kill()` keeps snapshots as before.
15
+
16
+ ### Fixed
17
+
18
+ - `files.read` of a staged file replayed in a durable task after its signed URL expired now asks the provider to sign the same file again instead of failing with `staging_failed`.
19
+
9
20
  ## [0.1.0] - 2026-10-27
10
21
 
11
22
  ### Added
package/README.md CHANGED
@@ -65,6 +65,7 @@ Your app only talks to Hatchet. A brickie provider, connected to the same tenant
65
65
  | `create(opts?)` | `Sandbox` | Starts a sandbox. |
66
66
  | `connect(id, { template? })` | `Sandbox` | Picks up a sandbox from its latest snapshot. Starts it again if it was stopped. Throws if the ID does not exist. |
67
67
  | `list({ sandboxId?, metadata? })` | `SandboxInfo[]` | Finds sandboxes by ID or by metadata. All given pairs must match. |
68
+ | `delete({ sandboxId?, metadata? })` | `string[]` | Deletes every matching sandbox with all of its snapshots and files, and returns their IDs. One filter is required. |
68
69
 
69
70
  Options for `create`:
70
71
 
@@ -74,6 +75,7 @@ Options for `create`:
74
75
  | `template` | The container image. Defaults to the provider's image. |
75
76
  | `from` | Start from this snapshot. Create two sandboxes from the same snapshot and you have a fork. |
76
77
  | `metadata` | Tags you can search for with `list`. |
78
+ | `inheritParentMetadata` | With `from`, copy the tags of the sandbox that made the snapshot onto the new one. Tags given in `metadata` take precedence. |
77
79
  | `idleTimeoutMs` | Remove the sandbox after this long with no calls. The provider sets the maximum. |
78
80
 
79
81
  ### `Sandbox`
@@ -81,7 +83,8 @@ Options for `create`:
81
83
  | Call | Returns | What it does |
82
84
  |---|---|---|
83
85
  | `exec(cmd, { timeoutMs? })` | `{ stdout, stderr, exitCode, snapshotId?, timings? }` | Runs `sh -c cmd` in the workspace and takes a snapshot. A non-zero exit code is a normal result, not an error. On timeout the exit code is 124. |
84
- | `kill()` | `void` | Removes the sandbox. The snapshots stay, so `connect` still works. |
86
+ | `kill()` | `void` | Stops the sandbox. The snapshots stay, so `connect` still works. |
87
+ | `delete()` | `void` | Stops the sandbox and deletes every snapshot and file stored for it. `connect` fails afterwards. |
85
88
  | `sandboxId` | `string` | The ID to use with `connect` later. |
86
89
  | `files` | `Files` | The file API below. |
87
90
 
@@ -110,6 +113,7 @@ The sandbox itself does not need network access for any of this.
110
113
  - `FilesystemError` has a `code`. One of: `not_found`, `invalid_path`, `invalid_url`, `invalid_content`, `invalid`, `is_a_directory`, `not_a_directory`, `too_large`, `staging_failed`, `transfer_failed`.
111
114
  - A command that fails does not throw. Look at `exitCode`.
112
115
  - `connect` with an unknown ID throws. `create` with an `id` that already exists throws too. Use `connect` for that one, or pick a new ID.
116
+ - `hatchet.sandbox.delete` without a `sandboxId` or a non-empty `metadata` filter throws before touching anything. Metadata matching is a subset match, the same as `list`: a filter of `{ a, b }` matches a sandbox tagged `{ a, b, c }`.
113
117
 
114
118
  ## Telemetry
115
119
 
package/dist/files.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { JsonObject } from '@hatchet-dev/typescript-sdk/v1';
2
- export type FileType = 'file' | 'dir' | 'symlink';
2
+ export type FileType = 'file' | 'dir';
3
3
  export interface EntryInfo {
4
4
  name: string;
5
5
  path: string;
@@ -8,7 +8,6 @@ export interface EntryInfo {
8
8
  mode: number;
9
9
  permissions: string;
10
10
  modifiedTime?: Date;
11
- symlinkTarget?: string;
12
11
  }
13
12
  export type FileData = string | ArrayBuffer | Uint8Array | Blob;
14
13
  export interface WriteEntry {
@@ -56,7 +55,7 @@ export declare class Files {
56
55
  /** @internal */
57
56
  constructor(turn: FsTurn);
58
57
  private call;
59
- /** Reads a file as text (default), bytes, or a Blob. Files over the provider's `--max-file-bytes` (2 MiB) come through the provider's object store instead of Hatchet. */
58
+ /** Reads a file as text (default), bytes, or a Blob. Files over the provider's `--max-file-bytes` (2 MiB) come through the provider's `--store` instead of Hatchet. */
60
59
  read(path: string, opts?: ReadOpts & {
61
60
  format?: 'text';
62
61
  }): Promise<string>;
@@ -66,7 +65,9 @@ export declare class Files {
66
65
  read(path: string, opts: ReadOpts & {
67
66
  format: 'blob';
68
67
  }): Promise<Blob>;
69
- /** Writes one file or several. Parent directories are created. Bytes are stored exactly. Files over 2 MiB go through the provider's object store instead of Hatchet. */
68
+ /** A replayed durable read may hold a URL that has expired; the provider signs the same staged file again. */
69
+ private readStaged;
70
+ /** Writes one file or several. Parent directories are created. Bytes are stored exactly. Files over 2 MiB go through the provider's `--store` instead of Hatchet. */
70
71
  write(path: string, data: FileData): Promise<EntryInfo>;
71
72
  write(files: WriteEntry[]): Promise<EntryInfo[]>;
72
73
  /** Writes several files in one task. */
package/dist/files.js CHANGED
@@ -70,10 +70,7 @@ async function putStaged(url, bytes) {
70
70
  }
71
71
  async function getStaged(url) {
72
72
  if (url.startsWith('file://')) {
73
- const path = decodeURIComponent(url.slice('file://'.length));
74
- const bytes = await fs.readFile(path);
75
- await fs.rm(path, { force: true });
76
- return bytes;
73
+ return fs.readFile(decodeURIComponent(url.slice('file://'.length)));
77
74
  }
78
75
  const res = await fetch(url);
79
76
  if (!res.ok)
@@ -95,13 +92,25 @@ class Files {
95
92
  }
96
93
  async read(path, opts = {}) {
97
94
  const out = await this.call('read', { path });
98
- const bytes = out.url ? await getStaged(out.url) : Buffer.from(out.content ?? '', 'base64');
95
+ const bytes = out.url ? await this.readStaged(out.url, out.stagedKey) : Buffer.from(out.content ?? '', 'base64');
99
96
  if (opts.format === 'bytes')
100
97
  return new Uint8Array(bytes);
101
98
  if (opts.format === 'blob')
102
99
  return new Blob([new Uint8Array(bytes)]);
103
100
  return bytes.toString('utf8');
104
101
  }
102
+ /** A replayed durable read may hold a URL that has expired; the provider signs the same staged file again. */
103
+ async readStaged(url, stagedKey) {
104
+ try {
105
+ return await getStaged(url);
106
+ }
107
+ catch (e) {
108
+ if (url.startsWith('file://') || !(e instanceof FilesystemError))
109
+ throw e;
110
+ const { getUrl } = await this.call('presign', { stagedKey });
111
+ return getStaged(getUrl);
112
+ }
113
+ }
105
114
  async write(pathOrFiles, data) {
106
115
  if (typeof pathOrFiles !== 'string')
107
116
  return this.writeFiles(pathOrFiles);
package/dist/sandbox.d.ts CHANGED
@@ -23,6 +23,8 @@ export interface SandboxOpts {
23
23
  metadata?: Record<string, string>;
24
24
  /** Milliseconds without a turn before the provider removes the sandbox. Capped by the provider. */
25
25
  idleTimeoutMs?: number;
26
+ /** With `from`, start from the metadata of the sandbox that made that snapshot. Pairs given in `metadata` take precedence over inherited ones. */
27
+ inheritParentMetadata?: boolean;
26
28
  }
27
29
  export interface ExecOpts {
28
30
  /** Kill the command after this long. The result then has `exitCode` 124. */
@@ -61,10 +63,14 @@ export declare class Sandbox {
61
63
  static connect(client: HatchetClient, sandboxId: string, opts?: ConnectOpts): Promise<Sandbox>;
62
64
  /** Lists sandboxes on the provider. Prefer `hatchet.sandbox.list`. */
63
65
  static list(client: HatchetClient, opts?: SandboxListOpts): Promise<SandboxInfo[]>;
66
+ /** Deletes every sandbox matching the filter, with all their snapshots and files. Prefer `hatchet.sandbox.delete`. */
67
+ static delete(client: HatchetClient, opts: SandboxListOpts): Promise<string[]>;
64
68
  /** Runs a shell command with `sh -c` in the workspace and makes a snapshot. A timeout gives `exitCode` 124, not an error. */
65
69
  exec(cmd: string, opts?: ExecOpts): Promise<CommandResult>;
66
70
  /** Removes the sandbox. Its snapshots stay; `connect` can bring it back. */
67
71
  kill(): Promise<void>;
72
+ /** Removes the sandbox and deletes every snapshot and file the provider stored for it. `connect` fails afterwards. */
73
+ delete(): Promise<void>;
68
74
  private turn;
69
75
  }
70
76
  /** `hatchet.sandbox`, added to `HatchetClient` by importing `@hatchet-dev/sandbox`. */
@@ -75,6 +81,8 @@ export interface SandboxNamespace {
75
81
  connect(sandboxId: string, opts?: ConnectOpts): Promise<Sandbox>;
76
82
  /** Lists sandboxes the provider knows, filtered by ID or metadata. */
77
83
  list(opts?: SandboxListOpts): Promise<SandboxInfo[]>;
84
+ /** Deletes the sandboxes matching the filter, with every snapshot and file stored for them. Returns their IDs. */
85
+ delete(opts: SandboxListOpts): Promise<string[]>;
78
86
  }
79
87
  declare module '@hatchet-dev/typescript-sdk/v1' {
80
88
  interface HatchetClient {
package/dist/sandbox.js CHANGED
@@ -44,6 +44,7 @@ class Sandbox {
44
44
  ...(opts.from ? { snapshotId: opts.from } : {}),
45
45
  ...(opts.metadata ? { metadata: opts.metadata } : {}),
46
46
  ...(opts.idleTimeoutMs ? { idleTtlMs: opts.idleTimeoutMs } : {}),
47
+ ...(opts.inheritParentMetadata ? { inheritMetadata: true } : {}),
47
48
  });
48
49
  return new Sandbox(run, created.sandboxId, opts.template);
49
50
  }
@@ -63,6 +64,18 @@ class Sandbox {
63
64
  const raw = (result.output.sandboxes ?? []);
64
65
  return raw.map((s) => ({ ...s, createdAt: new Date(s.createdAt), lastUsed: s.lastUsed ? new Date(s.lastUsed) : undefined }));
65
66
  }
67
+ /** Deletes every sandbox matching the filter, with all their snapshots and files. Prefer `hatchet.sandbox.delete`. */
68
+ static async delete(client, opts) {
69
+ if (!opts.sandboxId && Object.keys(opts.metadata ?? {}).length === 0)
70
+ throw new Error('delete needs a sandboxId or a metadata filter');
71
+ const run = turnRunner(client);
72
+ const ids = [];
73
+ for (const { sandboxId } of await Sandbox.list(client, opts)) {
74
+ await run({ fn: '$delete', input: {}, sandboxId });
75
+ ids.push(sandboxId);
76
+ }
77
+ return ids;
78
+ }
66
79
  /** Runs a shell command with `sh -c` in the workspace and makes a snapshot. A timeout gives `exitCode` 124, not an error. */
67
80
  async exec(cmd, opts = {}) {
68
81
  return this.turn('$command', { cmd, ...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}) });
@@ -71,6 +84,10 @@ class Sandbox {
71
84
  async kill() {
72
85
  await this.turn('$destroy', {});
73
86
  }
87
+ /** Removes the sandbox and deletes every snapshot and file the provider stored for it. `connect` fails afterwards. */
88
+ async delete() {
89
+ await this.turn('$delete', {});
90
+ }
74
91
  async turn(fn, input) {
75
92
  const result = await this.run({
76
93
  sandboxId: this.sandboxId,
@@ -89,6 +106,7 @@ Object.defineProperty(v1_1.HatchetClient.prototype, 'sandbox', {
89
106
  create: (opts) => Sandbox.create(this, opts),
90
107
  connect: (sandboxId, opts) => Sandbox.connect(this, sandboxId, opts),
91
108
  list: (opts) => Sandbox.list(this, opts),
109
+ delete: (opts) => Sandbox.delete(this, opts),
92
110
  };
93
111
  },
94
112
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchet-dev/sandbox",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Hatchet Sandbox TypeScript SDK",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,12 +34,14 @@
34
34
  "devDependencies": {
35
35
  "@hatchet-dev/typescript-sdk": "^1.33.1",
36
36
  "@types/node": "^24.0.0",
37
+ "tsx": "^4.23.15",
37
38
  "typedoc": "^0.28.0",
38
39
  "typescript": "^5.9.0"
39
40
  },
40
41
  "scripts": {
41
42
  "build": "tsc -p tsconfig.build.json",
42
43
  "typecheck": "tsc",
43
- "docs": "typedoc"
44
+ "docs": "typedoc",
45
+ "test": "node --import tsx --test --test-force-exit --test-timeout=180000 test/e2e.test.ts"
44
46
  }
45
47
  }