@hatchet-dev/sandbox 0.0.1 → 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@hatchet-dev/sandbox`, the TypeScript SDK. The provider has its own changelog at the repository root.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Each release reads its notes from the matching section here.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-27
10
+
11
+ ### Added
12
+
13
+ - `hatchet.sandbox.create`, `connect`, and `list` on the Hatchet client, by importing `@hatchet-dev/sandbox`.
14
+ - `Sandbox.exec` with `timeoutMs`, returning stdout, stderr, exit code, and per-step timings; `Sandbox.kill`.
15
+ - `Sandbox.files`: read, write, writeFiles, list, exists, getInfo, makeDir, rename, copy, remove, download, upload. Files over 2 MiB are staged through the provider's object store on presigned URLs.
16
+ - `create({ id, template, from, metadata, idleTimeoutMs })` for caller-chosen ids, images, forks, and expiry.
17
+ - Every call is a Hatchet task run, and a durable step inside a durable task.
18
+ - OpenTelemetry instrumentation, off with `BRICKIE_OTEL=false`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Hatchet Technologies Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,129 @@
1
1
  # @hatchet-dev/sandbox
2
2
 
3
- Placeholder package for Hatchet Sandbox.
3
+ <div align="center">
4
+
5
+ [![npm version](https://img.shields.io/npm/v/%40hatchet-dev%2Fsandbox)](https://www.npmjs.com/package/@hatchet-dev/sandbox)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ </div>
9
+
10
+ Run agent sandboxes from [Hatchet](https://hatchet.run) tasks.
11
+
12
+ A sandbox is a workspace with a shell. You run commands in it and read and write its files. Each call is a Hatchet task. Each call that changes files takes a snapshot. You can start a new sandbox from any snapshot, and you can come back to a sandbox later by its ID. If you use it inside a durable task, a crash picks up where it left off.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @hatchet-dev/sandbox @hatchet-dev/typescript-sdk
18
+ ```
19
+
20
+ ```bash
21
+ npm install @hatchet-dev/sandbox @hatchet-dev/typescript-sdk
22
+ ```
23
+
24
+ ```bash
25
+ yarn add @hatchet-dev/sandbox @hatchet-dev/typescript-sdk
26
+ ```
27
+
28
+ You need `@hatchet-dev/typescript-sdk` 1.33.1 or newer.
29
+
30
+ ## Quick start
31
+
32
+ Import the package once. It adds `hatchet.sandbox` to the Hatchet client.
33
+
34
+ ```ts
35
+ import { HatchetClient } from '@hatchet-dev/typescript-sdk/v1';
36
+ import '@hatchet-dev/sandbox';
37
+
38
+ const hatchet = HatchetClient.init();
39
+
40
+ const session = hatchet.durableTask({
41
+ name: 'agent-session',
42
+ executionTimeout: '1h',
43
+ fn: async (input: { conversationId: string }) => {
44
+ const sandbox = await hatchet.sandbox.create({ id: input.conversationId });
45
+ await sandbox.files.write('haiku.txt', 'an old silent pond');
46
+ const haiku = await sandbox.exec('cat haiku.txt');
47
+ await sandbox.kill();
48
+ return { haiku: haiku.stdout };
49
+ },
50
+ });
51
+
52
+ // later, from anywhere with a Hatchet client
53
+ const again = await hatchet.sandbox.connect('conv-42');
54
+ await again.exec('cat haiku.txt');
55
+ ```
56
+
57
+ Your app only talks to Hatchet. A brickie provider, connected to the same tenant, runs the sandboxes. Set `HATCHET_CLIENT_TOKEN` like you would for any Hatchet client. That is all the app needs.
58
+
59
+ ## API
60
+
61
+ ### `hatchet.sandbox`
62
+
63
+ | Call | Returns | What it does |
64
+ |---|---|---|
65
+ | `create(opts?)` | `Sandbox` | Starts a sandbox. |
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
+ | `list({ sandboxId?, metadata? })` | `SandboxInfo[]` | Finds sandboxes by ID or by metadata. All given pairs must match. |
68
+
69
+ Options for `create`:
70
+
71
+ | Option | What it does |
72
+ |---|---|
73
+ | `id` | Pick your own sandbox ID. Lowercase letters, digits, and hyphens. It must not exist yet. |
74
+ | `template` | The container image. Defaults to the provider's image. |
75
+ | `from` | Start from this snapshot. Create two sandboxes from the same snapshot and you have a fork. |
76
+ | `metadata` | Tags you can search for with `list`. |
77
+ | `idleTimeoutMs` | Remove the sandbox after this long with no calls. The provider sets the maximum. |
78
+
79
+ ### `Sandbox`
80
+
81
+ | Call | Returns | What it does |
82
+ |---|---|---|
83
+ | `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. |
85
+ | `sandboxId` | `string` | The ID to use with `connect` later. |
86
+ | `files` | `Files` | The file API below. |
87
+
88
+ Each result also has `timings` in milliseconds for `claim`, `restore`, `exec`, and `checkpoint`, and the `snapshotId` the call made.
89
+
90
+ ### `sandbox.files`
91
+
92
+ Paths are relative to the workspace. A path that points outside it throws a `FilesystemError` with code `invalid_path`.
93
+
94
+ | Call | What it does |
95
+ |---|---|
96
+ | `read(path, { format? })` | Reads a file. `format` is `'text'` (default), `'bytes'` for a `Uint8Array`, or `'blob'`. |
97
+ | `write(path, data)` or `write([{ path, data }, ...])` | Writes one file or many. `data` can be a string, `ArrayBuffer`, `Uint8Array`, or `Blob`. |
98
+ | `list(path?, { depth? })` | Lists entries with name, path, type, size, mode, permissions, and modified time. |
99
+ | `exists(path)`, `getInfo(path)` | Checks a path or gets its info. |
100
+ | `makeDir(path)`, `rename(from, to)`, `copy(from, to)`, `remove(path)` | The usual file operations. |
101
+ | `download(url, path, { timeoutMs? })` | The provider fetches an https URL into the workspace. Private and loopback addresses are refused. |
102
+ | `upload(path, url, { timeoutMs? })` | The provider sends a file to an https URL with HTTP PUT. Presigned URLs work well here. |
103
+
104
+ The sandbox itself does not need network access for any of this.
105
+
106
+ `read` and `write` carry up to 2 MiB through Hatchet. Bigger files go through the provider's object store (`--object-store` on the provider: S3, GCS, Azure Blob, or a local folder) and never pass through Hatchet. You call the same methods either way.
107
+
108
+ ## Errors
109
+
110
+ - `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
+ - A command that fails does not throw. Look at `exitCode`.
112
+ - `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.
113
+
114
+ ## Telemetry
115
+
116
+ On first use the SDK turns on the Hatchet OpenTelemetry instrumentor. If you have OpenTelemetry set up, sandbox calls show up as spans in your task's trace. Set `BRICKIE_OTEL=false` to turn this off.
117
+
118
+ ## What you need
119
+
120
+ - A Hatchet tenant. Hatchet Cloud, or a self-hosted Hatchet v0.110.0 or newer with gRPC operators turned on.
121
+ - A brickie provider connected to that tenant. It is one binary, `brickie worker`. Get it from [github.com/hatchet-dev/brickie](https://github.com/hatchet-dev/brickie).
122
+
123
+ ## Changelog
124
+
125
+ See [CHANGELOG.md](https://github.com/hatchet-dev/brickie/blob/main/sdks/typescript/CHANGELOG.md).
126
+
127
+ ## License
128
+
129
+ MIT. See [LICENSE](https://github.com/hatchet-dev/brickie/blob/main/LICENSE).
@@ -0,0 +1,93 @@
1
+ import type { JsonObject } from '@hatchet-dev/typescript-sdk/v1';
2
+ export type FileType = 'file' | 'dir' | 'symlink';
3
+ export interface EntryInfo {
4
+ name: string;
5
+ path: string;
6
+ type: FileType;
7
+ size: number;
8
+ mode: number;
9
+ permissions: string;
10
+ modifiedTime?: Date;
11
+ symlinkTarget?: string;
12
+ }
13
+ export type FileData = string | ArrayBuffer | Uint8Array | Blob;
14
+ export interface WriteEntry {
15
+ path: string;
16
+ data: FileData;
17
+ }
18
+ export interface ReadOpts {
19
+ format?: 'text' | 'bytes' | 'blob';
20
+ }
21
+ export interface TransferOpts {
22
+ /** Give up after this long. */
23
+ timeoutMs?: number;
24
+ }
25
+ export interface ListOpts {
26
+ depth?: number;
27
+ }
28
+ export type FilesystemErrorCode = 'staging_failed' | 'not_found' | 'invalid_path' | 'invalid_url' | 'invalid_content' | 'invalid' | 'is_a_directory' | 'not_a_directory' | 'too_large' | 'transfer_failed';
29
+ /** Thrown for caller mistakes such as a missing file or a path outside the workspace. */
30
+ export declare class FilesystemError extends Error {
31
+ readonly code: FilesystemErrorCode;
32
+ constructor(code: FilesystemErrorCode, message: string);
33
+ }
34
+ type RawEntry = Omit<EntryInfo, 'modifiedTime'> & {
35
+ modifiedTime?: string;
36
+ };
37
+ interface FsOutput extends JsonObject {
38
+ content?: string;
39
+ stagedKey?: string;
40
+ url?: string;
41
+ putUrl?: string;
42
+ getUrl?: string;
43
+ entry?: RawEntry | null;
44
+ entries?: RawEntry[];
45
+ created?: boolean;
46
+ error?: {
47
+ code: FilesystemErrorCode;
48
+ message: string;
49
+ };
50
+ }
51
+ /** @internal */
52
+ export type FsTurn = (fn: string, input: JsonObject) => Promise<FsOutput>;
53
+ /** File operations on a sandbox workspace. Paths are relative to the workspace. Each call is one Hatchet task. */
54
+ export declare class Files {
55
+ private readonly turn;
56
+ /** @internal */
57
+ constructor(turn: FsTurn);
58
+ 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. */
60
+ read(path: string, opts?: ReadOpts & {
61
+ format?: 'text';
62
+ }): Promise<string>;
63
+ read(path: string, opts: ReadOpts & {
64
+ format: 'bytes';
65
+ }): Promise<Uint8Array>;
66
+ read(path: string, opts: ReadOpts & {
67
+ format: 'blob';
68
+ }): 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. */
70
+ write(path: string, data: FileData): Promise<EntryInfo>;
71
+ write(files: WriteEntry[]): Promise<EntryInfo[]>;
72
+ /** Writes several files in one task. */
73
+ writeFiles(files: WriteEntry[]): Promise<EntryInfo[]>;
74
+ /** Lists a directory. `depth` 1 (default) is the directory itself; higher values recurse. No snapshot is made. */
75
+ list(path?: string, opts?: ListOpts): Promise<EntryInfo[]>;
76
+ /** True if the path exists. No snapshot is made. */
77
+ exists(path: string): Promise<boolean>;
78
+ /** Metadata of a file or directory. Throws `FilesystemError` with code `not_found` if absent. */
79
+ getInfo(path: string): Promise<EntryInfo>;
80
+ /** Creates a directory and its parents. Returns false if it already existed. */
81
+ makeDir(path: string): Promise<boolean>;
82
+ /** Renames or moves a file or directory. */
83
+ rename(oldPath: string, newPath: string): Promise<EntryInfo>;
84
+ /** Copies a file or directory tree. */
85
+ copy(srcPath: string, dstPath: string): Promise<EntryInfo>;
86
+ /** Deletes a file or directory tree. */
87
+ remove(path: string): Promise<void>;
88
+ /** The provider fetches an https URL into the workspace. The sandbox needs no network. Private and loopback addresses are refused. */
89
+ download(url: string, path: string, opts?: TransferOpts): Promise<EntryInfo>;
90
+ /** The provider sends a workspace file to an https URL with HTTP PUT, for example a presigned URL. */
91
+ upload(path: string, url: string, opts?: TransferOpts): Promise<void>;
92
+ }
93
+ export {};
package/dist/files.js ADDED
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Files = exports.FilesystemError = void 0;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ /** Thrown for caller mistakes such as a missing file or a path outside the workspace. */
39
+ class FilesystemError extends Error {
40
+ code;
41
+ constructor(code, message) {
42
+ super(message);
43
+ this.code = code;
44
+ this.name = 'FilesystemError';
45
+ }
46
+ }
47
+ exports.FilesystemError = FilesystemError;
48
+ const toEntry = (e) => ({
49
+ ...e,
50
+ modifiedTime: e.modifiedTime ? new Date(e.modifiedTime) : undefined,
51
+ });
52
+ const STAGE_OVER_BYTES = 2 << 20;
53
+ async function toBytes(data) {
54
+ if (typeof data === 'string')
55
+ return Buffer.from(data, 'utf8');
56
+ if (data instanceof Blob)
57
+ return Buffer.from(await data.arrayBuffer());
58
+ if (data instanceof Uint8Array)
59
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
60
+ return Buffer.from(data);
61
+ }
62
+ async function putStaged(url, bytes) {
63
+ if (url.startsWith('file://')) {
64
+ await fs.writeFile(decodeURIComponent(url.slice('file://'.length)), bytes);
65
+ return;
66
+ }
67
+ const res = await fetch(url, { method: 'PUT', body: new Uint8Array(bytes), headers: { 'Content-Type': 'application/octet-stream' } });
68
+ if (!res.ok)
69
+ throw new FilesystemError('staging_failed', `stage upload failed: HTTP ${res.status}`);
70
+ }
71
+ async function getStaged(url) {
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;
77
+ }
78
+ const res = await fetch(url);
79
+ if (!res.ok)
80
+ throw new FilesystemError('staging_failed', `stage download failed: HTTP ${res.status}`);
81
+ return Buffer.from(await res.arrayBuffer());
82
+ }
83
+ /** File operations on a sandbox workspace. Paths are relative to the workspace. Each call is one Hatchet task. */
84
+ class Files {
85
+ turn;
86
+ /** @internal */
87
+ constructor(turn) {
88
+ this.turn = turn;
89
+ }
90
+ async call(fn, input) {
91
+ const out = await this.turn(`$fs.${fn}`, input);
92
+ if (out.error)
93
+ throw new FilesystemError(out.error.code, out.error.message);
94
+ return out;
95
+ }
96
+ async read(path, opts = {}) {
97
+ const out = await this.call('read', { path });
98
+ const bytes = out.url ? await getStaged(out.url) : Buffer.from(out.content ?? '', 'base64');
99
+ if (opts.format === 'bytes')
100
+ return new Uint8Array(bytes);
101
+ if (opts.format === 'blob')
102
+ return new Blob([new Uint8Array(bytes)]);
103
+ return bytes.toString('utf8');
104
+ }
105
+ async write(pathOrFiles, data) {
106
+ if (typeof pathOrFiles !== 'string')
107
+ return this.writeFiles(pathOrFiles);
108
+ const [entry] = await this.writeFiles([{ path: pathOrFiles, data: data ?? '' }]);
109
+ return entry;
110
+ }
111
+ /** Writes several files in one task. */
112
+ async writeFiles(files) {
113
+ const encoded = await Promise.all(files.map(async (f) => {
114
+ const bytes = await toBytes(f.data);
115
+ if (bytes.length <= STAGE_OVER_BYTES)
116
+ return { path: f.path, content: bytes.toString('base64') };
117
+ const { stagedKey, putUrl } = await this.call('presign', {});
118
+ await putStaged(putUrl, bytes);
119
+ return { path: f.path, stagedKey: stagedKey };
120
+ }));
121
+ const { entries = [] } = await this.call('write', { files: encoded });
122
+ return entries.map(toEntry);
123
+ }
124
+ /** Lists a directory. `depth` 1 (default) is the directory itself; higher values recurse. No snapshot is made. */
125
+ async list(path = '.', opts = {}) {
126
+ const { entries = [] } = await this.call('list', { path, depth: opts.depth ?? 1 });
127
+ return entries.map(toEntry);
128
+ }
129
+ /** True if the path exists. No snapshot is made. */
130
+ async exists(path) {
131
+ const { entry } = await this.call('stat', { path });
132
+ return entry != null;
133
+ }
134
+ /** Metadata of a file or directory. Throws `FilesystemError` with code `not_found` if absent. */
135
+ async getInfo(path) {
136
+ const { entry } = await this.call('stat', { path });
137
+ if (!entry)
138
+ throw new FilesystemError('not_found', `${path} does not exist`);
139
+ return toEntry(entry);
140
+ }
141
+ /** Creates a directory and its parents. Returns false if it already existed. */
142
+ async makeDir(path) {
143
+ const { created } = await this.call('mkdir', { path });
144
+ return created === true;
145
+ }
146
+ /** Renames or moves a file or directory. */
147
+ async rename(oldPath, newPath) {
148
+ const { entry } = await this.call('rename', { path: oldPath, newPath });
149
+ return toEntry(entry);
150
+ }
151
+ /** Copies a file or directory tree. */
152
+ async copy(srcPath, dstPath) {
153
+ const { entry } = await this.call('copy', { path: srcPath, newPath: dstPath });
154
+ return toEntry(entry);
155
+ }
156
+ /** Deletes a file or directory tree. */
157
+ async remove(path) {
158
+ await this.call('remove', { path });
159
+ }
160
+ /** The provider fetches an https URL into the workspace. The sandbox needs no network. Private and loopback addresses are refused. */
161
+ async download(url, path, opts = {}) {
162
+ const { entry } = await this.call('download', { url, path, ...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}) });
163
+ return toEntry(entry);
164
+ }
165
+ /** The provider sends a workspace file to an https URL with HTTP PUT, for example a presigned URL. */
166
+ async upload(path, url, opts = {}) {
167
+ await this.call('upload', { path, url, ...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}) });
168
+ }
169
+ }
170
+ exports.Files = Files;
@@ -0,0 +1,2 @@
1
+ export { Sandbox, type SandboxNamespace, type SandboxOpts, type ConnectOpts, type ExecOpts, type SandboxListOpts, type SandboxInfo, type CommandResult, type Timings, } from './sandbox.js';
2
+ export { Files, FilesystemError, type FilesystemErrorCode, type EntryInfo, type FileType, type FileData, type WriteEntry, type ReadOpts, type ListOpts, type TransferOpts, } from './files.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FilesystemError = exports.Files = exports.Sandbox = void 0;
4
+ var sandbox_js_1 = require("./sandbox.js");
5
+ Object.defineProperty(exports, "Sandbox", { enumerable: true, get: function () { return sandbox_js_1.Sandbox; } });
6
+ var files_js_1 = require("./files.js");
7
+ Object.defineProperty(exports, "Files", { enumerable: true, get: function () { return files_js_1.Files; } });
8
+ Object.defineProperty(exports, "FilesystemError", { enumerable: true, get: function () { return files_js_1.FilesystemError; } });
package/dist/otel.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function setupTelemetry(): Promise<void>;
package/dist/otel.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupTelemetry = setupTelemetry;
4
+ let telemetryStarted = false;
5
+ async function setupTelemetry() {
6
+ if (telemetryStarted || process.env.BRICKIE_OTEL === 'false') {
7
+ return;
8
+ }
9
+ telemetryStarted = true;
10
+ try {
11
+ const [{ registerInstrumentations }, { HatchetInstrumentor }] = await Promise.all([
12
+ import('@opentelemetry/instrumentation'),
13
+ import('@hatchet-dev/typescript-sdk/opentelemetry/index.js'),
14
+ ]);
15
+ registerInstrumentations({ instrumentations: [new HatchetInstrumentor()] });
16
+ }
17
+ catch (err) {
18
+ console.error(`brickie: opentelemetry disabled: ${err.message}`);
19
+ }
20
+ }
@@ -0,0 +1,83 @@
1
+ import { HatchetClient } from '@hatchet-dev/typescript-sdk/v1';
2
+ import { Files } from './files.js';
3
+ export interface Timings {
4
+ /** Snapshot made after this turn. Absent for turns that do not change the workspace. */
5
+ snapshotId?: string;
6
+ /** Milliseconds per phase: `claim`, `restore`, `exec`, `checkpoint`. */
7
+ timings?: Record<string, number>;
8
+ }
9
+ export interface CommandResult extends Timings {
10
+ stdout: string;
11
+ stderr: string;
12
+ /** A non-zero code is a normal result, not an error. */
13
+ exitCode: number;
14
+ }
15
+ export interface SandboxOpts {
16
+ /** Sandbox ID to use instead of a generated one. Lowercase letters, digits, and hyphens; must be new on the provider. */
17
+ id?: string;
18
+ /** Container image. Defaults to the provider's image. */
19
+ template?: string;
20
+ /** Snapshot to start from. Two sandboxes with the same `from` are a fork. */
21
+ from?: string;
22
+ /** Key-value tags, used to find the sandbox again with {@link SandboxNamespace.list}. */
23
+ metadata?: Record<string, string>;
24
+ /** Milliseconds without a turn before the provider removes the sandbox. Capped by the provider. */
25
+ idleTimeoutMs?: number;
26
+ }
27
+ export interface ExecOpts {
28
+ /** Kill the command after this long. The result then has `exitCode` 124. */
29
+ timeoutMs?: number;
30
+ }
31
+ export interface ConnectOpts {
32
+ /** Image to use if the sandbox has to be started again. Defaults to the provider's image. */
33
+ template?: string;
34
+ }
35
+ export interface SandboxListOpts {
36
+ /** Only the sandbox with this ID. */
37
+ sandboxId?: string;
38
+ /** Only sandboxes whose metadata contains every one of these pairs. */
39
+ metadata?: Record<string, string>;
40
+ }
41
+ export interface SandboxInfo {
42
+ sandboxId: string;
43
+ metadata?: Record<string, string>;
44
+ /** True while the sandbox is running. A sandbox that is not live is started again by `connect`. */
45
+ live: boolean;
46
+ latestSnapshot?: string;
47
+ createdAt: Date;
48
+ lastUsed?: Date;
49
+ }
50
+ /** One sandbox. Every method is one `brickie-sandbox` Hatchet task. */
51
+ export declare class Sandbox {
52
+ private readonly run;
53
+ readonly sandboxId: string;
54
+ private readonly template?;
55
+ /** File operations on the workspace. */
56
+ readonly files: Files;
57
+ private constructor();
58
+ /** Starts a new sandbox. Prefer `hatchet.sandbox.create`. */
59
+ static create(client: HatchetClient, opts?: SandboxOpts): Promise<Sandbox>;
60
+ /** Attaches to a sandbox by ID. Prefer `hatchet.sandbox.connect`. */
61
+ static connect(client: HatchetClient, sandboxId: string, opts?: ConnectOpts): Promise<Sandbox>;
62
+ /** Lists sandboxes on the provider. Prefer `hatchet.sandbox.list`. */
63
+ static list(client: HatchetClient, opts?: SandboxListOpts): Promise<SandboxInfo[]>;
64
+ /** Runs a shell command with `sh -c` in the workspace and makes a snapshot. A timeout gives `exitCode` 124, not an error. */
65
+ exec(cmd: string, opts?: ExecOpts): Promise<CommandResult>;
66
+ /** Removes the sandbox. Its snapshots stay; `connect` can bring it back. */
67
+ kill(): Promise<void>;
68
+ private turn;
69
+ }
70
+ /** `hatchet.sandbox`, added to `HatchetClient` by importing `@hatchet-dev/sandbox`. */
71
+ export interface SandboxNamespace {
72
+ /** Starts a new sandbox. Inside a durable task this is a durable step. */
73
+ create(opts?: SandboxOpts): Promise<Sandbox>;
74
+ /** Attaches to a sandbox by ID. If it is not running, the provider starts it from its latest snapshot. */
75
+ connect(sandboxId: string, opts?: ConnectOpts): Promise<Sandbox>;
76
+ /** Lists sandboxes the provider knows, filtered by ID or metadata. */
77
+ list(opts?: SandboxListOpts): Promise<SandboxInfo[]>;
78
+ }
79
+ declare module '@hatchet-dev/typescript-sdk/v1' {
80
+ interface HatchetClient {
81
+ readonly sandbox: SandboxNamespace;
82
+ }
83
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Sandbox = void 0;
4
+ const v1_1 = require("@hatchet-dev/typescript-sdk/v1");
5
+ const files_js_1 = require("./files.js");
6
+ const otel_js_1 = require("./otel.js");
7
+ const SANDBOX_TASK_NAME = 'brickie-sandbox';
8
+ function turnRunner(client) {
9
+ const task = client.workflow({ name: SANDBOX_TASK_NAME });
10
+ return async (input) => {
11
+ const result = await task.run(input, {
12
+ additionalMetadata: {
13
+ 'brickie.fn': input.fn,
14
+ ...(input.sandboxId ? { 'brickie.sandbox': input.sandboxId } : {}),
15
+ ...(input.image ? { 'brickie.image': input.image } : {}),
16
+ ...(input.snapshotId ? { 'brickie.from': input.snapshotId } : {}),
17
+ },
18
+ });
19
+ return result[SANDBOX_TASK_NAME] ?? result;
20
+ };
21
+ }
22
+ /** One sandbox. Every method is one `brickie-sandbox` Hatchet task. */
23
+ class Sandbox {
24
+ run;
25
+ sandboxId;
26
+ template;
27
+ /** File operations on the workspace. */
28
+ files;
29
+ constructor(run, sandboxId, template) {
30
+ this.run = run;
31
+ this.sandboxId = sandboxId;
32
+ this.template = template;
33
+ this.files = new files_js_1.Files((fn, input) => this.turn(fn, input));
34
+ }
35
+ /** Starts a new sandbox. Prefer `hatchet.sandbox.create`. */
36
+ static async create(client, opts = {}) {
37
+ void (0, otel_js_1.setupTelemetry)();
38
+ const run = turnRunner(client);
39
+ const created = await run({
40
+ fn: '$create',
41
+ input: {},
42
+ ...(opts.id ? { sandboxId: opts.id } : {}),
43
+ ...(opts.template ? { image: opts.template } : {}),
44
+ ...(opts.from ? { snapshotId: opts.from } : {}),
45
+ ...(opts.metadata ? { metadata: opts.metadata } : {}),
46
+ ...(opts.idleTimeoutMs ? { idleTtlMs: opts.idleTimeoutMs } : {}),
47
+ });
48
+ return new Sandbox(run, created.sandboxId, opts.template);
49
+ }
50
+ /** Attaches to a sandbox by ID. Prefer `hatchet.sandbox.connect`. */
51
+ static async connect(client, sandboxId, opts = {}) {
52
+ void (0, otel_js_1.setupTelemetry)();
53
+ const run = turnRunner(client);
54
+ await run({ fn: '$connect', input: {}, sandboxId, ...(opts.template ? { image: opts.template } : {}) });
55
+ return new Sandbox(run, sandboxId, opts.template);
56
+ }
57
+ /** Lists sandboxes on the provider. Prefer `hatchet.sandbox.list`. */
58
+ static async list(client, opts = {}) {
59
+ const result = await turnRunner(client)({
60
+ fn: '$list',
61
+ input: { ...(opts.sandboxId ? { sandboxId: opts.sandboxId } : {}), ...(opts.metadata ? { metadata: opts.metadata } : {}) },
62
+ });
63
+ const raw = (result.output.sandboxes ?? []);
64
+ return raw.map((s) => ({ ...s, createdAt: new Date(s.createdAt), lastUsed: s.lastUsed ? new Date(s.lastUsed) : undefined }));
65
+ }
66
+ /** Runs a shell command with `sh -c` in the workspace and makes a snapshot. A timeout gives `exitCode` 124, not an error. */
67
+ async exec(cmd, opts = {}) {
68
+ return this.turn('$command', { cmd, ...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}) });
69
+ }
70
+ /** Removes the sandbox. Its snapshots stay; `connect` can bring it back. */
71
+ async kill() {
72
+ await this.turn('$destroy', {});
73
+ }
74
+ async turn(fn, input) {
75
+ const result = await this.run({
76
+ sandboxId: this.sandboxId,
77
+ ...(this.template ? { image: this.template } : {}),
78
+ fn,
79
+ input,
80
+ });
81
+ return { ...result.output, snapshotId: result.snapshotId, timings: result.timings };
82
+ }
83
+ }
84
+ exports.Sandbox = Sandbox;
85
+ Object.defineProperty(v1_1.HatchetClient.prototype, 'sandbox', {
86
+ configurable: true,
87
+ get() {
88
+ return {
89
+ create: (opts) => Sandbox.create(this, opts),
90
+ connect: (sandboxId, opts) => Sandbox.connect(this, sandboxId, opts),
91
+ list: (opts) => Sandbox.list(this, opts),
92
+ };
93
+ },
94
+ });
package/package.json CHANGED
@@ -1,14 +1,45 @@
1
1
  {
2
2
  "name": "@hatchet-dev/sandbox",
3
- "version": "0.0.1",
4
- "description": "Hatchet Sandbox SDK",
3
+ "version": "0.1.0",
4
+ "description": "Hatchet Sandbox TypeScript SDK",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/hatchet-dev/hatchet.git"
8
+ "url": "git+https://github.com/hatchet-dev/brickie.git",
9
+ "directory": "sdks/typescript"
9
10
  },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "CHANGELOG.md"
20
+ ],
10
21
  "publishConfig": {
11
22
  "access": "public"
12
23
  },
13
- "main": "index.js"
14
- }
24
+ "dependencies": {
25
+ "@opentelemetry/api": "^1.9.0",
26
+ "@opentelemetry/core": "^2.0.0",
27
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
28
+ "@opentelemetry/instrumentation": "^0.221.0",
29
+ "@opentelemetry/sdk-trace-base": "^2.0.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@hatchet-dev/typescript-sdk": ">=1.33.1"
33
+ },
34
+ "devDependencies": {
35
+ "@hatchet-dev/typescript-sdk": "^1.33.1",
36
+ "@types/node": "^24.0.0",
37
+ "typedoc": "^0.28.0",
38
+ "typescript": "^5.9.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "typecheck": "tsc",
43
+ "docs": "typedoc"
44
+ }
45
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = {};