From 56349c67de5df9a9a77f127c613b27d811e770c1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Nov 2026 13:37:56 +0000 Subject: [PATCH] Align Blaxel wiring with sibling providers and Blaxel floors. Fix reserved ports in examples, add Go TTL/memory floors to match TS, mark delete as Deleted/terminal in TS like Go, reject idle-disable consistently, and correct region/scope docs without inventing Go create timeout or private-field standby mapping. Co-authored-by: Nabhag Motivaras --- docs/providers.md | 5 ++-- .../create-blaxel-with-ports/README.md | 1 + .../create-blaxel-with-ports/main.go | 4 +-- .../create-blaxel-with-ports/README.md | 1 + .../create-blaxel-with-ports/src/main.ts | 4 +-- sdks/go/providers/blaxel/README.md | 8 +++-- sdks/go/providers/blaxel/creation.go | 4 +-- sdks/go/providers/blaxel/provider_checks.go | 3 ++ sdks/go/providers/blaxel/provider_mappings.go | 6 +++- sdks/go/providers/blaxel/rejection_test.go | 29 +++++++++++++++++++ sdks/go/providers/blaxel/sandbox_test.go | 27 +++++++++-------- sdks/typescript/providers/blaxel/README.md | 6 ++-- .../providers/blaxel/src/blaxel.test.ts | 28 +++++++++++++++++- .../typescript/providers/blaxel/src/blaxel.ts | 8 ++++- .../providers/blaxel/src/mappings/mappings.ts | 6 +++- .../blaxel/src/mappings/provider_mappings.ts | 3 +- specs/providers/blaxel.yaml | 1 + website/content/docs/configuration.mdx | 3 +- 18 files changed, 115 insertions(+), 32 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 78ab5ae2..96ef1be6 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -66,8 +66,9 @@ supported call for a particular sandbox, and Kit returns that error. Set Daytona's target with `region` on the client. A placement block on the create request is rejected. Modal accepts cloud and region on the create -request, and rejects a placement target. Blaxel requires `region` on the -client for create and rejects per-request placement. +request, and rejects a placement target. Blaxel create needs a region via +`Config.region` or `BL_REGION`; Go requires `Config.Region`, and TypeScript +accepts either. Per-request placement is rejected. Closing the client closes your connection to the provider. The sandboxes keep running. Closing a handle leaves the sandbox as it is. You can keep using the 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 a1c6ab13..d86058c1 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-ports/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-with-ports/README.md @@ -10,6 +10,7 @@ Maps `network.ports` to Blaxel runtime ports. Default/plain → TCP, TLS → TLS - Other network settings (egress mode, CIDRs, private network, proxies) are rejected. - Port numbers must be in range. +- Blaxel reserves ports 80, 443, and 8080; this example uses 3000 and 3001. ## Run diff --git a/examples/go/blaxel/configuration/create-blaxel-with-ports/main.go b/examples/go/blaxel/configuration/create-blaxel-with-ports/main.go index f6bd4983..d0dbd73c 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-ports/main.go +++ b/examples/go/blaxel/configuration/create-blaxel-with-ports/main.go @@ -44,8 +44,8 @@ func main() { Source: &sandbox.SandboxSource{Kind: &sandbox.Image{Image: &sandbox.ImageSource{Reference: "blaxel/base-image:latest"}}}, Network: &sandbox.NetworkConfig{ Ports: []*sandbox.PortBinding{ - {Port: 443, Transport: sandbox.PortTransportTLS}, - {Port: 8080, Transport: sandbox.PortTransportPlain}, + {Port: 3000, Transport: sandbox.PortTransportTLS}, + {Port: 3001, Transport: sandbox.PortTransportPlain}, }, }, }) 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 f6347a84..96293ab9 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-ports/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-ports/README.md @@ -10,6 +10,7 @@ Maps `network.ports` to Blaxel runtime ports. Default/plain → TCP, TLS → TLS - Other network settings (egress mode, CIDRs, private network, proxies) are rejected. - Port numbers must be in range. +- Blaxel reserves ports 80, 443, and 8080; this example uses 3000 and 3001. ## Run diff --git a/examples/typescript/blaxel/configuration/create-blaxel-with-ports/src/main.ts b/examples/typescript/blaxel/configuration/create-blaxel-with-ports/src/main.ts index 53cc6c90..dfcca774 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-ports/src/main.ts +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-ports/src/main.ts @@ -16,8 +16,8 @@ const sandbox = await client.create({ source: { image: { reference: "blaxel/base-image:latest" } }, network: { ports: [ - { port: 443, transport: PortTransport.Tls }, - { port: 8080, transport: PortTransport.Plain }, + { port: 3000, transport: PortTransport.Tls }, + { port: 3001, transport: PortTransport.Plain }, ], }, }); diff --git a/sdks/go/providers/blaxel/README.md b/sdks/go/providers/blaxel/README.md index 9912a5db..14291e63 100644 --- a/sdks/go/providers/blaxel/README.md +++ b/sdks/go/providers/blaxel/README.md @@ -62,7 +62,9 @@ rejected. `Scope` fields other than `Workspace` are rejected. Create from a registry image with an explicit name. You can set labels, environment variables, memory, ports, outbound domains, maximum lifetime, and -idle terminate. +idle terminate. Max-age TTL must be at least 5m; idle TTL must be at least 1m. +Create provisioning timeout is not mapped (the Go Blaxel SDK has no create +timeout option). On the handle: `Get`, `Refresh`, `Delete`, and `Close`. `Get` and `Refresh` look up the sandbox by name. `Attach`, `Start`, `Stop`, `Pause`, `Archive`, and @@ -83,8 +85,8 @@ exit code is success for the SDK—read `GetExitCode`. Blaxel returns stdout and stderr. Omit or zero `Timeout` for no Blaxel command limit. Command timeouts must be whole seconds. Cancel the context to stop waiting. -Memory is allocated in MiB. Blaxel derives CPU from memory, so `cpu_cores` and -disk MiB overrides are rejected. +Memory is allocated in MiB and must be at least 256 when set. Blaxel derives +CPU from memory, so `cpu_cores` and disk MiB overrides are rejected. ## License diff --git a/sdks/go/providers/blaxel/creation.go b/sdks/go/providers/blaxel/creation.go index a37db775..75a9a4c8 100644 --- a/sdks/go/providers/blaxel/creation.go +++ b/sdks/go/providers/blaxel/creation.go @@ -93,7 +93,7 @@ func (c *creation) lifetime() error { } config := cloneMessage(c.request.Lifetime) if config.MaximumLifetime != nil { - formatted, err := formatBlaxelDuration(config.MaximumLifetime.AsDuration()) + formatted, err := formatBlaxelDuration(config.MaximumLifetime.AsDuration(), 5*time.Minute) if err != nil { return providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldLifetimeMaximumLifetime, err.Error()) } @@ -136,7 +136,7 @@ func mapIdleTerminate(action *sandbox.AutomaticAction) (*blaxel.ExpirationPolicy if action.After == nil { return nil, providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldLifetimeIdleTerminate, "duration is required") } - formatted, err := formatBlaxelDuration(action.After.AsDuration()) + formatted, err := formatBlaxelDuration(action.After.AsDuration(), time.Minute) if err != nil { return nil, providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldLifetimeIdleTerminate, err.Error()) } diff --git a/sdks/go/providers/blaxel/provider_checks.go b/sdks/go/providers/blaxel/provider_checks.go index 5c52e0ac..b5bddfc0 100644 --- a/sdks/go/providers/blaxel/provider_checks.go +++ b/sdks/go/providers/blaxel/provider_checks.go @@ -32,6 +32,9 @@ func validateProviderCreate(request *sandbox.CreateOptions) error { if resources.CpuLimitCores != nil { return providerCheckError(sandbox.ErrorKindUnsupported, sandbox.CreateFieldResourcesCpuLimitCores, "Blaxel resource limits are not mapped") } + if resources.MemoryMib != nil && *resources.MemoryMib < 256 { + return providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldResourcesMemoryMib, "resources.memory_mib is outside supported values") + } if resources.MemoryLimitMib != nil { return providerCheckError(sandbox.ErrorKindUnsupported, sandbox.CreateFieldResourcesMemoryLimitMib, "Blaxel resource limits are not mapped") } diff --git a/sdks/go/providers/blaxel/provider_mappings.go b/sdks/go/providers/blaxel/provider_mappings.go index c22d0095..7e3eb7da 100644 --- a/sdks/go/providers/blaxel/provider_mappings.go +++ b/sdks/go/providers/blaxel/provider_mappings.go @@ -102,10 +102,14 @@ func mapAllocatedResources(source *blaxel.Sandbox) (*sandbox.Resources, error) { } // formatBlaxelDuration formats a whole-second duration as a Blaxel TTL string. -func formatBlaxelDuration(value time.Duration) (string, error) { +// minimum is the Blaxel floor for that policy (max-age TTL: 5m, idle TTL: 1m). +func formatBlaxelDuration(value, minimum time.Duration) (string, error) { if value <= 0 || value%time.Second != 0 { return "", fmt.Errorf("duration must be a positive whole number of seconds") } + if value < minimum { + return "", fmt.Errorf("duration must be at least %s", minimum) + } seconds := int64(value / time.Second) switch { case seconds%86400 == 0: diff --git a/sdks/go/providers/blaxel/rejection_test.go b/sdks/go/providers/blaxel/rejection_test.go index 37978a9c..5ceace45 100644 --- a/sdks/go/providers/blaxel/rejection_test.go +++ b/sdks/go/providers/blaxel/rejection_test.go @@ -92,6 +92,35 @@ func TestRejectedCreateSettings(t *testing.T) { kind: sandbox.ErrorKindInvalidArgument, field: sandbox.CreateFieldLifetimeMaximumLifetime, }, + { + name: "maximum lifetime below floor", + request: namedImage(&sandbox.CreateOptions{ + Lifetime: &sandbox.LifetimePolicy{MaximumLifetime: sandbox.Duration(4 * time.Minute)}, + }), + kind: sandbox.ErrorKindInvalidArgument, + field: sandbox.CreateFieldLifetimeMaximumLifetime, + }, + { + name: "idle terminate below floor", + request: namedImage(&sandbox.CreateOptions{ + Lifetime: &sandbox.LifetimePolicy{ + IdleTerminate: &sandbox.AutomaticAction{ + Mode: sandbox.PolicyModeAfter, + After: sandbox.Duration(30 * time.Second), + }, + }, + }), + kind: sandbox.ErrorKindInvalidArgument, + field: sandbox.CreateFieldLifetimeIdleTerminate, + }, + { + name: "memory below floor", + request: namedImage(&sandbox.CreateOptions{ + Resources: &sandbox.Resources{MemoryMib: sandbox.Value(uint64(255))}, + }), + kind: sandbox.ErrorKindInvalidArgument, + field: sandbox.CreateFieldResourcesMemoryMib, + }, } { t.Run(tc.name, func(t *testing.T) { _, err := planCreate(tc.request, "us-pdx-1") diff --git a/sdks/go/providers/blaxel/sandbox_test.go b/sdks/go/providers/blaxel/sandbox_test.go index a381c6a9..a7939fa2 100644 --- a/sdks/go/providers/blaxel/sandbox_test.go +++ b/sdks/go/providers/blaxel/sandbox_test.go @@ -87,26 +87,29 @@ func TestMapSandboxInfoUsesNameAsID(t *testing.T) { func TestFormatBlaxelDuration(t *testing.T) { for _, tc := range []struct { - in time.Duration - want string - ok bool + in time.Duration + minimum time.Duration + want string + ok bool }{ - {30 * time.Second, "30s", true}, - {5 * time.Minute, "5m", true}, - {2 * time.Hour, "2h", true}, - {48 * time.Hour, "2d", true}, - {1500 * time.Millisecond, "", false}, - {0, "", false}, + {30 * time.Second, time.Second, "30s", true}, + {5 * time.Minute, 5 * time.Minute, "5m", true}, + {2 * time.Hour, 5 * time.Minute, "2h", true}, + {48 * time.Hour, 5 * time.Minute, "2d", true}, + {30 * time.Second, time.Minute, "", false}, + {4 * time.Minute, 5 * time.Minute, "", false}, + {1500 * time.Millisecond, time.Second, "", false}, + {0, time.Second, "", false}, } { - got, err := formatBlaxelDuration(tc.in) + got, err := formatBlaxelDuration(tc.in, tc.minimum) if tc.ok { if err != nil || got != tc.want { - t.Fatalf("%v: got %q err %v", tc.in, got, err) + t.Fatalf("%v (min %v): got %q err %v", tc.in, tc.minimum, got, err) } continue } if err == nil { - t.Fatalf("%v: expected error", tc.in) + t.Fatalf("%v (min %v): expected error", tc.in, tc.minimum) } } } diff --git a/sdks/typescript/providers/blaxel/README.md b/sdks/typescript/providers/blaxel/README.md index d6a33cb9..b8972df4 100644 --- a/sdks/typescript/providers/blaxel/README.md +++ b/sdks/typescript/providers/blaxel/README.md @@ -54,9 +54,9 @@ than `workspace` are rejected. **Create** requires a nonblank `name` and an image source. Optional memory (MiB; Blaxel CPU follows memory), labels, environment variables, ports, outbound -domains, max-age TTL, idle-delete policy, and a create timeout (1–50 whole -seconds). Creation calls native `SandboxInstance.create`, not -`createIfNotExists`. +domains, max-age TTL (at least 5m), idle-delete policy (at least 1m; explicit +disable is rejected), and a create timeout (1–50 whole seconds). Creation +calls native `SandboxInstance.create`, not `createIfNotExists`. **Lifecycle** on the handle: `get` / `refresh` (lookup by name), `delete`, and `close`. `close` does not delete the sandbox. Start, stop, pause, archive, diff --git a/sdks/typescript/providers/blaxel/src/blaxel.test.ts b/sdks/typescript/providers/blaxel/src/blaxel.test.ts index d2a3e1a4..174543c3 100644 --- a/sdks/typescript/providers/blaxel/src/blaxel.test.ts +++ b/sdks/typescript/providers/blaxel/src/blaxel.test.ts @@ -6,10 +6,12 @@ import { ErrorKind, PolicyMode, PortTransport, + SandboxState, WaitCondition, ConfigFieldAuthApiKeyKey, ConfigFieldEndpoint, CreateFieldEnvironment, + CreateFieldLifetimeIdleTerminate, CreateFieldLifetimeMaximumLifetime, CreateFieldName, CreateFieldResourcesMemoryMib, @@ -181,6 +183,24 @@ describe("Blaxel mappings", () => { ); }); + it("rejects disabling idle terminate", () => { + expect(() => + mapCreateRequest( + { + name: "example", + source: { image: { reference: "blaxel/base-image:latest" } }, + lifetime: { idleTerminate: { mode: PolicyMode.Disabled } }, + }, + { scope: { workspace: "ws" } }, + ), + ).toThrowError( + expect.objectContaining({ + kind: ErrorKind.Unsupported, + field: CreateFieldLifetimeIdleTerminate, + }), + ); + }); + it("classifies native ResponseError status codes", () => { const native = new ResponseError( new Response(null, { status: 404, statusText: "Not Found" }), @@ -333,8 +353,14 @@ describe("Blaxel provider orchestration", () => { scope: { workspace: "ws" }, }); if (backend.delete === undefined) throw new Error("delete is missing"); - await backend.delete("doomed", {}, {}); + const info = await backend.delete("doomed", {}, {}); expect(deleted).toEqual(["doomed"]); + expect(info).toMatchObject({ + id: "doomed", + state: SandboxState.Deleted, + terminal: true, + providerState: "TERMINATED", + }); }); it("does not create a sandbox after the signal aborts during mapping", async () => { diff --git a/sdks/typescript/providers/blaxel/src/blaxel.ts b/sdks/typescript/providers/blaxel/src/blaxel.ts index bb878a03..2f4bad22 100644 --- a/sdks/typescript/providers/blaxel/src/blaxel.ts +++ b/sdks/typescript/providers/blaxel/src/blaxel.ts @@ -15,6 +15,7 @@ import { ExecFieldTimeout, FileFieldParents, FileType, + SandboxState, type FileInfo, type OperationOptions, type Provider, @@ -190,7 +191,12 @@ class BlaxelBackend extends BackendBase implements Backend { throwIfAborted(options.signal); const info = mapSandboxInfo(sandbox); await sandbox.delete(); - return info; + return { + ...info, + state: SandboxState.Deleted, + terminal: true, + providerState: "TERMINATED", + }; } catch (error) { throw mapBlaxelError(error, "delete"); } diff --git a/sdks/typescript/providers/blaxel/src/mappings/mappings.ts b/sdks/typescript/providers/blaxel/src/mappings/mappings.ts index 7b3e4db4..69d410ea 100644 --- a/sdks/typescript/providers/blaxel/src/mappings/mappings.ts +++ b/sdks/typescript/providers/blaxel/src/mappings/mappings.ts @@ -18,6 +18,7 @@ import { ConfigFieldRegion, CreateFieldIsolation, CreateFieldLifetime, + CreateFieldLifetimeIdleTerminate, CreateFieldLifetimeIdleTerminateAfter, CreateFieldLifetimeIdleTerminateMode, CreateFieldLifetimeMaximumLifetime, @@ -234,7 +235,10 @@ function mapLifetime(r: CreateOptions, p: SandboxCreateConfiguration): void { if (v.idleTerminate) { const mode = v.idleTerminate.mode ?? PolicyMode.Default; if (mode === PolicyMode.Disabled) { - // Explicit disable: omit idle policy. + unsupported( + CreateFieldLifetimeIdleTerminate, + "disabling idle terminate is not representable", + ); } else if (mode === PolicyMode.After) { if (v.idleTerminate.after === undefined) invalid(CreateFieldLifetimeIdleTerminateAfter, "duration is required"); diff --git a/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts b/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts index ef514130..66a102c1 100644 --- a/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts +++ b/sdks/typescript/providers/blaxel/src/mappings/provider_mappings.ts @@ -134,7 +134,8 @@ function mapSandboxState(status: string): SandboxState | undefined { return SandboxState.Error; case "TERMINATED": case "DELETED": - return SandboxState.Terminated; + case "DEACTIVATED": + return SandboxState.Deleted; default: return undefined; } diff --git a/specs/providers/blaxel.yaml b/specs/providers/blaxel.yaml index 99276338..2f6fc233 100644 --- a/specs/providers/blaxel.yaml +++ b/specs/providers/blaxel.yaml @@ -8,6 +8,7 @@ checks: - { path: placement, unsupported: true } - { path: resources.cpu_cores, unsupported: true } - { path: resources.cpu_limit_cores, unsupported: true } + - { path: resources.memory_mib, minimum: 256 } - { path: resources.memory_limit_mib, unsupported: true } - { path: resources.disk_mib, unsupported: true } client: diff --git a/website/content/docs/configuration.mdx b/website/content/docs/configuration.mdx index 22951eca..7a05c606 100644 --- a/website/content/docs/configuration.mdx +++ b/website/content/docs/configuration.mdx @@ -42,7 +42,7 @@ if err != nil { | ---------- | ------------------------------------------------------------------------------------------- | | `provider` | Provider constructor | | `auth` | One of `apiKey`, `tokenPair`, `bearerToken`, `oauthRefresh`. Omit to use provider env creds | -| `scope` | App, environment, or organization | +| `scope` | App, environment, organization, or workspace | | `endpoint` | Provider endpoint URL override | | `region` | Target / default placement | | `timeout` | Default deadline for calls that omit their own | @@ -51,6 +51,7 @@ if err != nil { Provider setup: [Daytona](/docs/daytona). Provider setup: [Modal](/docs/modal). +Provider setup: [Blaxel](/docs/blaxel). ## Client