diff --git a/README.md b/README.md index d356bb2e..61171e4b 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,9 @@ func main() { ### What gets installed -| Language | Core SDK | Providers | -| ---------- | -------------------------------------------- | ---------------------------------------------- | -| TypeScript | `@sandbox-kit/sandbox` | `@sandbox-kit/daytona`, `@sandbox-kit/modal`, `@sandbox-kit/blaxel` | +| Language | Core SDK | Providers | +| ---------- | -------------------------------------------- | ---------------------------------------------------------------------- | +| TypeScript | `@sandbox-kit/sandbox` | `@sandbox-kit/daytona`, `@sandbox-kit/modal`, `@sandbox-kit/blaxel` | | Go | `github.com/sandbox-kit/kit/sdks/go/sandbox` | `.../providers/daytona`, `.../providers/modal`, `.../providers/blaxel` | Install only the provider you use. The core SDK exposes the shared client @@ -263,8 +263,9 @@ More detail: [responses and errors](docs/responses-and-errors.md). Go helpers: `Value(v)` for optional scalars, `Duration(d)` for timeouts. - Unsupported settings fail before provider calls; quotas stay provider-enforced. - Memory and disk are MiB. Modal CPU: fractional cores from 0.125 (0.001 steps). - Daytona: whole cores and whole GiB expressed as MiB. Blaxel: `memoryMib` / - `memory_mib` maps 1:1; Blaxel derives CPU from memory. + Daytona: whole cores and whole GiB expressed as MiB. Blaxel: set + `memoryMib` / `memory_mib` (Blaxel documents MB; Kit does not convert); CPU + follows memory. Guides: [configuration](docs/configuration.md) · [creation](docs/sandbox-creation.md) · @@ -285,17 +286,17 @@ Open http://localhost:3000. Docs are organized as Quickstart, Concepts, Provider These pages are the behavior reference for the same API: -| Guide | Covers | -| -------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -| [Client configuration](docs/configuration.md) | Auth, scope, defaults, deadlines | -| [Sandbox creation](docs/sandbox-creation.md) | Fields, units, policies | -| [Sandbox lifecycle](docs/sandbox-lifecycle.md) | Get, attach, capabilities | -| [Filesystem](docs/filesystem.md) | Read, write, copy, list, stat, mkdir, remove | -| [Process](docs/process.md) | Run a command and wait for exit code + output | -| [Providers](docs/providers.md) | Daytona, Modal, and Blaxel | -| [Responses and errors](docs/responses-and-errors.md) | Error details and provider causes | -| [TS Daytona](sdks/typescript/providers/daytona/README.md) / [Modal](sdks/typescript/providers/modal/README.md) / [Blaxel](sdks/typescript/providers/blaxel/README.md) | TypeScript provider notes | -| [Go Daytona](sdks/go/providers/daytona/README.md) / [Modal](sdks/go/providers/modal/README.md) / [Blaxel](sdks/go/providers/blaxel/README.md) | Go provider notes | +| Guide | Covers | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| [Client configuration](docs/configuration.md) | Auth, scope, defaults, deadlines | +| [Sandbox creation](docs/sandbox-creation.md) | Fields, units, policies | +| [Sandbox lifecycle](docs/sandbox-lifecycle.md) | Get, attach, capabilities | +| [Filesystem](docs/filesystem.md) | Read, write, copy, list, stat, mkdir, remove | +| [Process](docs/process.md) | Run a command and wait for exit code + output | +| [Providers](docs/providers.md) | Daytona, Modal, and Blaxel | +| [Responses and errors](docs/responses-and-errors.md) | Error details and provider causes | +| [TS Daytona](sdks/typescript/providers/daytona/README.md) / [Modal](sdks/typescript/providers/modal/README.md) / [Blaxel](sdks/typescript/providers/blaxel/README.md) | TypeScript provider notes | +| [Go Daytona](sdks/go/providers/daytona/README.md) / [Modal](sdks/go/providers/modal/README.md) / [Blaxel](sdks/go/providers/blaxel/README.md) | Go provider notes | ## License diff --git a/docs/configuration.md b/docs/configuration.md index c2a56f5d..0aab14bd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -46,8 +46,8 @@ does not change the handle. ## Provider support -The bindings use Modal v0.11.1, Daytona v0.225.0, `@blaxel/core@0.3.36`, and -`github.com/blaxel-ai/sdk-go@v0.31.0`. +The bindings target Modal, Daytona, and Blaxel. Pinned dependency versions live +in each provider package. | Setting | Modal | Daytona | Blaxel | | ---------------- | ---------------------------------------------------------- | --------------------------------- | ------------------------------ | @@ -68,9 +68,8 @@ created. Omit `auth` when the provider should use credentials it already finds in the environment. Daytona can take the organization from that environment when you use a bearer token and leave organization unset. -TypeScript Blaxel clients share `@blaxel/core` process-global `settings`. Use -one Blaxel Kit client, or one credential set, per process; a second client -overwrites the first. +In TypeScript, use one Blaxel client (or one API key) per process. A second +client with different credentials replaces the first. A remote endpoint uses HTTPS. HTTP is accepted for `localhost` or a loopback address. A URL with a username, password, or fragment is rejected. Daytona uses @@ -91,9 +90,8 @@ deadline or abort signal that arrives earlier still applies. Daytona create keeps its 60-second default when the timeout is omitted and when it is explicit zero. Daytona start, stop, pause, and delete use that 60-second default when the timeout is omitted, and treat explicit zero as no -operation timeout. Blaxel uses the Kit deadline for create. `@blaxel/core` -also accepts a native creation timeout (1–50 whole seconds); this provider -does not forward it. The Go Blaxel SDK has no equivalent argument. +operation timeout. On Blaxel, your client/create `timeout` is how long create +may take. ## Credentials diff --git a/docs/filesystem.md b/docs/filesystem.md index 52f63c26..ec37b243 100644 --- a/docs/filesystem.md +++ b/docs/filesystem.md @@ -215,11 +215,13 @@ entries, err := instance.Filesystem().ListDirectory(ctx, "/tmp/kit-files", nil) ## Read metadata -`Stat` returns one path. A symlink result describes the symlink. Modal can -report a symlink. Daytona reports a file or a directory. `mode` is an octal -string when the provider supplies one, such as `0644`. `permissions`, `owner`, -and `group` are set when the provider supplies them. `modifiedAt` is set when -the provider supplies a time. +`Stat` returns one path. A symlink result describes the symlink. On Blaxel, +directories return type directory; files return type file, with size and +permission fields when Blaxel provides them. Modal can report a symlink. +Daytona reports a file or a directory. `mode` is an octal string when the +provider supplies one, such as `0644`. `permissions`, `owner`, and `group` are +set when the provider supplies them. `modifiedAt` is set when the provider +supplies a time. ```ts const info = await sandbox.filesystem.stat("/tmp/note.txt"); diff --git a/docs/process.md b/docs/process.md index 2ce968be..37d91d77 100644 --- a/docs/process.md +++ b/docs/process.md @@ -27,7 +27,7 @@ How providers run the command: | You pass | Daytona | Modal | Blaxel | | -------- | ------------------------- | ------------------------ | ------------------------- | -| `argv` | POSIX-quoted shell string | argument list as-is | POSIX-quoted shell string | +| `argv` | One quoted shell command string | argument list as-is | One quoted shell command string | | `shell` | shell string as-is | `bash -c` with that text | shell string as-is | ## Working directory and environment @@ -66,7 +66,7 @@ code. | `timeout: 0` / `Duration(0)` | Do not add an extra client deadline for this call | | Positive timeout | Cap this call; must be a whole number of seconds (`5000` ms or `5 * time.Second` is fine; `1500` ms is rejected, field `timeout`) | -Omit or zero also means **no separate provider process auto-kill** (see +Omit or zero also means **no separate provider process time limit** (see provider quirks). Provider quirks: - **Daytona** — if you omit a native timeout, Daytona defaults to 10 seconds. @@ -74,12 +74,10 @@ provider quirks). Provider quirks: capped at 10 seconds. - **Modal** — omit/zero adds no Modal command limit. In TypeScript the client omits `timeoutMs` (sending `0` is rejected by Modal). -- **Blaxel** — omit or zero adds no Blaxel process auto-kill. Timeouts must - be whole seconds. A positive timeout of at most 60 seconds uses Blaxel's - blocking completion request. Omit, zero, and longer timeouts start the - command and poll until it ends. The Kit deadline and cancel stop that - wait and leave the remote command running. A positive timeout still sets - Blaxel's process auto-kill. An HTTP 504 during the wait is a timeout error. +- **Blaxel** — omit or zero waits until the command ends (or you cancel), with + no Blaxel process time limit. Positive timeouts must be whole seconds and + cap how long the command may run. Canceling stops waiting; the remote + command may keep running. If the wait times out, you get a timeout error. Cancel behavior: @@ -87,10 +85,9 @@ Cancel behavior: error. The handle stays busy until that provider call returns. - **TypeScript** — abort rejects the `await`. The remote command may keep running. Daytona and Modal do not observe the signal inside exec. Blaxel - polls stop when the signal aborts. A Blaxel run whose timeout is 1–60 - seconds uses a completion request that does not observe the signal. The - handle stays busy until the provider call settles, so the next call on - the same handle does not overlap it. + waits stop when the signal aborts. The handle stays busy until the + provider call settles, so the next call on the same handle does not + overlap it. ```ts const result = await sandbox.process.run({ diff --git a/docs/providers.md b/docs/providers.md index 07d94fb9..398cd64a 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -78,8 +78,7 @@ volume fields; Blaxel, Daytona, and Modal volumes stay out of create), browsers, streaming process handles, sessions, PTY, and macOS or Windows creation are outside it. -TypeScript Blaxel uses `@blaxel/core` process-global `settings`: one Blaxel -Kit client, or one credential set, per process. +In TypeScript, use one Blaxel client (or one API key) per process. See [configuration](configuration.md), [creation](sandbox-creation.md), [lifecycle](sandbox-lifecycle.md), [filesystem](filesystem.md), and diff --git a/docs/responses-and-errors.md b/docs/responses-and-errors.md index 07a9ee2b..3df7e978 100644 --- a/docs/responses-and-errors.md +++ b/docs/responses-and-errors.md @@ -109,6 +109,10 @@ An error that Kit could not classify keeps an unknown kind and the original cause. A kind is a category. It is not a signal that retrying is safe, and Kit does not retry for you. +On Blaxel, creating a sandbox whose name already exists returns +`AlreadyExists`. If a delete is still finishing or another conflict occurs, +you get `Conflict`. + ## After a failure An error does not prove that no sandbox exists. Readiness or metadata handling diff --git a/docs/sandbox-creation.md b/docs/sandbox-creation.md index 5ae71d3d..89bf0530 100644 --- a/docs/sandbox-creation.md +++ b/docs/sandbox-creation.md @@ -6,23 +6,22 @@ from lifecycle operations on an existing sandbox. ## Supported creation settings -Go and TypeScript integrations target Modal v0.11.1, Daytona v0.225.0, and -Blaxel (`modal@0.11.1`, `@daytona/sdk@0.225.0`, `@blaxel/core@0.3.36` / -`github.com/blaxel-ai/sdk-go@v0.31.0`). - -| Setting | Modal | Daytona | Blaxel | -| ---------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | -| Source | Explicit registry image and an existing app | Default/specified snapshot or registry image | Named create (Blaxel name rules); registry image required | -| Runtime | Entrypoint, absolute working directory, PTY | Toolbox language and user | Unsupported | -| Isolation | Default, gVisor container, Linux VM | Nested KVM flag; class inherited from source | Unsupported | -| Resources | Fractional CPU, CPU limit, memory request/limit | Whole CPU and GiB memory/disk; image source required for overrides | Memory 1:1 (`memory_mib`); CPU/disk overrides rejected | -| Placement | Cloud and region preferences | Client target; request placement unsupported | Client `region` required for create; request placement unsupported | -| Environment and labels | Environment map and tags | Environment map and labels | Environment map and labels | -| Security | Named secrets and workload identity | Egress-placeholder secrets and public access | Unsupported | -| Network | Egress allowlists, inbound CIDRs, private network, managed proxy, ports, custom domain | Egress allowlists, outbound proxy, linked sandbox | Ports (not 80/443/8080; Default → Blaxel HTTP); outbound domains | -| Lifetime | Maximum lifetime and idle termination | TTL, ephemeral behavior, stop/pause/archive/delete policies | Maximum lifetime (≥5m) and idle terminate (≥1m); explicit idle disable rejected | -| Wait | Default/scheduled; ready requires a probe and positive timeout | Go: default, submitted, or started. TypeScript: default or started | Kit client/create timeout; native create timeout not forwarded; waitFor Default/Scheduled only | -| Observability | Verbose output | Telemetry endpoint | Unsupported | +Go and TypeScript integrations target Modal, Daytona, and Blaxel. Pinned +dependency versions live in each provider package. + +| Setting | Modal | Daytona | Blaxel | +| ---------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------- | +| Source | Explicit registry image and an existing app | Default/specified snapshot or registry image | Named create (Blaxel name rules); registry image required | +| Runtime | Entrypoint, absolute working directory, PTY | Toolbox language and user | Unsupported | +| Isolation | Default, gVisor container, Linux VM | Nested KVM flag; class inherited from source | Unsupported | +| Resources | Fractional CPU, CPU limit, memory request/limit | Whole CPU and GiB memory/disk; image source required for overrides | Memory (`memory_mib` / `memoryMib`); CPU/disk overrides rejected | +| Placement | Cloud and region preferences | Client target; request placement unsupported | Client `region` required for create; request placement unsupported | +| Environment and labels | Environment map and tags | Environment map and labels | Environment map and labels | +| Security | Named secrets and workload identity | Egress-placeholder secrets and public access | Unsupported | +| Network | Egress allowlists, inbound CIDRs, private network, managed proxy, ports, custom domain | Egress allowlists, outbound proxy, linked sandbox | Ports (not 80/443/8080; Default → Blaxel HTTP); outbound domains | +| Lifetime | Maximum lifetime and idle termination | TTL, ephemeral behavior, stop/pause/archive/delete policies | Maximum lifetime (≥5m) and idle terminate (≥1m); explicit idle disable rejected | +| Wait | Default/scheduled; ready requires a probe and positive timeout | Go: default, submitted, or started. TypeScript: default or started | Client/create timeout only; waitFor Default/Scheduled only; Ready unsupported | +| Observability | Verbose output | Telemetry endpoint | Unsupported | Kit has no portable volume fields; Blaxel volumes stay out of create the same way Daytona and Modal volumes do. @@ -50,18 +49,18 @@ field was omitted. Invalid combinations fail in the client before the provider call. A field the selected provider does not support fails there too. -| Resource | Modal | Daytona | Blaxel | -| ---------------- | ----------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------- | -| CPU | Physical cores; minimum 0.125, precision 0.001 | Whole CPU cores | Derived by Blaxel from memory; Kit rejects CPU overrides | -| Memory | MiB; minimum request 128 | Whole GiB expressed as MiB | `memory_mib` / `memoryMib` passed 1:1; Blaxel labels the unit MB | -| Disk | Creation override unsupported | Whole GiB expressed as MiB | Creation override unsupported | -| Maximum lifetime | Positive whole seconds, up to 24 hours (TS: ms) | Whole-minute TTL (TS: ms); zero disables TTL | Blaxel TTL string (at least 5m when set) | +| Resource | Modal | Daytona | Blaxel | +| ---------------- | ----------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------- | +| CPU | Physical cores; minimum 0.125, precision 0.001 | Whole CPU cores | Derived by Blaxel from memory; Kit rejects CPU overrides | +| Memory | MiB; minimum request 128 | Whole GiB expressed as MiB | `memory_mib` / `memoryMib` (Blaxel documents MB; Kit does not convert) | +| Disk | Creation override unsupported | Whole GiB expressed as MiB | Creation override unsupported | +| Maximum lifetime | Positive whole seconds, up to 24 hours (TS: ms) | Whole-minute TTL (TS: ms); zero disables TTL | At least 5 minutes when set | Daytona request conversion divides MiB exactly by 1024. Allocated metadata converts GiB back to MiB. Snapshot creation inherits snapshot resources. Positive CPU or memory limits require a corresponding request and cannot be -lower than that request. Blaxel memory is a 1:1 integer pass-through; Kit -does not convert MiB to MB. +lower than that request. On Blaxel, pass memory as an integer; Kit does not +convert units. Local checks cover syntax, known enums, documented bounds, precision, and supported combinations. Account quotas, class-specific permissions, image diff --git a/docs/sandbox-lifecycle.md b/docs/sandbox-lifecycle.md index aa5102e6..fd6174fb 100644 --- a/docs/sandbox-lifecycle.md +++ b/docs/sandbox-lifecycle.md @@ -86,16 +86,16 @@ canceled before it starts never reaches the provider. ## Provider support -| Operation | Modal | Daytona | Blaxel | -| ----------- | -------------------------------------- | ----------- | ---------------------------------------- | -| Attach | Supported; ID only, no provider lookup | Unsupported | Unsupported | -| Refresh/Get | Unsupported | Supported | Supported (lookup by sandbox name) | -| Start | Unsupported | Supported | Unsupported | -| Stop | Unsupported | Supported | Unsupported | -| Pause | Unsupported | Supported | Unsupported | -| Terminate | Supported | Unsupported | Unsupported | -| Archive | Unsupported | Supported | Unsupported | -| Delete | Unsupported | Supported | Supported | +| Operation | Modal | Daytona | Blaxel | +| ----------- | -------------------------------------- | ----------- | ---------------------------------- | +| Attach | Supported; ID only, no provider lookup | Unsupported | Unsupported | +| Refresh/Get | Unsupported | Supported | Supported (lookup by sandbox name) | +| Start | Unsupported | Supported | Unsupported | +| Stop | Unsupported | Supported | Unsupported | +| Pause | Unsupported | Supported | Unsupported | +| Terminate | Supported | Unsupported | Unsupported | +| Archive | Unsupported | Supported | Unsupported | +| Delete | Unsupported | Supported | Supported | Daytona reports its resolved sandbox class after creation. Kit maps container and Linux VM to shared isolation kinds and preserves Windows as a provider-specific @@ -108,9 +108,12 @@ Modal gVisor and Linux VM sandboxes share Modal's terminal execution lifecycle. `Stop` or `Delete`. Modal's native `FromID` performs no remote lookup, so Kit exposes it as `Client.Attach`; Modal does not advertise `Get` or `Refresh`. -Blaxel looks up sandboxes by name for `Get` and `Refresh`. `Delete` removes the -Blaxel resource. `Attach`, `Start`, `Stop`, `Pause`, `Archive`, and `Terminate` -are unsupported. Closing a Blaxel handle does not contact Blaxel. +Blaxel looks up sandboxes by name for `Get` and `Refresh`. After a successful +`Delete`, the handle reports `Deleted` and `terminal`. Wait before recreating +the same name — Blaxel may still be cleaning up and return `Conflict`. +`Attach`, `Start`, `Stop`, `Pause`, `Archive`, and `Terminate` are unsupported. +Closing a Blaxel handle does not contact Blaxel. On `Get` / `Refresh`, an idle +sandbox shows as paused and an active one shows as running. File calls on the same handle are documented in [filesystem](filesystem.md). Daytona, Modal, and Blaxel implement the same eight methods; each handle diff --git a/examples/README.md b/examples/README.md index 0833c621..00a7398c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -144,7 +144,7 @@ Indexes: [Go](go/README.md) · [TypeScript](typescript/README.md). | Blaxel remove one path | [go](go/blaxel/filesystem/remove-blaxel-file/README.md) | [typescript](typescript/blaxel/filesystem/remove-blaxel-file/README.md) | | Blaxel remote retrieval and metadata | [go](go/blaxel/lifecycle/get-blaxel-sandbox/README.md) | [typescript](typescript/blaxel/lifecycle/get-blaxel-sandbox/README.md) | | Blaxel metadata refresh | [go](go/blaxel/lifecycle/refresh-blaxel-sandbox/README.md) | [typescript](typescript/blaxel/lifecycle/refresh-blaxel-sandbox/README.md) | -| Blaxel waited deletion | [go](go/blaxel/lifecycle/delete-blaxel-sandbox/README.md) | [typescript](typescript/blaxel/lifecycle/delete-blaxel-sandbox/README.md) | +| Blaxel delete by name | [go](go/blaxel/lifecycle/delete-blaxel-sandbox/README.md) | [typescript](typescript/blaxel/lifecycle/delete-blaxel-sandbox/README.md) | | Blaxel close the handle; sandbox keeps running | [go](go/blaxel/lifecycle/close-blaxel-handle/README.md) | [typescript](typescript/blaxel/lifecycle/close-blaxel-handle/README.md) | | Blaxel typed validation errors | [go](go/blaxel/errors/handle-blaxel-errors/README.md) | [typescript](typescript/blaxel/errors/handle-blaxel-errors/README.md) | diff --git a/examples/go/README.md b/examples/go/README.md index e9aed6be..328a59fa 100644 --- a/examples/go/README.md +++ b/examples/go/README.md @@ -143,7 +143,7 @@ closes your connection to Daytona, Modal, or Blaxel. | [remove-blaxel-file](blaxel/filesystem/remove-blaxel-file/README.md) | remove one path | | [get-blaxel-sandbox](blaxel/lifecycle/get-blaxel-sandbox/README.md) | remote retrieval and metadata | | [refresh-blaxel-sandbox](blaxel/lifecycle/refresh-blaxel-sandbox/README.md) | metadata refresh | -| [delete-blaxel-sandbox](blaxel/lifecycle/delete-blaxel-sandbox/README.md) | waited deletion | +| [delete-blaxel-sandbox](blaxel/lifecycle/delete-blaxel-sandbox/README.md) | delete by name | | [close-blaxel-handle](blaxel/lifecycle/close-blaxel-handle/README.md) | close the handle; sandbox keeps running | | [handle-blaxel-errors](blaxel/errors/handle-blaxel-errors/README.md) | typed validation errors | diff --git a/examples/go/blaxel/configuration/create-blaxel-client-timeout/README.md b/examples/go/blaxel/configuration/create-blaxel-client-timeout/README.md index be7ff9bc..05820772 100644 --- a/examples/go/blaxel/configuration/create-blaxel-client-timeout/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-client-timeout/README.md @@ -8,8 +8,7 @@ Sets `Config.Timeout` / `timeout` on the shared client. This bounds local waitin ## Edge cases -- Kit `Timeout` / `provisioning.timeout` is the local create deadline. -- The Go Blaxel SDK has no separate creation-timeout argument. +- Client `Timeout` is how long create may take. ## Run diff --git a/examples/go/blaxel/configuration/create-blaxel-idle/README.md b/examples/go/blaxel/configuration/create-blaxel-idle/README.md index 1ed5e717..115c4f0c 100644 --- a/examples/go/blaxel/configuration/create-blaxel-idle/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-idle/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox that deletes after an idle period. ## What this means -Maps `lifetime.idleTerminate` (`PolicyModeAfter`) to a Blaxel `ttl-idle` expiration policy with action delete. +Set `lifetime.idleTerminate` with `after` and a delay (≥1 minute). The sandbox deletes after that idle period. You cannot disable idle terminate. ## Edge cases diff --git a/examples/go/blaxel/configuration/create-blaxel-sandbox/README.md b/examples/go/blaxel/configuration/create-blaxel-sandbox/README.md index a4f71ea7..3ada61c5 100644 --- a/examples/go/blaxel/configuration/create-blaxel-sandbox/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-sandbox/README.md @@ -4,11 +4,10 @@ Create one Blaxel sandbox from a registry image with an explicit name. Prints th ## What this means -Blaxel create requires a Blaxel-valid `name` (lowercase alphanumeric with -single hyphens, at most 49 characters) and an image source. Kit calls native -create (not create-if-not-exists). Region comes from the client config -(`BL_REGION`, default `us-pdx-1`). Closing the Kit client only drops the local -SDK connection. +Create needs a Blaxel-valid `name` (lowercase alphanumeric with single hyphens, +at most 49 characters), an image, and client `region` (`BL_REGION`, default +`us-pdx-1`). If the name already exists, you get `AlreadyExists`. Closing the +client does not delete the sandbox. ## Edge cases diff --git a/examples/go/blaxel/configuration/create-blaxel-with-allowlists/README.md b/examples/go/blaxel/configuration/create-blaxel-with-allowlists/README.md index 38604ccb..f29f6e1c 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-allowlists/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-with-allowlists/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox that allows selected outbound domains. ## What this means -Maps `network.outboundDomains` to Blaxel `allowedDomains`. Other egress controls are unsupported. +Set `network.outboundDomains` to allow those hosts. Other egress controls are unsupported on Blaxel. ## Edge cases diff --git a/examples/go/blaxel/configuration/create-blaxel-with-labels/README.md b/examples/go/blaxel/configuration/create-blaxel-with-labels/README.md index f7580dbb..60bc3cca 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-labels/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-with-labels/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox with labels and environment variables. ## What this means -Labels become Blaxel metadata labels. Environment entries are injected into the sandbox runtime. +Set `labels` and create-time environment variables. They appear as sandbox metadata and runtime env. ## Edge cases diff --git a/examples/go/blaxel/configuration/create-blaxel-with-ports/README.md b/examples/go/blaxel/configuration/create-blaxel-with-ports/README.md index 958c3ba8..8130b787 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-ports/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-with-ports/README.md @@ -4,8 +4,8 @@ Create one Blaxel sandbox that publishes selected ports. ## What this means -Maps `network.ports` to Blaxel runtime ports. Default leaves the protocol to -Blaxel (HTTP). Plain → TCP, TLS → TLS. HTTP/2 over TLS is rejected. +Publish ports with `network.ports`. Default protocol is HTTP; `plain` is TCP; +`tls` is TLS. HTTP/2 over TLS is not supported. ## Edge cases diff --git a/examples/go/blaxel/configuration/create-blaxel-with-resources/README.md b/examples/go/blaxel/configuration/create-blaxel-with-resources/README.md index deb277cf..74bc69fe 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-resources/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-with-resources/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox with an explicit memory size in MiB. Blaxel derives CP ## What this means -Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Omitted memory keeps the Blaxel default. +You can set `resources.memory_mib` only. CPU cores, limits, and disk overrides are rejected. Omit memory to use Blaxel's default. ## Edge cases diff --git a/examples/go/blaxel/configuration/create-blaxel-with-ttl/README.md b/examples/go/blaxel/configuration/create-blaxel-with-ttl/README.md index fd88526b..a5bc4649 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-ttl/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-with-ttl/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox that deletes after a maximum lifetime TTL. ## What this means -Maps `lifetime.maximumLifetime` to Blaxel max-age TTL. Durations must be whole seconds and meet Blaxel minima (5 minutes for max-age). +Set `lifetime.maximumLifetime` (whole seconds, at least 5 minutes). The sandbox deletes when that max age elapses. ## Edge cases diff --git a/examples/go/blaxel/errors/handle-blaxel-errors/README.md b/examples/go/blaxel/errors/handle-blaxel-errors/README.md index e61c3202..5837e996 100644 --- a/examples/go/blaxel/errors/handle-blaxel-errors/README.md +++ b/examples/go/blaxel/errors/handle-blaxel-errors/README.md @@ -4,7 +4,7 @@ Demonstrate typed validation errors from the Blaxel provider without creating a ## What this means -Requests an unsupported CPU override so Kit fails before calling Blaxel. Applications inspect `SandboxError` kind and field. +Requests an unsupported CPU override so validation fails before any cloud call. Inspect `SandboxError` `kind` and `field`. ## Edge cases @@ -34,8 +34,8 @@ BL_REGION= ## Warnings -- Acts on **real** Blaxel resources when credentials succeed. -- Closing the handle leaves the sandbox as it is. Closing the client closes your connection to the provider. +- Validation fails locally for the unsupported CPU field; no sandbox is created. +- Closing the client closes your connection to the provider. ## Related diff --git a/examples/go/blaxel/filesystem/stat-blaxel-file/README.md b/examples/go/blaxel/filesystem/stat-blaxel-file/README.md index d3c34bcf..74c8aff9 100644 --- a/examples/go/blaxel/filesystem/stat-blaxel-file/README.md +++ b/examples/go/blaxel/filesystem/stat-blaxel-file/README.md @@ -4,7 +4,8 @@ Stat one absolute path in a Blaxel sandbox. ## What this means -Returns type and size for the path. +Returns whether the path is a file or a directory, plus size and mode when +Blaxel provides them. Size may be zero when Blaxel does not return it. ## Edge cases diff --git a/examples/go/blaxel/lifecycle/delete-blaxel-sandbox/README.md b/examples/go/blaxel/lifecycle/delete-blaxel-sandbox/README.md index f5c15d06..a24aa306 100644 --- a/examples/go/blaxel/lifecycle/delete-blaxel-sandbox/README.md +++ b/examples/go/blaxel/lifecycle/delete-blaxel-sandbox/README.md @@ -1,10 +1,12 @@ # Delete a Blaxel sandbox -Delete an existing Blaxel sandbox by name and wait for completion. +Delete an existing Blaxel sandbox by name. ## What this means -`Delete` permanently removes the Blaxel sandbox. Start, stop, pause, archive, and terminate are unsupported. +`Delete` permanently removes the Blaxel sandbox. When the call succeeds you get +`Deleted` with `terminal` set. Wait before recreating the same name, or you may +get `Conflict`. Start, stop, pause, archive, and terminate are unsupported. ## Edge cases diff --git a/examples/go/blaxel/lifecycle/get-blaxel-sandbox/README.md b/examples/go/blaxel/lifecycle/get-blaxel-sandbox/README.md index 7dd59b04..9c20ab2c 100644 --- a/examples/go/blaxel/lifecycle/get-blaxel-sandbox/README.md +++ b/examples/go/blaxel/lifecycle/get-blaxel-sandbox/README.md @@ -4,7 +4,7 @@ Look up an existing Blaxel sandbox by name and print metadata. ## What this means -`Get` retrieves the sandbox by name (the Blaxel id). Use the name printed by a create example as `BL_SANDBOX_NAME`. +`client.Get` loads the sandbox by name (the Blaxel id). Use the name printed by a create example as `BL_SANDBOX_NAME`. ## Edge cases diff --git a/examples/go/blaxel/lifecycle/get-blaxel-sandbox/main.go b/examples/go/blaxel/lifecycle/get-blaxel-sandbox/main.go index 631c801d..e05df12a 100644 --- a/examples/go/blaxel/lifecycle/get-blaxel-sandbox/main.go +++ b/examples/go/blaxel/lifecycle/get-blaxel-sandbox/main.go @@ -44,5 +44,5 @@ func main() { log.Fatal(err) } info := instance.Info() - fmt.Printf("Blaxel sandbox: id=%s state=%s native=%s capabilities=%v\n", info.Id, info.State, info.GetProviderState(), instance.Capabilities()) + fmt.Printf("Blaxel sandbox: id=%s state=%s terminal=%v capabilities=%v\n", info.Id, info.State, info.GetTerminal(), instance.Capabilities()) } diff --git a/examples/go/blaxel/lifecycle/refresh-blaxel-sandbox/README.md b/examples/go/blaxel/lifecycle/refresh-blaxel-sandbox/README.md index 7f515ed1..e77aa5ca 100644 --- a/examples/go/blaxel/lifecycle/refresh-blaxel-sandbox/README.md +++ b/examples/go/blaxel/lifecycle/refresh-blaxel-sandbox/README.md @@ -4,11 +4,11 @@ Refresh metadata for an existing Blaxel sandbox by name. ## What this means -`Refresh` re-fetches provider metadata onto the handle. +`Refresh` updates `Info()` on this handle (state, labels, and related metadata). ## Edge cases -- Unsupported when the handle does not report the refresh capability. +- If this handle does not support refresh, you get `Unsupported`. ## Run diff --git a/examples/go/blaxel/process/run-blaxel-check-exit/README.md b/examples/go/blaxel/process/run-blaxel-check-exit/README.md index 404dd054..d522867b 100644 --- a/examples/go/blaxel/process/run-blaxel-check-exit/README.md +++ b/examples/go/blaxel/process/run-blaxel-check-exit/README.md @@ -4,7 +4,7 @@ Run a command that exits non-zero and read the exit code from the result. ## What this means -A non-zero exit is not an SDK error. Applications check `exitCode` / `GetExitCode`. +A non-zero exit is not an SDK error. Check `GetExitCode` on the result. ## Edge cases diff --git a/examples/go/blaxel/process/run-blaxel-command/README.md b/examples/go/blaxel/process/run-blaxel-command/README.md index c4752777..b6589b83 100644 --- a/examples/go/blaxel/process/run-blaxel-command/README.md +++ b/examples/go/blaxel/process/run-blaxel-command/README.md @@ -4,7 +4,7 @@ Run one argv command in a Blaxel sandbox and print exit code plus streams. ## What this means -`Process.Run` is blocking. Set exactly one of `argv` or `shell`. Argv is sent as a POSIX-quoted shell string. +`Process().Run` waits until the command finishes. Set exactly one of `Argv` or `Shell`. `Argv` is sent as one quoted shell command string. ## Edge cases diff --git a/examples/go/blaxel/process/run-blaxel-with-timeout/README.md b/examples/go/blaxel/process/run-blaxel-with-timeout/README.md index 10484231..fe03fe1b 100644 --- a/examples/go/blaxel/process/run-blaxel-with-timeout/README.md +++ b/examples/go/blaxel/process/run-blaxel-with-timeout/README.md @@ -1,13 +1,12 @@ # Run a Blaxel command with timeout -Cap a Blaxel Process.Run with a whole-second timeout. +Cap a Blaxel `Process().Run` with a whole-second timeout. ## What this means -Command timeouts must be whole seconds. This run uses 5 seconds, so Blaxel -kills the process at 5 seconds and waits on its completion request. Omit or -zero adds no process auto-kill; Kit polls until the command ends or the call -is canceled. A timeout longer than 60 seconds sets the auto-kill and polls. +Command timeouts must be whole seconds. This run uses 5 seconds and caps how +long the command may run. Omit or zero waits until the command ends (or you +cancel), with no Blaxel process time limit. ## Edge cases diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 54a0b9fe..03daac26 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -146,7 +146,7 @@ Blaxel needs an API key, workspace, and client region: | [remove-blaxel-file](blaxel/filesystem/remove-blaxel-file/README.md) | remove one path | | [get-blaxel-sandbox](blaxel/lifecycle/get-blaxel-sandbox/README.md) | remote retrieval and metadata | | [refresh-blaxel-sandbox](blaxel/lifecycle/refresh-blaxel-sandbox/README.md) | metadata refresh | -| [delete-blaxel-sandbox](blaxel/lifecycle/delete-blaxel-sandbox/README.md) | waited deletion | +| [delete-blaxel-sandbox](blaxel/lifecycle/delete-blaxel-sandbox/README.md) | delete by name | | [close-blaxel-handle](blaxel/lifecycle/close-blaxel-handle/README.md) | close the handle; sandbox keeps running | | [handle-blaxel-errors](blaxel/errors/handle-blaxel-errors/README.md) | typed validation errors | diff --git a/examples/typescript/blaxel/configuration/create-blaxel-client-timeout/README.md b/examples/typescript/blaxel/configuration/create-blaxel-client-timeout/README.md index ec9a7b1f..97cc74ce 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-client-timeout/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-client-timeout/README.md @@ -8,8 +8,7 @@ Sets `Config.Timeout` / `timeout` on the shared client. This bounds local waitin ## Edge cases -- Kit `timeout` / `provisioning.timeout` is the local create deadline. -- Blaxel's native creation timeout (1–50s on `@blaxel/core`) is not forwarded. +- Client `timeout` is how long create may take. ## Run diff --git a/examples/typescript/blaxel/configuration/create-blaxel-idle/README.md b/examples/typescript/blaxel/configuration/create-blaxel-idle/README.md index 80a76660..ff9b7d40 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-idle/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-idle/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox that deletes after an idle period. ## What this means -Maps `lifetime.idleTerminate` (`PolicyModeAfter`) to a Blaxel `ttl-idle` expiration policy with action delete. +Set `lifetime.idleTerminate` with `after` and a delay (≥1 minute). The sandbox deletes after that idle period. You cannot disable idle terminate. ## Edge cases diff --git a/examples/typescript/blaxel/configuration/create-blaxel-sandbox/README.md b/examples/typescript/blaxel/configuration/create-blaxel-sandbox/README.md index 34e61c14..f0fdc4d4 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-sandbox/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-sandbox/README.md @@ -4,11 +4,10 @@ Create one Blaxel sandbox from a registry image with an explicit name. Prints th ## What this means -Blaxel create requires a Blaxel-valid `name` (lowercase alphanumeric with -single hyphens, at most 49 characters), an image source, and client `region` -(examples read `BL_REGION`, default `us-pdx-1`). Kit calls native create (not -create-if-not-exists). Closing the Kit client only drops the local SDK -connection. +Create needs a Blaxel-valid `name` (lowercase alphanumeric with single hyphens, +at most 49 characters), an image, and client `region` (examples read +`BL_REGION`, default `us-pdx-1`). If the name already exists, you get +`AlreadyExists`. Closing the client does not delete the sandbox. ## Edge cases diff --git a/examples/typescript/blaxel/configuration/create-blaxel-with-allowlists/README.md b/examples/typescript/blaxel/configuration/create-blaxel-with-allowlists/README.md index ed063530..58aacdef 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-allowlists/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-allowlists/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox that allows selected outbound domains. ## What this means -Maps `network.outboundDomains` to Blaxel `allowedDomains`. Other egress controls are unsupported. +Set `network.outboundDomains` to allow those hosts. Other egress controls are unsupported on Blaxel. ## Edge cases diff --git a/examples/typescript/blaxel/configuration/create-blaxel-with-labels/README.md b/examples/typescript/blaxel/configuration/create-blaxel-with-labels/README.md index 3b78ed50..4ca86597 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-labels/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-labels/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox with labels and environment variables. ## What this means -Labels become Blaxel metadata labels. Environment entries are injected into the sandbox runtime. +Set `labels` and create-time environment variables. They appear as sandbox metadata and runtime env. ## Edge cases diff --git a/examples/typescript/blaxel/configuration/create-blaxel-with-ports/README.md b/examples/typescript/blaxel/configuration/create-blaxel-with-ports/README.md index 20a57e3f..b03a2b12 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-ports/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-ports/README.md @@ -4,8 +4,8 @@ Create one Blaxel sandbox that publishes selected ports. ## What this means -Maps `network.ports` to Blaxel runtime ports. Default leaves the protocol to -Blaxel (HTTP). Plain → TCP, TLS → TLS. HTTP/2 over TLS is rejected. +Publish ports with `network.ports`. Default protocol is HTTP; `plain` is TCP; +`tls` is TLS. HTTP/2 over TLS is not supported. ## Edge cases diff --git a/examples/typescript/blaxel/configuration/create-blaxel-with-resources/README.md b/examples/typescript/blaxel/configuration/create-blaxel-with-resources/README.md index 20d973a8..6cf1ed43 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-resources/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-resources/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox with an explicit memory size in MiB. Blaxel derives CP ## What this means -Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Omitted memory keeps the Blaxel default. +You can set `resources.memoryMib` only. CPU cores, limits, and disk overrides are rejected. Omit memory to use Blaxel's default. ## Edge cases diff --git a/examples/typescript/blaxel/configuration/create-blaxel-with-ttl/README.md b/examples/typescript/blaxel/configuration/create-blaxel-with-ttl/README.md index 6dd0a4e8..681637f8 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-ttl/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-ttl/README.md @@ -4,7 +4,7 @@ Create one Blaxel sandbox that deletes after a maximum lifetime TTL. ## What this means -Maps `lifetime.maximumLifetime` to Blaxel max-age TTL. Durations must be whole seconds and meet Blaxel minima (5 minutes for max-age). +Set `lifetime.maximumLifetime` (whole seconds, at least 5 minutes). The sandbox deletes when that max age elapses. ## Edge cases diff --git a/examples/typescript/blaxel/errors/handle-blaxel-errors/README.md b/examples/typescript/blaxel/errors/handle-blaxel-errors/README.md index 50d0d5e6..0bc9b30c 100644 --- a/examples/typescript/blaxel/errors/handle-blaxel-errors/README.md +++ b/examples/typescript/blaxel/errors/handle-blaxel-errors/README.md @@ -4,7 +4,7 @@ Demonstrate typed validation errors from the Blaxel provider without creating a ## What this means -Requests an unsupported CPU override so Kit fails before calling Blaxel. Applications inspect `SandboxError` kind and field. +Requests an unsupported CPU override so validation fails before any cloud call. Inspect `SandboxError` `kind` and `field`. ## Edge cases @@ -36,8 +36,8 @@ BL_REGION= ## Warnings -- Acts on **real** Blaxel resources when credentials succeed. -- Closing the handle leaves the sandbox as it is. Closing the client closes your connection to the provider. +- Validation fails locally for the unsupported CPU field; no sandbox is created. +- Closing the client closes your connection to the provider. ## Related diff --git a/examples/typescript/blaxel/filesystem/stat-blaxel-file/README.md b/examples/typescript/blaxel/filesystem/stat-blaxel-file/README.md index f09f82c0..f14f2a09 100644 --- a/examples/typescript/blaxel/filesystem/stat-blaxel-file/README.md +++ b/examples/typescript/blaxel/filesystem/stat-blaxel-file/README.md @@ -4,7 +4,8 @@ Stat one absolute path in a Blaxel sandbox. ## What this means -Returns type and size for the path. +Returns whether the path is a file or a directory, plus size and mode when +Blaxel provides them. ## Edge cases diff --git a/examples/typescript/blaxel/lifecycle/delete-blaxel-sandbox/README.md b/examples/typescript/blaxel/lifecycle/delete-blaxel-sandbox/README.md index 4fdaa8a5..20219121 100644 --- a/examples/typescript/blaxel/lifecycle/delete-blaxel-sandbox/README.md +++ b/examples/typescript/blaxel/lifecycle/delete-blaxel-sandbox/README.md @@ -1,10 +1,12 @@ # Delete a Blaxel sandbox -Delete an existing Blaxel sandbox by name and wait for completion. +Delete an existing Blaxel sandbox by name. ## What this means -`Delete` permanently removes the Blaxel sandbox. Start, stop, pause, archive, and terminate are unsupported. +`delete` permanently removes the Blaxel sandbox. When the call succeeds you get +`deleted` / `terminal: true`. Wait before recreating the same name, or you may +get `Conflict`. Start, stop, pause, archive, and terminate are unsupported. ## Edge cases diff --git a/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/README.md b/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/README.md index f1992552..c5dadaa5 100644 --- a/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/README.md +++ b/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/README.md @@ -4,7 +4,7 @@ Look up an existing Blaxel sandbox by name and print metadata. ## What this means -`Get` retrieves the sandbox by name (the Blaxel id). Use the name printed by a create example as `BL_SANDBOX_NAME`. +`client.get` loads the sandbox by name (the Blaxel id). Use the name printed by a create example as `BL_SANDBOX_NAME`. ## Edge cases diff --git a/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/src/main.ts b/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/src/main.ts index 716b0a2c..781cda82 100644 --- a/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/src/main.ts +++ b/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox/src/main.ts @@ -14,5 +14,5 @@ await using client = new Client({ const sandbox = await client.get(process.env.BL_SANDBOX_NAME!); const info = sandbox.info(); console.log( - `Blaxel sandbox: id=${info.id} state=${info.state} native=${info.providerState ?? ""} capabilities=${sandbox.capabilities().join(",")}`, + `Blaxel sandbox: id=${info.id} state=${info.state} terminal=${info.terminal ?? false} capabilities=${sandbox.capabilities().join(",")}`, ); diff --git a/examples/typescript/blaxel/lifecycle/refresh-blaxel-sandbox/README.md b/examples/typescript/blaxel/lifecycle/refresh-blaxel-sandbox/README.md index 081c167b..fc8fcfac 100644 --- a/examples/typescript/blaxel/lifecycle/refresh-blaxel-sandbox/README.md +++ b/examples/typescript/blaxel/lifecycle/refresh-blaxel-sandbox/README.md @@ -4,11 +4,11 @@ Refresh metadata for an existing Blaxel sandbox by name. ## What this means -`Refresh` re-fetches provider metadata onto the handle. +`sandbox.refresh` updates `info()` on this handle (state, labels, and related metadata). ## Edge cases -- Unsupported when the handle does not report the refresh capability. +- If this handle does not support refresh, you get `Unsupported`. ## Run diff --git a/examples/typescript/blaxel/process/run-blaxel-check-exit/README.md b/examples/typescript/blaxel/process/run-blaxel-check-exit/README.md index b7a3bc69..d6519273 100644 --- a/examples/typescript/blaxel/process/run-blaxel-check-exit/README.md +++ b/examples/typescript/blaxel/process/run-blaxel-check-exit/README.md @@ -4,7 +4,7 @@ Run a command that exits non-zero and read the exit code from the result. ## What this means -A non-zero exit is not an SDK error. Applications check `exitCode` / `GetExitCode`. +A non-zero exit is not an SDK error. Check `exitCode` on the result. ## Edge cases diff --git a/examples/typescript/blaxel/process/run-blaxel-command/README.md b/examples/typescript/blaxel/process/run-blaxel-command/README.md index 919567a5..bd77ba12 100644 --- a/examples/typescript/blaxel/process/run-blaxel-command/README.md +++ b/examples/typescript/blaxel/process/run-blaxel-command/README.md @@ -4,7 +4,7 @@ Run one argv command in a Blaxel sandbox and print exit code plus streams. ## What this means -`Process.Run` is blocking. Set exactly one of `argv` or `shell`. Argv is sent as a POSIX-quoted shell string. +`sandbox.process.run` waits until the command finishes. Set exactly one of `argv` or `shell`. `argv` is sent as one quoted shell command string. ## Edge cases diff --git a/examples/typescript/blaxel/process/run-blaxel-with-timeout/README.md b/examples/typescript/blaxel/process/run-blaxel-with-timeout/README.md index 0c054906..440b2ea1 100644 --- a/examples/typescript/blaxel/process/run-blaxel-with-timeout/README.md +++ b/examples/typescript/blaxel/process/run-blaxel-with-timeout/README.md @@ -1,13 +1,12 @@ # Run a Blaxel command with timeout -Cap a Blaxel Process.Run with a whole-second timeout. +Cap a Blaxel `sandbox.process.run` with a whole-second timeout. ## What this means -Command timeouts must be whole seconds. This run uses 5 seconds, so Blaxel -kills the process at 5 seconds and waits on its completion request. Omit or -zero adds no process auto-kill; Kit polls until the command ends or the call -is canceled. A timeout longer than 60 seconds sets the auto-kill and polls. +Command timeouts must be whole seconds. This run uses 5 seconds and caps how +long the command may run. Omit or zero waits until the command ends (or you +cancel), with no Blaxel process time limit. ## Edge cases diff --git a/sdks/go/providers/blaxel/README.md b/sdks/go/providers/blaxel/README.md index cb045c82..7e4c9dc1 100644 --- a/sdks/go/providers/blaxel/README.md +++ b/sdks/go/providers/blaxel/README.md @@ -3,8 +3,8 @@ Create and manage Blaxel sandboxes with the shared sandbox client. Requires Go 1.26.0 or later and a Blaxel API key. Set `Auth` to an API key and -`Scope.Workspace` to the workspace name. If either is omitted, the Blaxel SDK -uses `BL_API_KEY` and `BL_WORKSPACE` from the environment when present. +`Scope.Workspace` to the workspace name. If either is omitted, the client uses +`BL_API_KEY` and `BL_WORKSPACE` from the environment when present. `Config.Region` is required for create. ## Install @@ -60,47 +60,68 @@ sandbox. Call `Delete`, or set a lifetime policy. `Config.Endpoint` is not supported. Token pair, OAuth, and bearer auth are rejected. `Scope` fields other than `Workspace` are rejected. -## Operations - -Create from a registry image with an explicit name. The name must be lowercase -alphanumeric with single hyphens and at most 49 characters. You can set labels, -environment variables, memory, ports, outbound domains, maximum lifetime, and -idle terminate. Max-age TTL must be at least 5m; idle TTL must be at least 1m. -`PolicyModeDisabled` on idle terminate is rejected. Ports 80, 443, and 8080 are -rejected (Blaxel-reserved). Default port transport leaves the protocol to -Blaxel (HTTP); plain maps to TCP; TLS maps to TLS. HTTP/2 over TLS is rejected. -Kit `Config.Timeout` / create call timeout is the create deadline. The Go -Blaxel SDK has no separate creation-timeout argument. `WaitFor` accepts only -Default and Scheduled; `WaitConditionReady` is rejected. Create always uses -the native create path (not create-if-not-exists). - -On the handle: `Get`, `Refresh`, `Delete`, and `Close`. `Get` and `Refresh` look -up the sandbox by name (`ID()` equals the create name). `Attach`, `Start`, -`Stop`, `Pause`, `Archive`, and `Terminate` are not supported. `Close` does not -contact Blaxel and does not change the sandbox. - -`Filesystem` supports the shared file calls: `ReadFile`, `WriteFile`, -`CopyFromLocal`, `CopyToLocal`, `MakeDirectory`, `ListDirectory`, `Stat`, and -`Remove`. Paths in the sandbox are absolute. A file timeout is the context -deadline. `Parents` defaults to true. `Parents` set to false is rejected. A -missing file passed to `CopyFromLocal` is not found, and the error field is -`local_path`. A local directory is an invalid argument with the same field. -`CopyToLocal` creates a missing local parent directory. - -`Process().Run` runs one command and waits for the result. Set exactly one of -`Argv` or `Shell`. `Argv` is sent as a POSIX-quoted shell string. A non-zero -exit code is success for the SDK—read `GetExitCode`. Blaxel returns stdout and -stderr. Omit or zero `Timeout` for no Blaxel process auto-kill; Kit then polls -until the command ends or the context ends. A positive timeout of at most 60 -seconds uses Blaxel's blocking completion request. A longer timeout sets the -process auto-kill and polls. Command timeouts must be whole seconds. Cancel -the context to stop waiting; the remote command may keep running. An HTTP 504 -during the wait is a timeout error. - -`resources.memory_mib` maps 1:1 onto Blaxel's memory field. Blaxel labels that -integer MB; Kit does not convert. Blaxel derives CPU from that value, so -`cpu_cores` and disk overrides are rejected. Kit has no portable volume -fields; Blaxel volumes are outside create. +## What you can do + +### Create + +You need a name and an image. Names are lowercase alphanumeric with single +hyphens (max 49 characters). You can also set labels, environment variables, +memory, ports, outbound domains, maximum lifetime (≥5 minutes), and idle +terminate (≥1 minute). You cannot disable idle terminate. Ports 80, 443, and +8080 are reserved. Default port transport is HTTP; plain is TCP; TLS is TLS; +HTTP/2 over TLS is not supported. + +Your client/create timeout is how long create may take. Ready wait and +readiness probes are not supported (`WaitFor` only Default or Scheduled). + +Set `Resources.MemoryMib` for memory size. Blaxel documents that value as MB; +Kit does not convert units. CPU follows memory on Blaxel — do not set CPU or +disk overrides. If you omit memory, Blaxel applies its default. Volumes are not +part of create. + +If the name already exists, you get `AlreadyExists`. + +### Lifecycle + +You can `Get`, `Refresh`, `Delete`, and `Close`. Lookup is by name +(`ID()` equals the create name). Attach, start, stop, pause, archive, and +terminate are not available. `Close` only drops the local handle. + +After a successful `Delete`, the handle reports `Deleted` and `terminal`. +Blaxel can still be cleaning up — recreating the same name right away may +return `Conflict`. + +On `Get` / `Refresh` / `Info`, an idle sandbox shows as `Paused`; an active +one shows as `Running`. + +### Filesystem + +You get the usual file APIs: read, write, copy in/out, mkdir, list, stat, and +remove. Paths must be absolute. `Parents` defaults to true; `Parents: false` +is rejected. + +`Stat` returns whether the path is a file or a directory, plus size and mode +when Blaxel provides them. Missing local files for `CopyFromLocal` return not +found with field `local_path`. + +### Process + +`Process().Run` runs one command and waits for it to finish. Set exactly one of +`Argv` or `Shell`. `Argv` is sent as one quoted shell command string. A +non-zero exit code is not an error — read `GetExitCode`. You get both stdout +and stderr. + +Omit or zero `Timeout` to wait without a Blaxel process time limit (your +context can still cancel). A positive timeout must be whole seconds and caps +how long the command may run. Canceling the context stops waiting; the remote +command may keep running. If the wait times out, you get a timeout error. + +### Errors you will see + +| Situation | Kind | +| ------------------------------------------ | --------------- | +| Create when the name already exists | `AlreadyExists` | +| Conflict while a delete is still finishing | `Conflict` | ## Examples diff --git a/sdks/go/providers/blaxel/creation.go b/sdks/go/providers/blaxel/creation.go index e9036cee..3fb5de27 100644 --- a/sdks/go/providers/blaxel/creation.go +++ b/sdks/go/providers/blaxel/creation.go @@ -153,7 +153,7 @@ func mapIdleTerminate(action *sandbox.AutomaticAction) (*blaxel.ExpirationPolicy Value: blaxel.String(formatted), }, nil default: - return nil, providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldLifetimeIdleTerminate, "unknown policy mode") + return nil, providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldLifetimeIdleTerminateMode, "unknown policy mode") } } diff --git a/sdks/go/providers/blaxel/doc.go b/sdks/go/providers/blaxel/doc.go index 41906517..46ecc05c 100644 --- a/sdks/go/providers/blaxel/doc.go +++ b/sdks/go/providers/blaxel/doc.go @@ -3,12 +3,13 @@ // Pass [New] to sandbox.NewClient. Create requires a Blaxel-valid sandbox name // (lowercase alphanumeric with single hyphens, at most 49 characters), an // image source, and [sandbox.Config.Region]. Set Scope.Workspace and an API -// key, or omit them so the Blaxel SDK can use BL_API_KEY and BL_WORKSPACE from -// the environment. The backend implements shared filesystem operations through +// key, or omit them to use BL_API_KEY and BL_WORKSPACE from the environment. +// The backend implements shared filesystem operations through // [sandbox.SandboxFiles] and blocking command execution through // [sandbox.SandboxProcess]. Get and Refresh look up a sandbox by name. Delete // removes the Blaxel resource. Closing the client releases local provider // state and leaves the Blaxel sandbox running. Start, Stop, Pause, Archive, -// Attach, and Terminate are unsupported. resources.memory_mib maps 1:1 onto -// Blaxel's memory field; Kit has no portable volume fields. +// Attach, and Terminate are unsupported. Set resources.memory_mib for memory +// size; Blaxel documents that value as MB and Kit does not convert units. +// Volumes are not part of create. package blaxel diff --git a/sdks/go/providers/blaxel/errors.go b/sdks/go/providers/blaxel/errors.go index 5df67a40..15e43778 100644 --- a/sdks/go/providers/blaxel/errors.go +++ b/sdks/go/providers/blaxel/errors.go @@ -3,6 +3,7 @@ package blaxel import ( "context" "errors" + "net/http" sdkblaxel "github.com/blaxel-ai/sdk-go" "github.com/sandbox-kit/kit/sdks/go/internal/sdk" @@ -78,6 +79,8 @@ var providerStatusKinds = map[int]sandbox.ErrorKind{ 404: sandbox.ErrorKindNotFound, 410: sandbox.ErrorKindNotFound, 408: sandbox.ErrorKindTimeout, + // Bare 409 is Conflict for non-create ops. Create maps it to AlreadyExists + // in statusKindFor (name collision). Coded conflicts still win above. 409: sandbox.ErrorKindConflict, 412: sandbox.ErrorKindFailedPrecondition, 429: sandbox.ErrorKindRateLimited, @@ -87,6 +90,17 @@ var providerStatusKinds = map[int]sandbox.ErrorKind{ 504: sandbox.ErrorKindTimeout, } +// statusKindFor maps an HTTP status. Bare 409 on create is treated as a name +// collision (AlreadyExists); other operations keep Conflict so portable code +// that branches on kind matches Daytona's conflict semantics outside create. +func statusKindFor(status int, operation string) (sandbox.ErrorKind, bool) { + if status == http.StatusConflict && operation == "create" { + return sandbox.ErrorKindAlreadyExists, true + } + kind, ok := providerStatusKinds[status] + return kind, ok +} + func mapProviderError(err error, operation string) error { if err == nil { return nil @@ -123,7 +137,7 @@ func mapProviderError(err error, operation string) error { if kind == sandbox.ErrorKindUnknown { if mapped, ok := providerCodeKinds[native.ErrorCode]; ok { kind = mapped - } else if mapped, ok := providerStatusKinds[native.StatusCode]; ok { + } else if mapped, ok := statusKindFor(native.StatusCode, operation); ok { kind = mapped } } diff --git a/sdks/go/providers/blaxel/errors_test.go b/sdks/go/providers/blaxel/errors_test.go index 89272f2e..f06cbc90 100644 --- a/sdks/go/providers/blaxel/errors_test.go +++ b/sdks/go/providers/blaxel/errors_test.go @@ -53,6 +53,55 @@ func TestMapProviderErrorSandboxAlreadyExists(t *testing.T) { } } +func TestMapProviderErrorBareConflictIsAlreadyExists(t *testing.T) { + parsed, err := url.Parse("https://example.invalid/sandboxes/demo") + if err != nil { + t.Fatal(err) + } + mapped := mapProviderError(&sdkblaxel.Error{ + StatusCode: http.StatusConflict, + Request: &http.Request{Method: http.MethodPost, URL: parsed}, + Response: &http.Response{StatusCode: http.StatusConflict}, + }, "create") + var detail *sandbox.Error + if !errors.As(mapped, &detail) || detail.Kind != sandbox.ErrorKindAlreadyExists { + t.Fatalf("error = %v detail = %#v", mapped, detail) + } +} + +func TestMapProviderErrorBareConflictOnDeleteIsConflict(t *testing.T) { + parsed, err := url.Parse("https://example.invalid/sandboxes/demo") + if err != nil { + t.Fatal(err) + } + mapped := mapProviderError(&sdkblaxel.Error{ + StatusCode: http.StatusConflict, + Request: &http.Request{Method: http.MethodDelete, URL: parsed}, + Response: &http.Response{StatusCode: http.StatusConflict}, + }, "delete") + var detail *sandbox.Error + if !errors.As(mapped, &detail) || detail.Kind != sandbox.ErrorKindConflict { + t.Fatalf("error = %v detail = %#v", mapped, detail) + } +} + +func TestMapProviderErrorDeletionInProgressStaysConflict(t *testing.T) { + parsed, err := url.Parse("https://example.invalid/sandboxes/demo") + if err != nil { + t.Fatal(err) + } + mapped := mapProviderError(&sdkblaxel.Error{ + StatusCode: http.StatusConflict, + ErrorCode: "SANDBOX_DELETION_IN_PROGRESS", + Request: &http.Request{Method: http.MethodDelete, URL: parsed}, + Response: &http.Response{StatusCode: http.StatusConflict}, + }, "delete") + var detail *sandbox.Error + if !errors.As(mapped, &detail) || detail.Kind != sandbox.ErrorKindConflict { + t.Fatalf("error = %v detail = %#v", mapped, detail) + } +} + func TestMapProviderErrorGatewayTimeout(t *testing.T) { parsed, err := url.Parse("https://example.invalid/sandboxes/demo/process") if err != nil { diff --git a/sdks/go/providers/blaxel/filesystem.go b/sdks/go/providers/blaxel/filesystem.go index f3275cde..76244991 100644 --- a/sdks/go/providers/blaxel/filesystem.go +++ b/sdks/go/providers/blaxel/filesystem.go @@ -81,13 +81,7 @@ func (b *backend) ListDirectory(ctx context.Context, id, path string, _ *sandbox return nil, err } result := make([]*sandbox.FileInfo, 0, len(listing.Files)+len(listing.Subdirectories)) - for _, entry := range listing.Files { - info, err := blaxelFileInfo(entry, sandbox.FileTypeFile, "list_directory") - if err != nil { - return nil, err - } - result = append(result, info) - } + // Directories first, then files — matches TypeScript Blaxel listing order. for _, entry := range listing.Subdirectories { result = append(result, &sandbox.FileInfo{ Name: entry.Name, @@ -95,6 +89,13 @@ func (b *backend) ListDirectory(ctx context.Context, id, path string, _ *sandbox Type: sandbox.FileTypeDirectory, }) } + for _, entry := range listing.Files { + info, err := blaxelFileInfo(entry, sandbox.FileTypeFile, "list_directory") + if err != nil { + return nil, err + } + result = append(result, info) + } return result, nil }) } @@ -104,12 +105,14 @@ func (b *backend) Stat(ctx context.Context, id, path string, _ *sandbox.StatOpti if err != nil { return nil, err } - // No public Stat on SandboxInstanceFSService; rebuild the lower FS client - // against the instance URL so metadata uses the same sandbox endpoint. + // SandboxInstanceFSService has Read/LS/… but no public Get/Stat. LS always + // calls AsDirectory(), so a file path would look like an empty directory. + // Rebuild the lower SandboxFilesystemService with the instance base URL + // (same construction NewInstance uses for instance.FS) and call Get. fs := b.filesystemService(instance) res, err := fs.Get(ctx, path, blaxel.SandboxFilesystemGetParams{}) if err != nil { - return nil, mapProviderError(err, "stat") + return nil, mapFilesystemError(err, "stat") } if res == nil { return nil, invalidResponse("stat", sandbox.FileFieldPath, "SDK returned no file metadata") @@ -166,11 +169,31 @@ func withFS[T any]( if errors.As(err, &local) { return zero, err } - return zero, mapProviderError(err, call.Operation) + return zero, mapFilesystemError(err, call.Operation) } return result, nil } +// mapFilesystemError attaches Field=path on NotFound so portable callers can +// branch the same way as TypeScript Blaxel Stat/list failures. +func mapFilesystemError(err error, operation string) error { + mapped := mapProviderError(err, operation) + var detail *sandbox.Error + if !errors.As(mapped, &detail) || detail.Kind != sandbox.ErrorKindNotFound || detail.Field != "" { + return mapped + } + return sandbox.NewError(&sandbox.ErrorInfo{ + Kind: detail.Kind, + Provider: detail.Provider, + Operation: detail.Operation, + Field: sandbox.FileFieldPath, + Message: detail.Message, + StatusCode: detail.StatusCode, + ProviderCode: detail.ProviderCode, + ProviderSource: detail.ProviderSource, + }, err) +} + func (b *backend) fileSandbox(ctx context.Context, id, operation string) (*blaxel.SandboxInstance, error) { instance, err := b.getInstance(ctx, id, operation) if err != nil { diff --git a/sdks/go/providers/blaxel/lifecycle.go b/sdks/go/providers/blaxel/lifecycle.go index 0eb3bc2f..eb665fcd 100644 --- a/sdks/go/providers/blaxel/lifecycle.go +++ b/sdks/go/providers/blaxel/lifecycle.go @@ -11,10 +11,12 @@ var _ sandbox.SandboxDeleter = (*backend)(nil) // DeleteSandbox permanently deletes a Blaxel sandbox by name. // Start, stop, pause, archive, and terminate are not implemented. +// Blaxel's DELETE may still finish asynchronously; a successful call is +// reported as Deleted so portable callers get the same terminal outcome. func (b *backend) DeleteSandbox(ctx context.Context, id string, _ *sandbox.DeleteOptions) (*sandbox.SandboxInfo, error) { - instance, err := b.getInstance(ctx, id, "delete") + instance, err := b.client.Sandboxes.DeleteInstance(ctx, id) if err != nil { - return nil, err + return nil, mapProviderError(err, "delete") } native, err := requireSandbox(instance, "delete") if err != nil { @@ -24,9 +26,6 @@ func (b *backend) DeleteSandbox(ctx context.Context, id string, _ *sandbox.Delet if err != nil { return nil, err } - if err := instance.Delete(ctx); err != nil { - return nil, mapProviderError(err, "delete") - } info.State = sandbox.SandboxStateDeleted info.Terminal = sandbox.Value(true) info.ProviderState = sandbox.Value(string(blaxel.StatusTerminated)) diff --git a/sdks/go/providers/blaxel/process.go b/sdks/go/providers/blaxel/process.go index 4300c5c2..3acd5b65 100644 --- a/sdks/go/providers/blaxel/process.go +++ b/sdks/go/providers/blaxel/process.go @@ -12,11 +12,8 @@ import ( ) const ( - // Docs limit waitForCompletion to 60s; longer holds can be closed by the edge. - blaxelCompletionHold = 60 * time.Second // Process.Wait treats 0 as 1m; only -1 is unlimited. blaxelWaitUnlimited = time.Duration(-1) - processPollInterval = time.Second // Blaxel SDK default poll gap ) var _ sandbox.SandboxProcess = (*backend)(nil) @@ -34,21 +31,15 @@ func (b *backend) RunCommand(ctx context.Context, id string, request *sandbox.Ex if err != nil { return nil, err } - startedAt := time.Now() started, err := instance.Process.New(ctx, body) if err != nil { return nil, mapProviderError(err, "run") } - return finishProcess(ctx, finishProcessArgs{ - Instance: instance, - Started: started, - Timeout: timeout, - Elapsed: time.Since(startedAt), - }) + return finishProcess(ctx, instance, started, timeout) } -// processRequest builds Process.New. Timeout 0 disables Blaxel auto-kill; -// WaitForCompletion is only set for 1–60s. +// processRequest starts without waitForCompletion; Kit waits via Process.Wait. +// Timeout 0 disables Blaxel process auto-kill. func processRequest(request *sandbox.ExecOptions, timeout time.Duration) (blaxel.ProcessRequestParam, error) { command, err := blaxelCommand(request) if err != nil { @@ -57,7 +48,7 @@ func processRequest(request *sandbox.ExecOptions, timeout time.Duration) (blaxel body := blaxel.ProcessRequestParam{ Command: command, Timeout: blaxel.Int(int64(timeout / time.Second)), - WaitForCompletion: blaxel.Bool(completionHold(timeout)), + WaitForCompletion: blaxel.Bool(false), } if request.WorkingDirectory != nil { body.WorkingDir = blaxel.String(*request.WorkingDirectory) @@ -68,58 +59,26 @@ func processRequest(request *sandbox.ExecOptions, timeout time.Duration) (blaxel return body, nil } -type finishProcessArgs struct { - Instance *blaxel.SandboxInstance - Started *blaxel.ProcessResponse - Timeout time.Duration - // Elapsed covers Process.New, including any completion-hold wait. - Elapsed time.Duration -} - -// finishProcess returns a terminal start as-is; otherwise polls with Wait -// (also when waitForCompletion left the process running). Wait retries 504. -func finishProcess(ctx context.Context, args finishProcessArgs) (*sandbox.ExecResult, error) { - if args.Started == nil || processFinished(args.Started) { - return execResult(args.Started) +// finishProcess returns a terminal start as-is; otherwise one native Wait. +func finishProcess(ctx context.Context, instance *blaxel.SandboxInstance, started *blaxel.ProcessResponse, timeout time.Duration) (*sandbox.ExecResult, error) { + if started == nil || processFinished(started) { + return execResult(started) } - identifier := processIdentifier(args.Started) + identifier := processIdentifier(started) if identifier == "" { return nil, invalidResponse("run", "", "SDK returned no process identifier") } - budget := remainingWaitBudget(args.Timeout, args.Elapsed) - if args.Timeout > 0 && budget <= 0 { - return nil, kitError(kitFault{ - Kind: sandbox.ErrorKindTimeout, - Operation: "run", - Field: sandbox.ExecFieldTimeout, - Message: "process did not finish before the timeout", - }) + budget := blaxelWaitUnlimited + if timeout > 0 { + budget = timeout } - finished, err := args.Instance.Process.Wait(ctx, identifier, budget, processPollInterval) + finished, err := instance.Process.Wait(ctx, identifier, budget, 0) if err != nil { return nil, mapProviderError(err, "run") } return execResult(finished) } -func completionHold(timeout time.Duration) bool { - return timeout > 0 && timeout <= blaxelCompletionHold -} - -// remainingWaitBudget is the poll window after Process.New. Omit/zero stays -// unlimited. A fully consumed positive timeout yields 0 (caller times out); -// Blaxel Wait treats 0 as 1m, so finishProcess must not pass that through. -func remainingWaitBudget(timeout, elapsed time.Duration) time.Duration { - if timeout <= 0 { - return blaxelWaitUnlimited - } - remaining := timeout - elapsed - if remaining <= 0 { - return 0 - } - return remaining -} - var terminalProcessStatus = map[blaxel.ProcessResponseStatus]struct{}{ blaxel.ProcessResponseStatusCompleted: {}, blaxel.ProcessResponseStatusFailed: {}, diff --git a/sdks/go/providers/blaxel/process_test.go b/sdks/go/providers/blaxel/process_test.go index dfd48907..370b293c 100644 --- a/sdks/go/providers/blaxel/process_test.go +++ b/sdks/go/providers/blaxel/process_test.go @@ -40,25 +40,7 @@ func TestCommandTimeoutWholeSeconds(t *testing.T) { } } -func TestCompletionHoldAndWaitBudget(t *testing.T) { - if completionHold(0) || completionHold(61*time.Second) || !completionHold(60*time.Second) || !completionHold(time.Second) { - t.Fatal("completion hold boundary") - } - if remainingWaitBudget(0, time.Minute) != blaxelWaitUnlimited { - t.Fatal("unlimited wait budget") - } - if remainingWaitBudget(90*time.Second, 0) != 90*time.Second { - t.Fatal("full wait budget") - } - if remainingWaitBudget(90*time.Second, 30*time.Second) != 60*time.Second { - t.Fatal("remaining wait budget") - } - if remainingWaitBudget(5*time.Second, 5*time.Second) != 0 || remainingWaitBudget(5*time.Second, 6*time.Second) != 0 { - t.Fatal("exhausted wait budget") - } -} - -func TestRunCommandUsesCompletionHoldForShortTimeouts(t *testing.T) { +func TestRunCommandStartsWithoutCompletionHold(t *testing.T) { var posts []map[string]any backend := processBackend(t, func(w http.ResponseWriter, r *http.Request) { switch { @@ -88,19 +70,19 @@ func TestRunCommandUsesCompletionHoldForShortTimeouts(t *testing.T) { if len(posts) != 2 { t.Fatalf("posts = %#v", posts) } - if posts[0]["waitForCompletion"] != true || posts[0]["timeout"] != float64(5) || posts[0]["command"] != "'echo' 'hi'" || posts[0]["workingDir"] != "/workspace" { + if posts[0]["waitForCompletion"] != false || posts[0]["timeout"] != float64(5) || posts[0]["command"] != "'echo' 'hi'" || posts[0]["workingDir"] != "/workspace" { t.Fatalf("short post = %#v", posts[0]) } env, _ := posts[0]["env"].(map[string]any) if env["MODE"] != "test" { t.Fatalf("env = %#v", posts[0]["env"]) } - if posts[1]["waitForCompletion"] != true || posts[1]["timeout"] != float64(60) { + if posts[1]["waitForCompletion"] != false || posts[1]["timeout"] != float64(60) { t.Fatalf("60s post = %#v", posts[1]) } } -func TestRunCommandPollsWhenTheCompletionHoldIsUnsafe(t *testing.T) { +func TestRunCommandWaitsWhenStartReturnsRunning(t *testing.T) { var gets atomic.Int32 var posts []map[string]any backend := processBackend(t, func(w http.ResponseWriter, r *http.Request) { @@ -129,6 +111,7 @@ func TestRunCommandPollsWhenTheCompletionHoldIsUnsafe(t *testing.T) { {"omit", &sandbox.ExecOptions{Shell: sandbox.Value("echo hi")}, 0}, {"zero", &sandbox.ExecOptions{Argv: []string{"echo", "hi"}, Timeout: sandbox.Duration(0)}, 0}, {"long", &sandbox.ExecOptions{Argv: []string{"echo", "hi"}, Timeout: sandbox.Duration(61 * time.Second)}, 61}, + {"short", &sandbox.ExecOptions{Argv: []string{"echo", "hi"}, Timeout: sandbox.Duration(5 * time.Second)}, 5}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -155,41 +138,6 @@ func TestRunCommandPollsWhenTheCompletionHoldIsUnsafe(t *testing.T) { } } -func TestRunCommandPollsWhenCompletionHoldStillReturnsRunning(t *testing.T) { - var gets atomic.Int32 - backend := processBackend(t, func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && r.URL.Path == "/sandboxes/sbx": - writeJSON(t, w, map[string]any{"metadata": map[string]string{"name": "sbx"}, "spec": map[string]any{}}) - case r.Method == http.MethodPost && r.URL.Path == "/process": - body := readJSON(t, r) - if body["waitForCompletion"] != true || body["timeout"] != float64(5) { - t.Fatalf("post = %#v", body) - } - writeProcess(t, w, "running", 0, "", "") - case r.Method == http.MethodGet && r.URL.Path == "/process/123": - gets.Add(1) - writeProcess(t, w, "completed", 0, "late", "") - default: - t.Errorf("unexpected %s %s", r.Method, r.URL.Path) - http.NotFound(w, r) - } - }) - result, err := backend.RunCommand(context.Background(), "sbx", &sandbox.ExecOptions{ - Argv: []string{"echo", "hi"}, - Timeout: sandbox.Duration(5 * time.Second), - }) - if err != nil { - t.Fatal(err) - } - if result.GetExitCode() != 0 || string(result.GetStdout()) != "late" { - t.Fatalf("result = %#v", result) - } - if gets.Load() < 1 { - t.Fatalf("gets = %d", gets.Load()) - } -} - func TestRunCommandReturnsAFinishedStartWithoutPolling(t *testing.T) { backend := processBackend(t, func(w http.ResponseWriter, r *http.Request) { switch { diff --git a/sdks/typescript/providers/blaxel/README.md b/sdks/typescript/providers/blaxel/README.md index ef16bbfe..72cbcdf7 100644 --- a/sdks/typescript/providers/blaxel/README.md +++ b/sdks/typescript/providers/blaxel/README.md @@ -34,68 +34,86 @@ const sandbox = await client.create({ console.log(sandbox.id); // same as create name; use with get / BL_SANDBOX_NAME ``` -Closing the client releases local SDK resources only. Reclaim sandboxes with -TTL / idle policies or `delete`. +Closing the client releases local resources only. Reclaim sandboxes with TTL / +idle policies or `delete`. ## Configuration -| Setting | Notes | -| -------------- | ----------------------------------------- | -| API key | `auth: { apiKey: { key } }` | -| Omitted `auth` | Blaxel SDK environment / CLI credentials | -| Workspace | `scope.workspace` — Blaxel workspace name | -| Region | `region` — required for create | -| Endpoint | Not supported | +| Setting | Notes | +| -------------- | ------------------------------------------------------- | +| API key | `auth: { apiKey: { key } }` | +| Omitted `auth` | Uses `BL_API_KEY` / `BL_WORKSPACE` from the environment | +| Workspace | `scope.workspace` — Blaxel workspace name | +| Region | `region` — required for create | +| Endpoint | Not supported | -Token-pair, bearer, and OAuth client modes are rejected. Scope fields other -than `workspace` are rejected. +Token-pair, bearer, and OAuth are rejected. Only `scope.workspace` is accepted +on scope. + +In TypeScript, use one Blaxel client (or one API key) per process. A second +client with different credentials replaces the first. ## What you can do -**Create** requires a Blaxel-valid `name` (lowercase alphanumeric with single -hyphens, at most 49 characters), an image source, and client `region`. -Optional `memoryMib` maps 1:1 onto Blaxel's memory field (Blaxel labels that -integer MB; Kit does not convert; Blaxel CPU follows memory), labels, -environment variables, ports, outbound domains, max-age TTL (at least 5m), and -idle-delete policy (at least 1m; explicit disable is rejected). Client / -create `timeout` is the Kit deadline. `@blaxel/core`'s native creation timeout -(1–50s) is not forwarded. `provisioning.waitFor` accepts only `Default` and -`Scheduled`; Ready (and Started / Submitted) are unsupported. Creation calls -native `SandboxInstance.create`, not `createIfNotExists`. Ports -80, 443, and 8080 are rejected (Blaxel-reserved). Default port transport leaves -the protocol to Blaxel (HTTP); plain maps to TCP; TLS maps to TLS. HTTP/2 over -TLS is rejected. Kit has no portable volume fields; Blaxel volumes are outside -create. - -**Lifecycle** on the handle: `get` / `refresh` (lookup by name), `delete`, and -`close`. `close` does not delete the sandbox. Start, stop, pause, archive, -terminate, and attach are unsupported. - -**Files** through `sandbox.filesystem`: `readFile`, `writeFile`, -`copyFromLocal`, `copyToLocal`, `makeDirectory`, `listDirectory`, `stat`, and -`remove`. Paths in the sandbox are absolute. `parents` defaults to true; -`parents: false` is rejected. A missing file passed to `copyFromLocal` is not -found before Blaxel is called, and the error field is `local_path`. A local -directory is an invalid argument with the same field. `copyToLocal` creates a -missing local parent directory. - -**Commands** through `sandbox.process.run`. Set exactly one of `argv` or -`shell`. `argv` is sent as a POSIX-quoted shell string. `run` waits until the -command ends. A non-zero exit code is not an SDK error—check `exitCode`. -Blaxel returns stdout and stderr. Omit or zero `timeout` for no process -auto-kill; Kit then polls until the command ends or the signal aborts. A -positive timeout of at most 60 seconds uses Blaxel's blocking completion -request. A longer timeout sets the process auto-kill and polls. Command -timeouts must be whole seconds (`2000` is two seconds; `1500` is rejected). -Aborting the signal rejects the `await`; the remote command may keep running. -An HTTP 504 during the wait is a timeout error. - -Omitted memory keeps the Blaxel default. CPU, CPU limit, memory limit, and -disk overrides are rejected. - -`@blaxel/core` applies credentials through process-global `settings`. Use one -Blaxel Kit client, or one credential set, per process; a second client -overwrites the first. +### Create + +You need a valid `name` (lowercase alphanumeric with single hyphens, max 49 +characters), an image, and `region`. You can also set `memoryMib`, labels, +environment variables, ports, outbound domains, max-age TTL (≥5 minutes), and +idle-delete (≥1 minute). You cannot disable idle terminate. If you omit +memory, Blaxel applies its default. CPU and disk overrides are rejected — +Blaxel derives CPU from memory. Set `memoryMib` for memory size; Blaxel +documents that value as MB and Kit does not convert units. + +Your client/create `timeout` is how long create may take. Ready wait is not +supported (`waitFor` only Default or Scheduled). Ports 80, 443, and 8080 are +reserved. Default port transport is HTTP; plain is TCP; TLS is TLS; HTTP/2 +over TLS is not supported. Volumes are not part of create. + +If the name already exists, you get `AlreadyExists`. + +### Lifecycle + +You can `get` / `refresh` (by name), `delete`, and `close`. `close` does not +delete the sandbox. Start, stop, pause, archive, terminate, and attach are not +available. + +After a successful `delete`, you get `state: "deleted"` and `terminal: true`. +Blaxel can still be cleaning up — recreating the same name right away may +return `Conflict`. + +On `get` / `refresh` / `info`, an idle sandbox shows as `paused`; an active one +shows as `running`. + +### Filesystem + +You get the usual file APIs: `readFile`, `writeFile`, `copyFromLocal`, +`copyToLocal`, `makeDirectory`, `listDirectory`, `stat`, and `remove`. Paths +must be absolute. `parents` defaults to true; `parents: false` is rejected. + +For directories, `stat` returns type directory. For files, `stat` returns type +file with size and permission fields when Blaxel provides them. +Missing local files for `copyFromLocal` return not found with field +`local_path`. + +### Process + +`sandbox.process.run` waits until the command finishes. Set exactly one of +`argv` or `shell`. `argv` is sent as one quoted shell command string. A non-zero exit code is not +an error — check `exitCode`. You get both stdout and stderr. + +Omit or zero `timeout` to wait without a Blaxel process time limit (your +signal can still abort). A positive timeout must be whole seconds (`2000` is +two seconds; `1500` is rejected) and caps how long the command may run. +Aborting the signal stops waiting; the remote command may keep running. If the +wait times out, you get a timeout error. + +### Errors you will see + +| Situation | Kind | +| ------------------------------------------ | --------------- | +| Create when the name already exists | `AlreadyExists` | +| Conflict while a delete is still finishing | `Conflict` | ## Examples @@ -107,6 +125,7 @@ overwrites the first. | Read a file | [read-blaxel-file](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/blaxel/filesystem/read-blaxel-file) | | Filesystem | [filesystem examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/blaxel/filesystem) | | Retrieval | [get-blaxel-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/blaxel/lifecycle/get-blaxel-sandbox) | +| Delete | [delete-blaxel-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/blaxel/lifecycle/delete-blaxel-sandbox) | | Full index | [TypeScript examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript) | ## License diff --git a/sdks/typescript/providers/blaxel/src/blaxel.test.ts b/sdks/typescript/providers/blaxel/src/blaxel.test.ts index 6e276d19..c00cb8a6 100644 --- a/sdks/typescript/providers/blaxel/src/blaxel.test.ts +++ b/sdks/typescript/providers/blaxel/src/blaxel.test.ts @@ -22,7 +22,6 @@ import { CreateFieldNetworkPortsTransport, CreateFieldProvisioningWaitFor, FileFieldLocalPath, - FileFieldPath, } from "@sandbox-kit/sandbox"; import { mapClientConfig, @@ -37,8 +36,8 @@ import { } from "#mappings/provider_mappings.js"; import { captureProviderState } from "#config/provider_config.js"; import { mapBlaxelError } from "#errors.js"; -import { remainingWaitBudgetMs } from "./process.js"; -import { ResponseError } from "@blaxel/core"; +import { projectHandle } from "./native.js"; +import { ResponseError, SandboxInstance } from "@blaxel/core"; const provider = { scope: { workspace: "ws" }, region: "us-pdx-1" }; @@ -163,7 +162,6 @@ describe("Blaxel mappings", () => { }, provider, ); - expect(plan.options).toBeUndefined(); expect(plan).toMatchObject({ params: { name: "example", @@ -321,6 +319,56 @@ describe("Blaxel mappings", () => { }); }); + it("classifies bare HTTP 409 as already exists", () => { + const native = new ResponseError( + new Response(null, { status: 409, statusText: "Conflict" }), + { error: { message: "conflict" } }, + new Error("conflict"), + ); + expect(mapBlaxelError(native, "create")).toMatchObject({ + kind: ErrorKind.AlreadyExists, + provider: "blaxel", + operation: "create", + statusCode: 409, + cause: native, + }); + }); + + it("classifies bare HTTP 409 outside create as conflict", () => { + const native = new ResponseError( + new Response(null, { status: 409, statusText: "Conflict" }), + { error: { message: "conflict" } }, + new Error("conflict"), + ); + expect(mapBlaxelError(native, "delete")).toMatchObject({ + kind: ErrorKind.Conflict, + provider: "blaxel", + operation: "delete", + statusCode: 409, + cause: native, + }); + }); + + it("keeps SANDBOX_DELETION_IN_PROGRESS as conflict", () => { + const native = new ResponseError( + new Response(null, { status: 409, statusText: "Conflict" }), + { + error: { + code: "SANDBOX_DELETION_IN_PROGRESS", + message: "deleting", + status: 409, + }, + }, + new Error("deleting"), + ); + expect(mapBlaxelError(native, "delete")).toMatchObject({ + kind: ErrorKind.Conflict, + providerCode: "SANDBOX_DELETION_IN_PROGRESS", + statusCode: 409, + cause: native, + }); + }); + it("classifies edge gateway 504 as timeout", () => { const native = new ResponseError( new Response(null, { status: 504, statusText: "Gateway Timeout" }), @@ -498,13 +546,40 @@ describe("Blaxel mappings", () => { }); }); - it("reads runtime state from SandboxInstance wire model", () => { + it("maps projected runtime state from the native adapter", () => { expect( mapSandboxIdentity({ metadata: { name: "demo" }, status: "DEPLOYED", - // SandboxInstance exposes runtime state only on its private wire model. - ...({ sandbox: { state: "STANDBY" } } as object), + // defaultNativeFactory.projectHandle copies wire sandbox.state here. + state: "STANDBY", + }).state, + ).toBe(SandboxState.Paused); + }); + + it("projectHandle copies private wire sandbox.state onto state", () => { + const instance = new SandboxInstance({ + metadata: { name: "demo" }, + status: "DEPLOYED", + spec: { region: "us-pdx-1" }, + } as never); + // @blaxel/core types sandbox private; seed the wire model for the adapter. + Object.assign(instance as object, { + sandbox: { + metadata: { name: "demo" }, + status: "DEPLOYED", + state: "STANDBY", + spec: { region: "us-pdx-1" }, + }, + }); + const handle = projectHandle(instance); + expect(handle.state).toBe("STANDBY"); + expect( + mapSandboxIdentity({ + metadata: { name: "demo" }, + status: "DEPLOYED", + state: handle.state, + spec: { region: "us-pdx-1" }, }).state, ).toBe(SandboxState.Paused); }); @@ -524,7 +599,9 @@ describe("Blaxel provider orchestration", () => { async wait() { return this; }, - async delete() {}, + async delete() { + return { metadata: { name: "example" }, status: "DELETED" }; + }, }; const client = new Client({ provider: new BlaxelProvider(() => ({ @@ -561,6 +638,7 @@ describe("Blaxel provider orchestration", () => { memory: 2048, region: "us-pdx-1", }); + // Kit never forwards @blaxel/core SandboxCreateOptions (native create timeout). expect(created?.options).toBeUndefined(); expect(sandbox.id).toBe("example"); expect(sandbox.info().name).toBe("example"); @@ -584,7 +662,9 @@ describe("Blaxel provider orchestration", () => { async wait() { return this; }, - async delete() {}, + async delete() { + return { metadata: { name: "unused" }, status: "DELETED" }; + }, }; }, async delete() { @@ -600,29 +680,20 @@ describe("Blaxel provider orchestration", () => { expect(info).toMatchObject({ id: "existing", provider: "blaxel" }); }); - it("deletes by name after a provider-verified lookup", async () => { + it("unifies a successful delete as Deleted", async () => { const deleted: string[] = []; const backend = new BlaxelProvider(() => ({ configure() {}, async create() { throw new Error("unexpected create"); }, - async get(name) { - return { - metadata: { name }, - status: "RUNNING", - fs: {} as never, - process: {} as never, - async wait() { - return this; - }, - async delete() { - deleted.push(name); - }, - }; + async get() { + throw new Error("unexpected get"); }, async delete(name) { - deleted.push(`static:${name}`); + deleted.push(name); + // Wire may still say DELETING; Kit reports Deleted. + return { metadata: { name }, status: "DELETING" }; }, })).initialize({ auth: { apiKey: { key: "bl_key" } }, @@ -735,7 +806,7 @@ describe("Blaxel provider orchestration", () => { expect(lookedUp).toBe(false); }); - it("keeps short runs on waitForCompletion and polls the rest", async () => { + it("starts without waitForCompletion and waits when still running", async () => { const seen: Array<{ command: string; waitForCompletion?: boolean; @@ -746,7 +817,6 @@ describe("Blaxel provider orchestration", () => { const waits: Array<{ identifier: string; maxWait?: number; - interval?: number; signal?: AbortSignal; }> = []; const signal = new AbortController().signal; @@ -772,15 +842,6 @@ describe("Blaxel provider orchestration", () => { entry.workingDir = request.workingDir; if (request.env !== undefined) entry.env = request.env; seen.push(entry); - if (request.waitForCompletion) { - return { - exitCode: 0, - stdout: "ok", - stderr: "", - status: "completed", - pid: "short", - }; - } return { exitCode: 0, stdout: "", @@ -796,9 +857,6 @@ describe("Blaxel provider orchestration", () => { ...(options?.maxWait === undefined ? {} : { maxWait: options.maxWait }), - ...(options?.interval === undefined - ? {} - : { interval: options.interval }), ...(options?.signal === undefined ? {} : { signal: options.signal }), @@ -814,7 +872,9 @@ describe("Blaxel provider orchestration", () => { async wait() { return this; }, - async delete() {}, + async delete() { + return { metadata: { name: "unused" }, status: "DELETED" }; + }, }; }, async delete() { @@ -828,7 +888,7 @@ describe("Blaxel provider orchestration", () => { const polled = await backend.run("sandbox-1", { argv: ["echo", "hi"] }, {}); await backend.run("sandbox-1", { argv: ["echo", "hi"], timeout: 0 }, {}); - const held = await backend.run( + await backend.run( "sandbox-1", { argv: ["echo", "hi"], @@ -859,25 +919,25 @@ describe("Blaxel provider orchestration", () => { expect(polled).toMatchObject({ exitCode: 7 }); expect(polled.stdout).toEqual(new TextEncoder().encode("out")); expect(polled.stderr).toEqual(new TextEncoder().encode("err")); - expect(held).toMatchObject({ exitCode: 0 }); - expect(held.stdout).toEqual(new TextEncoder().encode("ok")); expect(seen).toEqual([ { command: "'echo' 'hi'", waitForCompletion: false, timeout: 0 }, { command: "'echo' 'hi'", waitForCompletion: false, timeout: 0 }, { command: "'echo' 'hi'", - waitForCompletion: true, + waitForCompletion: false, timeout: 5, workingDir: "/workspace", env: { MODE: "test" }, }, - { command: "'echo' 'hi'", waitForCompletion: true, timeout: 60 }, + { command: "'echo' 'hi'", waitForCompletion: false, timeout: 60 }, { command: "'echo' 'hi'", waitForCompletion: false, timeout: 61 }, ]); expect(waits).toEqual([ - { identifier: "long", maxWait: -1, interval: 1_000 }, - { identifier: "long", maxWait: -1, interval: 1_000 }, - { identifier: "long", maxWait: 61_000, interval: 1_000, signal }, + { identifier: "long", maxWait: -1 }, + { identifier: "long", maxWait: -1 }, + { identifier: "long", maxWait: 5_000 }, + { identifier: "long", maxWait: 60_000 }, + { identifier: "long", maxWait: 61_000, signal }, ]); }); @@ -905,18 +965,11 @@ describe("Blaxel provider orchestration", () => { expect(result.stderr).toEqual(new TextEncoder().encode("stopped")); }); - it("subtracts elapsed time from the process wait budget", () => { - expect(remainingWaitBudgetMs(undefined, 0, 10_000)).toBe(-1); - expect(remainingWaitBudgetMs(90, 1_000, 31_000)).toBe(60_000); - expect(remainingWaitBudgetMs(5, 1_000, 6_000)).toBe(0); - expect(remainingWaitBudgetMs(5, 1_000, 7_000)).toBeLessThan(0); - }); - - it("polls when a completion-hold start still returns running", async () => { + it("waits when start still returns running", async () => { const waits: string[] = []; const backend = runBackend({ async exec(request) { - expect(request.waitForCompletion).toBe(true); + expect(request.waitForCompletion).toBe(false); expect(request.timeout).toBe(5); return { status: "running", pid: "held", stdout: "", stderr: "" }; }, @@ -997,9 +1050,9 @@ describe("Blaxel provider orchestration", () => { }); describe("Blaxel filesystem.stat", () => { - it("returns directory metadata from ls on the path", async () => { + it("returns directory metadata from get on the path", async () => { const backend = fsBackend({ - async ls(path) { + async get(path) { expect(path).toBe("/app"); return { name: "app", @@ -1017,28 +1070,18 @@ describe("Blaxel filesystem.stat", () => { }); }); - it("reads file metadata from the parent listing when ls rejects a file path", async () => { + it("returns file metadata including size from get", async () => { const backend = fsBackend({ - async ls(path) { - if (path === "/app/readme.txt") { - throw new Error(JSON.stringify({ error: "Directory not found" })); - } - expect(path).toBe("/app"); + async get(path) { + expect(path).toBe("/app/readme.txt"); return { - name: "app", - path: "/app", - files: [ - { - name: "readme.txt", - path: "/app/readme.txt", - size: 12, - permissions: "0644", - owner: "user", - group: "user", - lastModified: "2024-01-02T03:04:05Z", - }, - ], - subdirectories: [], + name: "readme.txt", + path: "/app/readme.txt", + size: 12, + permissions: "0644", + owner: "user", + group: "user", + lastModified: "2024-01-02T03:04:05Z", }; }, }); @@ -1050,57 +1093,49 @@ describe("Blaxel filesystem.stat", () => { type: FileType.File, size: 12n, permissions: "0644", + owner: "user", + group: "user", }); }); - it("propagates non-file ls errors and reports missing files", async () => { + it("propagates get errors", async () => { const missing = fsBackend({ - async ls() { + async get() { throw new Error("boom"); }, }); await expect( missing.stat!("sandbox-1", "/missing", {}, {}), ).rejects.toMatchObject({ kind: ErrorKind.Unknown, operation: "stat" }); - - const gone = fsBackend({ - async ls(path) { - if (path === "/app/gone.txt") { - throw new Error(JSON.stringify({ error: "Directory not found" })); - } - return { - name: "app", - path: "/app", - files: [], - subdirectories: [], - }; - }, - }); - await expect( - gone.stat!("sandbox-1", "/app/gone.txt", {}, {}), - ).rejects.toMatchObject({ - kind: ErrorKind.NotFound, - operation: "stat", - field: FileFieldPath, - }); }); }); function fsBackend(fs: { - ls(path: string): Promise<{ - name: string; - path: string; - files: Array<{ - name: string; - path: string; - size: number; - permissions?: string; - owner?: string; - group?: string; - lastModified?: string; - }>; - subdirectories: Array<{ name: string; path: string }>; - }>; + get(path: string): Promise< + | { + name: string; + path: string; + files: Array<{ + name: string; + path: string; + size: number; + permissions?: string; + owner?: string; + group?: string; + lastModified?: string; + }>; + subdirectories: Array<{ name: string; path: string }>; + } + | { + name: string; + path: string; + size: number; + permissions?: string; + owner?: string; + group?: string; + lastModified?: string; + } + >; }) { return new BlaxelProvider(() => ({ configure() {}, @@ -1115,7 +1150,9 @@ function fsBackend(fs: { async wait() { return this; }, - async delete() {}, + async delete() { + return { metadata: { name: "unused" }, status: "DELETED" }; + }, }; }, async delete() { @@ -1162,7 +1199,9 @@ function runBackend(process: { async wait() { return this; }, - async delete() {}, + async delete() { + return { metadata: { name: "unused" }, status: "DELETED" }; + }, }; }, async delete() { diff --git a/sdks/typescript/providers/blaxel/src/blaxel.ts b/sdks/typescript/providers/blaxel/src/blaxel.ts index 881d6a4e..f1a95f0d 100644 --- a/sdks/typescript/providers/blaxel/src/blaxel.ts +++ b/sdks/typescript/providers/blaxel/src/blaxel.ts @@ -95,7 +95,7 @@ class BlaxelBackend extends BackendBase implements Backend { throwIfAborted(options.signal); const plan = mapCreateRequest(request, this.config); throwIfAborted(options.signal); - const sandbox = await this.native.create(plan.params, plan.options); + const sandbox = await this.native.create(plan.params); return { sandbox: mapSandboxInfo(sandbox, request) }; } catch (error) { throw mapBlaxelError(error, "create"); @@ -116,21 +116,20 @@ class BlaxelBackend extends BackendBase implements Backend { _request: DeleteOptions, options: OperationOptions, ): Promise { - return this.#withSandbox({ - operation: "delete", - id, - options, - body: async (sandbox) => { - const info = mapSandboxInfo(sandbox); - await sandbox.delete(); - return { - ...info, - state: SandboxState.Deleted, - terminal: true, - providerState: "TERMINATED", - }; - }, - }); + try { + throwIfAborted(options.signal); + const deleted = await this.native.delete(id); + throwIfAborted(options.signal); + // Blaxel DELETE may still finish asynchronously; unify as Deleted. + return { + ...mapSandboxInfo(deleted), + state: SandboxState.Deleted, + terminal: true, + providerState: "TERMINATED", + }; + } catch (error) { + throw mapBlaxelError(error, "delete"); + } } async readFile( @@ -289,7 +288,6 @@ class BlaxelBackend extends BackendBase implements Backend { options, body: async (sandbox) => { const timeout = execTimeoutSeconds(request.timeout); - const startedAt = Date.now(); const started = await sandbox.process.exec( processRequest(request, timeout), ); @@ -297,7 +295,6 @@ class BlaxelBackend extends BackendBase implements Backend { process: sandbox.process, started, timeout, - startedAt, signal: options.signal, }); }, @@ -330,11 +327,11 @@ function throwIfAborted(signal: AbortSignal | undefined): void { * Selects Blaxel as the provider for the shared Sandbox Kit client. * * Create needs `name`, an image source, and client `region`. Set - * `auth.apiKey` and `scope.workspace`, or omit them so `@blaxel/core` can use - * `BL_API_KEY` / `BL_WORKSPACE`. `memoryMib` maps 1:1 onto Blaxel's memory - * field; Kit has no portable volume fields. `@blaxel/core` stores credentials - * in process-global `settings` — use one Blaxel Kit client, or one credential - * set, per process. + * `auth.apiKey` and `scope.workspace`, or omit them to use `BL_API_KEY` / + * `BL_WORKSPACE` from the environment. Set `memoryMib` for memory size; + * Blaxel documents that value as MB and Kit does not convert units. Volumes + * are not part of create. In TypeScript, use one Blaxel client (or one API + * key) per process. */ export function blaxel(): Provider { return new BlaxelProvider(); diff --git a/sdks/typescript/providers/blaxel/src/errors.ts b/sdks/typescript/providers/blaxel/src/errors.ts index 2884b2af..f08cadcc 100644 --- a/sdks/typescript/providers/blaxel/src/errors.ts +++ b/sdks/typescript/providers/blaxel/src/errors.ts @@ -97,6 +97,8 @@ const STATUS_KINDS: Record = { 404: ErrorKind.NotFound, 410: ErrorKind.NotFound, 408: ErrorKind.Timeout, + // Bare 409 is Conflict for non-create ops. Create maps it in statusKindFor. + // Coded conflicts still win through CODE_KINDS above. 409: ErrorKind.Conflict, 412: ErrorKind.FailedPrecondition, 429: ErrorKind.RateLimited, @@ -122,7 +124,7 @@ export function mapBlaxelError( getBlaxelErrorCode(error) ?? getBlaxelErrorCode(cause), ); return create({ - kind: classifyKind({ error, cause, providerCode, statusCode }), + kind: classifyKind({ error, cause, providerCode, statusCode, operation }), error, operation, ...(statusCode === undefined ? {} : { statusCode }), @@ -135,10 +137,11 @@ interface ClassifyArgs { cause: unknown; providerCode: string | undefined; statusCode: number | undefined; + operation: string; } function classifyKind(args: ClassifyArgs): ErrorInfo["kind"] { - const { error, cause, providerCode, statusCode } = args; + const { error, cause, providerCode, statusCode, operation } = args; if (error instanceof CredentialsError) return ErrorKind.Authentication; if ( error instanceof SandboxCreationTimeoutError || @@ -159,13 +162,24 @@ function classifyKind(args: ClassifyArgs): ErrorInfo["kind"] { // Unknown codes fall through so status can still classify the failure. if (providerCode !== undefined && CODE_KINDS[providerCode] !== undefined) return CODE_KINDS[providerCode]!; - if (statusCode !== undefined && STATUS_KINDS[statusCode] !== undefined) - return STATUS_KINDS[statusCode]!; + if (statusCode !== undefined) { + const mapped = statusKindFor(statusCode, operation); + if (mapped !== undefined) return mapped; + } if (error instanceof ResponseError && error.status !== undefined) - return STATUS_KINDS[error.status] ?? ErrorKind.Unknown; + return statusKindFor(error.status, operation) ?? ErrorKind.Unknown; return ErrorKind.Unknown; } +/** Bare 409 on create → AlreadyExists (name collision); otherwise Conflict. */ +function statusKindFor( + status: number, + operation: string, +): ErrorInfo["kind"] | undefined { + if (status === 409 && operation === "create") return ErrorKind.AlreadyExists; + return STATUS_KINDS[status]; +} + function isProcessWaitTimeout(error: unknown): boolean { return ( error instanceof Error && diff --git a/sdks/typescript/providers/blaxel/src/filesystem.ts b/sdks/typescript/providers/blaxel/src/filesystem.ts index a7cc33b8..53a46728 100644 --- a/sdks/typescript/providers/blaxel/src/filesystem.ts +++ b/sdks/typescript/providers/blaxel/src/filesystem.ts @@ -1,12 +1,6 @@ -import { basename, dirname } from "node:path"; +import { basename } from "node:path"; import type { Directory, File as BlaxelFile } from "@blaxel/core"; -import { - ErrorKind, - FileFieldPath, - FileType, - sandboxError, - type FileInfo, -} from "@sandbox-kit/sandbox"; +import { FileType, type FileInfo } from "@sandbox-kit/sandbox"; import type { NativeSandboxHandle } from "./native.js"; type BlaxelSubdirectory = Directory["subdirectories"][number]; @@ -34,43 +28,34 @@ export function directoryInfo(info: BlaxelSubdirectory): FileInfo { }; } -/** Blaxel has no Stat; ls wraps GET and rejects file paths with this error. */ +/** + * Stat one path. Uses the same filesystem GET as Go: a directory listing or + * file metadata (size, permissions when Blaxel supplies them). + */ export async function blaxelStat( fs: NativeSandboxHandle["fs"], path: string, ): Promise { - try { - const listing = await fs.ls(path); + const data = await fs.get(path); + if (isDirectoryListing(data)) { return { - name: listing.name || basename(path) || "/", - path: listing.path || path, + name: data.name || basename(path) || "/", + path: data.path || path, type: FileType.Directory, size: 0n, }; - } catch (error) { - if (!isBlaxelFilePath(error)) throw error; } - const parent = dirname(path); - const name = basename(path); - const listing = await fs.ls(parent === "." ? "/" : parent); - const file = listing.files.find((entry) => entry.name === name); - if (file) return fileInfo(file); - throw sandboxError({ - kind: ErrorKind.NotFound, - provider: "blaxel", - operation: "stat", - field: FileFieldPath, - message: `sandbox-kit blaxel: path not found: ${path}`, + return fileInfo({ + name: data.name || basename(path) || path, + path: data.path || path, + size: data.size, + permissions: data.permissions ?? "", + owner: data.owner ?? "", + group: data.group ?? "", + lastModified: data.lastModified ?? "", }); } -/** Blaxel `fs.ls` throws this when GET returns a file instead of a directory. */ -function isBlaxelFilePath(error: unknown): boolean { - if (!(error instanceof Error)) return false; - try { - const body = JSON.parse(error.message) as { error?: unknown }; - return body.error === "Directory not found"; - } catch { - return false; - } +function isDirectoryListing(data: Directory | BlaxelFile): data is Directory { + return "files" in data || "subdirectories" in data; } diff --git a/sdks/typescript/providers/blaxel/src/mappings/mappings.ts b/sdks/typescript/providers/blaxel/src/mappings/mappings.ts index 5a234434..50666400 100644 --- a/sdks/typescript/providers/blaxel/src/mappings/mappings.ts +++ b/sdks/typescript/providers/blaxel/src/mappings/mappings.ts @@ -3,7 +3,6 @@ import type { Config as NativeClientConfig, Port, SandboxCreateConfiguration, - SandboxCreateOptions, SandboxLifecycle, } from "@blaxel/core"; import { @@ -51,7 +50,6 @@ import { providerError } from "#errors.js"; export interface BlaxelCreatePlan { readonly params: SandboxCreateConfiguration; - readonly options?: SandboxCreateOptions; } export function mapClientConfig(config: Config): NativeClientConfig { @@ -192,12 +190,7 @@ function mapSecurity(r: CreateOptions): void { function mapLifetime(r: CreateOptions, p: SandboxCreateConfiguration): void { const v = r.lifetime; if (!v) return; - if (hasUnsupportedLifetimeFields(v)) - unsupported( - CreateFieldLifetime, - "one or more Blaxel lifetime fields are not mapped", - ); - + // Idle first (matches Go) so idle_terminate errors win over unmapped sibling fields. if (v.maximumLifetime !== undefined) { p.ttl = durationString({ ms: v.maximumLifetime, @@ -210,6 +203,12 @@ function mapLifetime(r: CreateOptions, p: SandboxCreateConfiguration): void { const lifecycle = mapIdleTerminate(v.idleTerminate); if (lifecycle !== undefined) p.lifecycle = lifecycle; } + + if (hasUnsupportedLifetimeFields(v)) + unsupported( + CreateFieldLifetime, + "one or more Blaxel lifetime fields are not mapped", + ); } function mapIdleTerminate( diff --git a/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts b/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts index da4b9315..e4ac81c3 100644 --- a/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts +++ b/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts @@ -30,7 +30,10 @@ export interface BlaxelSandboxIdentity { }; /** Deployment status (DEPLOYED, FAILED, …). */ readonly status?: unknown; - /** Runtime state (RUNNING / STANDBY). */ + /** + * Runtime state (RUNNING / STANDBY). `@blaxel/core` types `sandbox` private + * and has no `state` getter; the native adapter projects wire state here. + */ readonly state?: unknown; readonly spec?: { readonly region?: string; @@ -92,11 +95,15 @@ export function mapCommonCreateFields( const target: Mutable = {}; if (source === undefined) return target; if (source.name !== undefined) target.name = source.name; + // Match Go: omit envs when the map is empty so explicit {} equals omit. if (source.environment !== undefined) { - target.envs = Object.entries(source.environment).map(([name, value]) => ({ - name, - value: String(value), - })); + const entries = Object.entries(source.environment); + if (entries.length > 0) { + target.envs = entries.map(([name, value]) => ({ + name, + value: String(value), + })); + } } if (source.labels !== undefined) target.labels = { ...source.labels }; return target; @@ -146,9 +153,6 @@ export function mapAllocatedResources( function runtimeState(source: BlaxelSandboxIdentity): string | undefined { if (source.state !== undefined && String(source.state) !== "") return String(source.state); - // SandboxInstance keeps the wire model on a private field; read it when present. - const nested = (source as { sandbox?: { state?: unknown } }).sandbox?.state; - if (nested !== undefined && String(nested) !== "") return String(nested); return undefined; } diff --git a/sdks/typescript/providers/blaxel/src/native.ts b/sdks/typescript/providers/blaxel/src/native.ts index fbcadfeb..f7f0f507 100644 --- a/sdks/typescript/providers/blaxel/src/native.ts +++ b/sdks/typescript/providers/blaxel/src/native.ts @@ -1,10 +1,14 @@ import { + getFilesystemByPath, + ResponseError, SandboxInstance, settings, type Config as NativeClientConfig, type Directory, + type File as BlaxelFile, type SandboxCreateConfiguration, type SandboxCreateOptions, + type SandboxFileSystem, } from "@blaxel/core"; import type { BlaxelSandboxIdentity } from "#mappings/provider_mappings.js"; @@ -31,6 +35,8 @@ export interface NativeSandboxHandle extends BlaxelSandboxIdentity { writeBinary(path: string, content: Uint8Array): Promise; mkdir(path: string, permissions?: string): Promise; ls(path: string): Promise; + /** GET path: directory listing or file metadata (size, permissions, …). */ + get(path: string): Promise; rm(path: string, recursive?: boolean): Promise; download(src: string, destinationPath: string): Promise; }; @@ -45,7 +51,7 @@ export interface NativeSandboxHandle extends BlaxelSandboxIdentity { }, ): Promise; }; - delete(): Promise; + delete(): Promise; } export interface NativeBlaxel { @@ -55,7 +61,7 @@ export interface NativeBlaxel { options?: SandboxCreateOptions, ): Promise; get(name: string): Promise; - delete(name: string): Promise; + delete(name: string): Promise; } export type NativeFactory = (config: NativeClientConfig) => NativeBlaxel; @@ -66,14 +72,88 @@ export function defaultNativeFactory(config: NativeClientConfig): NativeBlaxel { configure(next: NativeClientConfig): void { settings.setConfig(next); }, - create(params, options) { - return SandboxInstance.create(params, options); + async create(params, options) { + return projectHandle(await SandboxInstance.create(params, options)); }, - get(name) { - return SandboxInstance.get(name); + async get(name) { + return projectHandle(await SandboxInstance.get(name)); }, - delete(name) { - return SandboxInstance.delete(name); + async delete(name) { + return (await SandboxInstance.delete(name)) as BlaxelSandboxIdentity; }, }; } + +/** + * `@blaxel/core` types `SandboxInstance.sandbox` private and has no `state` + * getter. Project runtime STANDBY/RUNNING onto a Kit-facing `state` field so + * identity mapping never reaches into the private wire model. Wrap `fs` with + * `get` so Stat can use the same filesystem GET as Go. + */ +export function projectHandle(instance: SandboxInstance): NativeSandboxHandle { + const state = wireRuntimeState(instance); + const base = + state === undefined ? instance : Object.assign(instance, { state }); + return Object.assign(base, { + fs: wrapFilesystem(instance.fs), + }) as unknown as NativeSandboxHandle; +} + +function wrapFilesystem(fs: SandboxFileSystem): NativeSandboxHandle["fs"] { + return { + readBinary: (path) => fs.readBinary(path), + writeBinary: (path, content) => fs.writeBinary(path, content), + mkdir: (path, permissions) => fs.mkdir(path, permissions), + ls: (path) => fs.ls(path), + rm: (path, recursive) => fs.rm(path, recursive), + download: (src, destinationPath) => fs.download(src, destinationPath), + get: (path) => filesystemGet(fs, path), + }; +} + +async function filesystemGet( + fs: SandboxFileSystem, + path: string, +): Promise { + const { response, data, error } = await getFilesystemByPath({ + path: { path }, + baseUrl: fs.url, + client: fs.client, + }); + if (!response.ok || data == null) { + throw new ResponseError(response, data, error); + } + if (isDirectoryListing(data)) return data; + if (isFileMetadata(data)) { + const { content: _content, ...meta } = data as BlaxelFile & { + content?: string; + }; + return meta; + } + throw new Error("Unsupported filesystem path type"); +} + +function isDirectoryListing(data: unknown): data is Directory { + return ( + !!data && + typeof data === "object" && + ("files" in data || "subdirectories" in data) + ); +} + +function isFileMetadata(data: unknown): data is BlaxelFile { + return ( + !!data && + typeof data === "object" && + "size" in data && + !("files" in data) && + !("subdirectories" in data) + ); +} + +function wireRuntimeState(instance: SandboxInstance): string | undefined { + const wire = instance as unknown as { sandbox?: { state?: unknown } }; + const value = wire.sandbox?.state; + if (value === undefined || String(value) === "") return undefined; + return String(value); +} diff --git a/sdks/typescript/providers/blaxel/src/process.ts b/sdks/typescript/providers/blaxel/src/process.ts index 8f2a2a6c..c3ecb9b3 100644 --- a/sdks/typescript/providers/blaxel/src/process.ts +++ b/sdks/typescript/providers/blaxel/src/process.ts @@ -12,9 +12,6 @@ import type { ProcessExecRequest, } from "./native.js"; -/** Blaxel's documented limit for a single waitForCompletion request. */ -const blaxelCompletionHoldSeconds = 60; - const terminalProcessStatus = new Set([ "completed", "failed", @@ -22,14 +19,14 @@ const terminalProcessStatus = new Set([ "stopped", ]); +/** Starts without waitForCompletion; Kit waits via process.wait. */ export function processRequest( request: ExecOptions, timeout: number | undefined, ): ProcessExecRequest { const body: ProcessExecRequest = { command: blaxelCommand(request), - // 1–60s can use Blaxel's completion hold; omit/0/longer timeouts poll. - waitForCompletion: holdsCompletion(timeout), + waitForCompletion: false, // omit/0 → 0 disables Blaxel's default process auto-kill. timeout: timeout ?? 0, }; @@ -42,14 +39,12 @@ export function processRequest( return body; } -/** Internal wait orchestration after Process.exec. */ +/** Internal wait after Process.exec: return terminal start, else one native wait. */ export interface FinishProcessArgs { process: NativeSandboxHandle["process"]; started: BlaxelProcessResult; /** Kit process timeout in whole seconds; undefined means unlimited. */ timeout: number | undefined; - /** Date.now() taken immediately before Process.exec. */ - startedAt: number; signal: AbortSignal | undefined; } @@ -67,37 +62,16 @@ export async function finishProcess( message: "sandbox-kit blaxel: SDK returned no process identifier", }); } - const maxWait = remainingWaitBudgetMs(args.timeout, args.startedAt); - if (args.timeout !== undefined && maxWait <= 0) { - providerError({ - kind: ErrorKind.Timeout, - operation: "run", - field: ExecFieldTimeout, - message: "process did not finish before the timeout", - }); - } const waitOptions: { maxWait: number; - interval: number; signal?: AbortSignal; } = { - maxWait, - interval: 1_000, + maxWait: args.timeout === undefined ? -1 : args.timeout * 1_000, }; if (args.signal !== undefined) waitOptions.signal = args.signal; return execResult(await args.process.wait(identifier, waitOptions)); } -/** Poll window after Process.exec, subtracting time spent on completion hold. */ -export function remainingWaitBudgetMs( - timeoutSeconds: number | undefined, - startedAt: number, - now: number = Date.now(), -): number { - if (timeoutSeconds === undefined) return -1; - return timeoutSeconds * 1_000 - (now - startedAt); -} - /** * Maps Kit ExecOptions.timeout (ms) to Blaxel seconds. * Omitted and explicit zero become undefined so the caller sends 0 (no limit). @@ -121,14 +95,6 @@ function isWholeSecondTimeout(timeout: number): boolean { return Number.isFinite(timeout) && timeout >= 0 && timeout % 1_000 === 0; } -function holdsCompletion(timeoutSeconds: number | undefined): boolean { - return ( - timeoutSeconds !== undefined && - timeoutSeconds > 0 && - timeoutSeconds <= blaxelCompletionHoldSeconds - ); -} - function processFinished(status: string | undefined): boolean { return status !== undefined && terminalProcessStatus.has(status); } diff --git a/website/content/docs/blaxel.mdx b/website/content/docs/blaxel.mdx index cae3bb3e..183e2283 100644 --- a/website/content/docs/blaxel.mdx +++ b/website/content/docs/blaxel.mdx @@ -53,7 +53,7 @@ const sandbox = await client.create({ console.log(sandbox.id); // same as create name; use with get / BL_SANDBOX_NAME ``` -`timeout` is milliseconds. Prefer `await using`; otherwise call `await client.close()` when finished. +`timeout` is milliseconds. Prefer `await using`; otherwise call `await client.close()` when finished. Use one Blaxel client (or one API key) per process — a second client with different credentials replaces the first. @@ -105,20 +105,22 @@ Optional strings use `sandbox.Value`. -Omit `auth` and `scope.workspace` to let the Blaxel SDK pick credentials from its environment (`BL_API_KEY`, `BL_WORKSPACE`). `region` is still required for create. `endpoint` is rejected. Closing the client does not delete the sandbox. +Omit `auth` and `scope.workspace` to use `BL_API_KEY` and `BL_WORKSPACE` from +the environment. `region` is still required for create. `endpoint` is rejected. +Closing the client does not delete the sandbox. ## Configuration -| Setting | Notes | -| -------------- | -------------------------------------------------- | -| API key | `auth.apiKey` | -| Omitted `auth` | Blaxel SDK environment / CLI credentials | -| Workspace | `scope.workspace` — or `BL_WORKSPACE` when omitted | -| Region | `region` — required for create | -| Endpoint | Unsupported | +| Setting | Notes | +| -------------- | ------------------------------------------------------- | +| API key | `auth.apiKey` | +| Omitted `auth` | Uses `BL_API_KEY` / `BL_WORKSPACE` from the environment | +| Workspace | `scope.workspace` — or `BL_WORKSPACE` when omitted | +| Region | `region` — required for create | +| Endpoint | Unsupported | ```ts await using client = new Client({ @@ -157,28 +159,30 @@ client, err := sandbox.NewClient(blaxel.New(), config) -Token-pair, bearer, and OAuth client modes are rejected. Scope fields other than workspace are rejected. TypeScript: `@blaxel/core` stores credentials in process-global `settings` — use one Blaxel Kit client, or one credential set, per process. Full rules: [Configuration](/docs/configuration). +Token-pair, bearer, and OAuth are rejected. Only workspace scope is accepted. +In TypeScript, use one Blaxel client (or one API key) per process. Full rules: +[Configuration](/docs/configuration). ## What works -| Area | Notes | -| --------- | ------------------------------------------------------------------------------ | -| Create | Valid name + image; memory 1:1, labels, env, ports, outbound domains, TTL/idle | -| Volumes | Not in Kit create; no portable volume fields | -| Open | `get` / `refresh` by sandbox name | -| Lifecycle | `delete`, `close` — no start/stop/pause/archive/terminate/attach | -| Files | Filesystem methods — [Filesystem](/docs/filesystem) | -| Process | Blocking `run` — [Run a command](/docs/process) | - -`resources.memoryMib` / `memory_mib` maps 1:1 onto Blaxel's memory field. -Blaxel labels that integer MB; Kit does not convert. Blaxel derives CPU from -memory, so CPU and disk overrides are rejected. Default port transport leaves -the protocol to Blaxel (HTTP); plain maps to TCP; TLS maps to TLS; HTTP/2 over -TLS is rejected. Client `timeout` is the Kit create/operation deadline. -`@blaxel/core`'s native creation timeout (1–50s) is not forwarded; the Go SDK -has no equivalent. `provisioning.waitFor` accepts only `Default` and -`Scheduled`; `Ready` (and `Started` / `Submitted`) are rejected. Readiness -probes are unsupported. Explicit idle-terminate disable is rejected. +| Area | Notes | +| --------- | -------------------------------------------------------------------------------- | +| Create | Name + image; memory, labels, env, ports, outbound domains, TTL/idle | +| Volumes | Not available on create | +| Open | `get` / `refresh` by sandbox name | +| Lifecycle | `delete`, `close` — no start/stop/pause/archive/terminate/attach | +| Delete | Succeeds as `deleted` / `terminal`; wait briefly before recreating the same name | +| State | Idle → paused; active → running | +| Errors | Name taken on create → AlreadyExists; delete still in progress → Conflict | +| Files | Shared filesystem methods — [Filesystem](/docs/filesystem) | +| Process | `run` waits for the command — [Run a command](/docs/process) | + +Set `memoryMib` / `memory_mib` for memory size. Blaxel documents that value as +MB; Kit does not convert units. CPU follows memory — CPU and disk overrides are +rejected. Omit memory to use Blaxel's default. Default ports are HTTP; plain is +TCP; TLS is TLS; HTTP/2 over TLS is not supported. Your client/create `timeout` +is how long create may take. Ready wait and readiness probes are not supported. +You cannot disable idle terminate. ## Related diff --git a/website/content/docs/configuration.mdx b/website/content/docs/configuration.mdx index b4e96410..8abe69df 100644 --- a/website/content/docs/configuration.mdx +++ b/website/content/docs/configuration.mdx @@ -342,13 +342,13 @@ client, err := sandbox.NewClient(modal.New(), config) | Setting | What to pass | | -------------- | ---------------------------------------- | | API key | `auth.apiKey.key` | -| Omitted `auth` | Blaxel SDK environment / CLI credentials | +| Omitted `auth` | Uses `BL_API_KEY` / `BL_WORKSPACE` from the environment | | Workspace | `scope.workspace` | | Region | `region` — required for create | | Endpoint | Unsupported | -`@blaxel/core` stores credentials in process-global `settings`. Use one Blaxel -Kit client, or one credential set, per process. +In TypeScript, use one Blaxel client (or one API key) per process. A second +client with different credentials replaces the first. ```ts await using client = new Client({ @@ -531,11 +531,9 @@ instance, err := client.Create(ctx, &sandbox.CreateOptions{ - Blaxel uses the Kit deadline for create. `@blaxel/core` also accepts a native - creation timeout (1–50 whole seconds); this provider does not forward it. The - Go Blaxel SDK has no equivalent argument. Ready wait and readiness probes are - unsupported. TypeScript Blaxel clients share `@blaxel/core` process-global - `settings`: use one Blaxel Kit client, or one credential set, per process. + Your client/create `timeout` is how long create may take. Ready wait and + readiness probes are unsupported. In TypeScript, use one Blaxel client (or one + API key) per process. ## Credentials diff --git a/website/content/docs/create.mdx b/website/content/docs/create.mdx index 0adb21c0..8483ee92 100644 --- a/website/content/docs/create.mdx +++ b/website/content/docs/create.mdx @@ -273,7 +273,7 @@ instance, err := client.Create(ctx, &sandbox.CreateOptions{ | --------- | ------------------------------------------------------------------------------------------------- | | Source | Registry image and Blaxel-valid `name` required (lowercase alphanumeric with single hyphens, ≤49) | | CPU | Derived by Blaxel from memory; Kit rejects CPU overrides | -| Memory | `memoryMib` / `memory_mib` passed 1:1; Blaxel labels the unit MB | +| Memory | `memoryMib` / `memory_mib` (Blaxel documents MB; Kit does not convert) | | Disk | Cannot be set | | Lifetime | Maximum lifetime (≥5m) and idle terminate (≥1m); explicit idle disable rejected | | Placement | Client `region` required. A placement block on this request is rejected | @@ -612,13 +612,13 @@ See [Examples](/docs/examples). Create needs a Blaxel-valid `name` (lowercase alphanumeric with single hyphens, at most 49 characters), an image, and client `region`. The returned id is that -name. Memory maps 1:1; CPU, disk, placement, readiness, runtime, and volumes -are rejected. Ports 80, 443, and 8080 are reserved. Default port transport -leaves the protocol to Blaxel (HTTP); plain maps to TCP; TLS maps to TLS; -HTTP/2 over TLS is rejected. Max-age TTL ≥5m; idle terminate ≥1m; explicit -idle disable is rejected. `waitFor` only `Default` / `Scheduled`; `Ready` is -rejected. Kit create timeout is the local deadline; Blaxel's native creation -timeout is not forwarded. See [Examples](/docs/examples). +name. Pass memory as `memoryMib` / `memory_mib`; CPU, disk, placement, +readiness, runtime, and volumes are rejected. Ports 80, 443, and 8080 are +reserved. Default ports are HTTP; plain is TCP; TLS is TLS; HTTP/2 over TLS is +not supported. Max-age TTL ≥5m; idle terminate ≥1m; you cannot disable idle +terminate. `waitFor` only `Default` / `Scheduled`; Ready wait is not +supported. Your client/create `timeout` is how long create may take. See +[Examples](/docs/examples). diff --git a/website/content/docs/errors.mdx b/website/content/docs/errors.mdx index 242c019d..dccfaf44 100644 --- a/website/content/docs/errors.mdx +++ b/website/content/docs/errors.mdx @@ -300,7 +300,9 @@ See [Examples](/docs/examples). -`get` before retry so you know whether the sandbox exists (lookup by name). +If the name is already taken on create, you get `AlreadyExists`. If a delete is +still finishing, you get `Conflict` — wait, then `get` before you recreate that +name. @@ -309,9 +311,11 @@ See [Examples](/docs/examples). try { await client.create(request); } catch (error) { - if (knownName) { - const sandbox = await client.get(knownName); - console.log(sandbox.info()); + if (!(error instanceof SandboxError)) throw error; + if (error.kind === ErrorKind.AlreadyExists) { + console.error("Name already taken:", error.message); + } else if (error.kind === ErrorKind.Conflict) { + console.error("Name still conflicting; wait and get before recreate."); } throw error; } @@ -323,10 +327,13 @@ try { ```go instance, err := client.Create(ctx, opts) if err != nil { - if knownName != "" { - existing, getErr := client.Get(ctx, knownName) - if getErr == nil { - fmt.Println(existing.Info()) + var detail *sandbox.Error + if errors.As(err, &detail) { + switch detail.Kind { + case sandbox.ErrorKindAlreadyExists: + fmt.Println("Name already taken:", detail.Message) + case sandbox.ErrorKindConflict: + fmt.Println("Name still conflicting; wait and Get before recreate.") } } return err diff --git a/website/content/docs/filesystem.mdx b/website/content/docs/filesystem.mdx index f3ff5d19..ac48ff06 100644 --- a/website/content/docs/filesystem.mdx +++ b/website/content/docs/filesystem.mdx @@ -139,7 +139,6 @@ data, err := instance.Filesystem().ReadFile(ctx, "/tmp/note.txt", &sandbox.FileR - ## Read a file Returns the bytes of one file. A missing file is not found. A directory path fails at the provider. @@ -347,6 +346,11 @@ fmt.Println(entries) A symlink result describes the symlink when Modal reports one. + + Directories return type directory. Files return type file, with size and + permission fields when Blaxel provides them. + + diff --git a/website/content/docs/lifecycle.mdx b/website/content/docs/lifecycle.mdx index efe98c57..1a6ed956 100644 --- a/website/content/docs/lifecycle.mdx +++ b/website/content/docs/lifecycle.mdx @@ -127,7 +127,7 @@ if err != nil { -`get` loads a sandbox that already exists by **name** (Blaxel id is the name). `refresh` loads it again. `attach` is not available. +`get` loads a sandbox that already exists by **name** (Blaxel id is the name). `refresh` loads it again. `attach` is not available. After `delete` succeeds you see `deleted` / `terminal: true` — wait before recreating the same name, or you may get `Conflict`. An idle sandbox shows as paused; an active one shows as running. @@ -268,7 +268,7 @@ if err := instance.Delete(ctx, &sandbox.DeleteOptions{ ## Delete -`delete` removes a Blaxel sandbox. +`delete` removes a Blaxel sandbox. When the call succeeds you get `deleted` with `terminal` set. Wait before recreating the same name. @@ -413,8 +413,9 @@ state. See [Errors](/docs/errors). - After a lifecycle call, call `refresh` (or `get` again) before assuming the - sandbox is gone. + After a successful `delete`, trust `info.state` / `terminal` on the handle. + Use `refresh` or `get` when a call failed or you need fresh metadata while + the sandbox still exists. Prefer portable `state` over `providerState`. diff --git a/website/content/docs/process.mdx b/website/content/docs/process.mdx index 7f99c4fa..bc09043b 100644 --- a/website/content/docs/process.mdx +++ b/website/content/docs/process.mdx @@ -78,7 +78,7 @@ fmt.Println(result) | You pass | On Daytona | | ---------------------- | --------------------------------------------------------------- | -| `argv` | POSIX-quoted shell string | +| `argv` | One quoted shell command string | | `shell` | shell string as-is | | omit / zero timeout | no command limit (`0`, so Daytona does not use its 10s default) | | fractional (`1500` ms) | rejected (`timeout`) | @@ -90,7 +90,7 @@ Daytona returns stdout only; `stderr` is empty. stdout may include merged stream ```ts const result = await sandbox.process.run({ - argv: ["echo", "hello"], // sent as a POSIX-quoted shell string + argv: ["echo", "hello"], // sent as one quoted shell command string }); // result: { exitCode, stdout, stderr } — stderr often empty on Daytona console.log(result); @@ -101,7 +101,7 @@ console.log(result); ```go result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ - Argv: []string{"echo", "hello"}, // sent as a POSIX-quoted shell string + Argv: []string{"echo", "hello"}, // sent as one quoted shell command string }) if err != nil { return err @@ -155,25 +155,24 @@ fmt.Println(result) -| You pass | On Blaxel | -| ---------------------- | ------------------------------------------------------------------------------ | -| `argv` | POSIX-quoted shell string | -| `shell` | shell string as-is | -| omit / zero timeout | no process auto-kill; Kit polls until the command ends or the call is canceled | -| 1–60 seconds | process auto-kill; blocking completion request | -| longer than 60 seconds | process auto-kill; Kit polls until the command ends or the call is canceled | -| fractional (`1500` ms) | rejected (`timeout`) | +| You pass | What happens | +| ---------------------- | ----------------------------------------------------------------- | +| `argv` | Sent as one quoted shell command string | +| `shell` | Run as-is | +| omit / zero timeout | Wait until the command ends (or you cancel); no Blaxel time limit | +| positive timeout | Cap how long the command may run (whole seconds only) | +| fractional (`1500` ms) | Rejected | -Blaxel returns both `stdout` and `stderr`. `run` waits until the command ends. -Canceling the context or aborting the signal stops the wait only; the remote -command may keep running. An HTTP 504 while waiting is a timeout error. +You get both `stdout` and `stderr`. `run` waits until the command ends. +Canceling stops waiting only — the remote command may keep running. If the wait +times out, you get a timeout error. ```ts const result = await sandbox.process.run({ - argv: ["echo", "hello"], // sent as a POSIX-quoted shell string + argv: ["echo", "hello"], // sent as one quoted shell command string }); // result: { exitCode, stdout, stderr } console.log(result); @@ -184,7 +183,7 @@ console.log(result); ```go result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ - Argv: []string{"echo", "hello"}, // sent as a POSIX-quoted shell string + Argv: []string{"echo", "hello"}, // sent as one quoted shell command string }) // result: ExitCode, Stdout, Stderr fmt.Println(result) @@ -205,7 +204,7 @@ A non-zero exit code is **not** an SDK error. | `0` | No extra client deadline for this call | | Positive value | Cap this call; must be whole seconds (`5000` ok, `1500` rejected) | -Omit or zero also means no separate provider process auto-kill (see the +Omit or zero also means no separate provider process time limit (see the provider tables above). diff --git a/website/content/docs/sdk/configuration.mdx b/website/content/docs/sdk/configuration.mdx index 318813ca..667b92b4 100644 --- a/website/content/docs/sdk/configuration.mdx +++ b/website/content/docs/sdk/configuration.mdx @@ -48,10 +48,9 @@ await using client = new Client({ - Blaxel uses the Kit deadline for create. `@blaxel/core`'s native creation - timeout (1–50s) is not forwarded. `region` is required for create. `endpoint` - is unsupported. TypeScript credentials go through process-global - `@blaxel/core` `settings`. + Your client/create `timeout` is how long create may take. `region` is required + for create. `endpoint` is unsupported. Use one Blaxel client (or one API key) + per process. @@ -95,9 +94,8 @@ client, err := sandbox.NewClient(provider, &sandbox.Config{ - Blaxel uses the Kit deadline for create. The Go Blaxel SDK has no separate - creation-timeout argument. `Region` is required for create. `Endpoint` is - unsupported. + Your client/create `Timeout` is how long create may take. `Region` is required + for create. `Endpoint` is unsupported. diff --git a/website/content/docs/sdk/create-options.mdx b/website/content/docs/sdk/create-options.mdx index 22f8d26c..65d5fec1 100644 --- a/website/content/docs/sdk/create-options.mdx +++ b/website/content/docs/sdk/create-options.mdx @@ -222,10 +222,9 @@ Set exactly one variant. Unsupported variants fail before the provider call. Blaxel requires an `image` source and a Blaxel-valid `name` (lowercase alphanumeric with single hyphens, at most 49 characters). Client `region` is - required. Default port transport leaves the protocol to Blaxel (HTTP); plain - maps to TCP; TLS maps to TLS; HTTP/2 over TLS is rejected. Ports 80, 443, and - 8080 are reserved. Kit create timeout is the local deadline; `@blaxel/core`'s - native creation timeout is not forwarded. + required. Default ports are HTTP; plain is TCP; TLS is TLS; HTTP/2 over TLS is + not supported. Ports 80, 443, and 8080 are reserved. Your client/create + `timeout` is how long create may take. ## RuntimeConfig @@ -424,10 +423,8 @@ Set exactly one variant. Unsupported variants fail before the provider call. - Kit `timeout` / client timeout is the create deadline. `queueTimeout` is - unsupported. `waitFor` accepts only `Default` and `Scheduled`; `Ready`, - `Started`, and `Submitted` are rejected. `@blaxel/core`'s native creation - timeout (1–50s) is not forwarded. + Client `timeout` is how long create may take. `queueTimeout` is unsupported. + `waitFor` accepts only `Default` and `Scheduled`; Ready wait is not supported. @@ -451,10 +448,8 @@ Set exactly one variant. Unsupported variants fail before the provider call. - Kit `Timeout` / client timeout is the create deadline. `QueueTimeout` is - unsupported. `WaitFor` accepts only `Default` and `Scheduled`; `Ready`, - `Started`, and `Submitted` are rejected. The Go Blaxel SDK has no separate - creation-timeout argument. + Client `Timeout` is how long create may take. `QueueTimeout` is unsupported. + `WaitFor` accepts only `Default` and `Scheduled`; Ready wait is not supported. diff --git a/website/content/docs/sdk/enums.mdx b/website/content/docs/sdk/enums.mdx index 962459bb..d73b63ac 100644 --- a/website/content/docs/sdk/enums.mdx +++ b/website/content/docs/sdk/enums.mdx @@ -93,8 +93,7 @@ Transport for a published port. Modal requires an explicit transport (`Default` is rejected). - Blaxel: `Default` omits protocol (Blaxel defaults to HTTP). `Plain` → TCP, - `Tls` → TLS. `Http2Tls` is rejected. + `Default` is HTTP. `Plain` is TCP. `Tls` is TLS. `Http2Tls` is not supported. ## IsolationKind diff --git a/website/content/docs/sdk/filesystem.mdx b/website/content/docs/sdk/filesystem.mdx index cf6d66f5..4a44f4a8 100644 --- a/website/content/docs/sdk/filesystem.mdx +++ b/website/content/docs/sdk/filesystem.mdx @@ -374,6 +374,11 @@ const info = await sandbox.filesystem.stat("/tmp/note.txt"); console.log(info); ``` + + Directories return type directory. Files return type file, with size and + permission fields when Blaxel provides them. + + @@ -395,6 +400,11 @@ info, err := instance.Filesystem().Stat(ctx, "/tmp/note.txt", nil) fmt.Println(info) ``` + + Directories return type directory. Files return type file, with size and mode + when Blaxel provides them. + + diff --git a/website/content/docs/sdk/process.mdx b/website/content/docs/sdk/process.mdx index 43d03308..02fc27bb 100644 --- a/website/content/docs/sdk/process.mdx +++ b/website/content/docs/sdk/process.mdx @@ -113,7 +113,7 @@ Outcome of a finished command. Daytona returns stdout only; `stderr` is empty (stdout may include merged - streams). `argv` is sent as a POSIX-quoted shell string. Omit/zero timeout + streams). `argv` is sent as one quoted shell command string. Omit/zero timeout sends `0` so Daytona does not apply its 10s command default. @@ -124,13 +124,11 @@ Outcome of a finished command. - Blaxel returns both `stdout` and `stderr`. `argv` is sent as a POSIX-quoted - shell string. Omit or zero timeout means no process auto-kill; Kit polls until - the command ends or the call is canceled. A timeout of at most 60 seconds uses - Blaxel's blocking completion request. A longer timeout sets the process - auto-kill and polls. `run` waits until the command ends. Aborting the signal - stops the wait only; the remote command may keep running. An HTTP 504 while - waiting is a timeout error. + You get both `stdout` and `stderr`. `argv` is one quoted shell command string. + Omit or zero timeout waits until the command ends (or you abort), with no + Blaxel time limit. A positive timeout must be whole seconds and caps how long + the command may run. Aborting stops waiting only; the remote command may keep + running. If the wait times out, you get a timeout error. @@ -144,7 +142,7 @@ Outcome of a finished command. Daytona returns Stdout only; Stderr is empty (Stdout may include merged - streams). Argv is sent as a POSIX-quoted shell string. Omit/zero Timeout sends + streams). Argv is sent as one quoted shell command string. Omit/zero Timeout sends 0 so Daytona does not apply its 10s command default. @@ -155,13 +153,11 @@ Outcome of a finished command. - Blaxel returns both Stdout and Stderr. Argv is sent as a POSIX-quoted shell - string. Omit or zero Timeout means no process auto-kill; Kit polls until the - command ends or the call is canceled. A Timeout of at most 60 seconds uses - Blaxel's blocking completion request. A longer Timeout sets the process - auto-kill and polls. Run waits until the command ends. Canceling the context - stops the wait only; the remote command may keep running. An HTTP 504 while - waiting is a timeout error. + You get both Stdout and Stderr. Argv is one quoted shell command string. Omit or + zero Timeout waits until the command ends (or you cancel), with no Blaxel time + limit. A positive Timeout must be whole seconds and caps how long the command + may run. Canceling stops waiting only; the remote command may keep running. An + If the wait times out, you get a timeout error. diff --git a/website/content/docs/sdk/sandbox-info.mdx b/website/content/docs/sdk/sandbox-info.mdx index 47fbe0de..979fbc12 100644 --- a/website/content/docs/sdk/sandbox-info.mdx +++ b/website/content/docs/sdk/sandbox-info.mdx @@ -24,7 +24,7 @@ Returned by info and many lifecycle methods. Values are a **copy**; mutating the | `id` | Sandbox identifier | | `provider` | Provider identifier | | `name` | Display name when reported | -| `providerState` | Raw state string from the cloud | +| `providerState` | Provider-specific status string; prefer portable `state` | | `region` | Region when reported | | `isolation` | Reported isolation config | | `resources` | Reported size (`cpuCores`, `memoryMib`, …) | @@ -51,7 +51,7 @@ console.log(info); | `Id` | Sandbox identifier | | `Provider` | Provider identifier | | `Name` | Display name when reported | -| `ProviderState` | Raw state string from the cloud | +| `ProviderState` | Provider-specific status string; prefer portable `State` | | `Region` | Region when reported | | `Isolation` | Reported isolation config | | `Resources` | Reported size (`CpuCores`, `MemoryMib`, …) |