From 531ddaea84658555c67c3f1093cd190078460063 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Nov 2026 14:06:46 +0000 Subject: [PATCH] Fix Blaxel Ready conflation and drop unproven floors. Reject WaitCondition.Ready in TS (sandbox.wait is a deprecated no-op), map provisioning.timeout only as Blaxel create timeout, drop the undocumented 256 MiB memory floor, pre-reject reserved ports, align SANDBOX_ALREADY_EXISTS to AlreadyExists, and document global settings. Co-authored-by: Nabhag Motivaras --- .../create-blaxel-with-resources/README.md | 5 +- .../create-blaxel-with-resources/README.md | 5 +- sdks/go/providers/blaxel/README.md | 8 ++-- sdks/go/providers/blaxel/creation.go | 3 ++ sdks/go/providers/blaxel/errors.go | 3 ++ sdks/go/providers/blaxel/errors_test.go | 17 +++++++ sdks/go/providers/blaxel/filesystem.go | 3 ++ sdks/go/providers/blaxel/provider_checks.go | 3 -- sdks/go/providers/blaxel/rejection_test.go | 18 ++++--- sdks/typescript/providers/blaxel/README.md | 15 ++++-- .../providers/blaxel/src/blaxel.test.ts | 48 +++++++++++++++---- .../typescript/providers/blaxel/src/blaxel.ts | 12 ----- .../blaxel/src/checks/provider_checks.ts | 10 ---- .../providers/blaxel/src/mappings/mappings.ts | 43 +++++------------ specs/providers/blaxel.yaml | 1 - 15 files changed, 108 insertions(+), 86 deletions(-) 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 20fa9d31..deb277cf 100644 --- a/examples/go/blaxel/configuration/create-blaxel-with-resources/README.md +++ b/examples/go/blaxel/configuration/create-blaxel-with-resources/README.md @@ -4,12 +4,11 @@ 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. Blaxel requires at least 256 MiB when memory is set. +Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Omitted memory keeps the Blaxel default. ## Edge cases -- Omitted memory keeps the Blaxel default. -- Fractional or sub-256 MiB values fail validation. +- Memory values that Blaxel rejects are returned as provider errors. ## Run 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 d4848b99..20d973a8 100644 --- a/examples/typescript/blaxel/configuration/create-blaxel-with-resources/README.md +++ b/examples/typescript/blaxel/configuration/create-blaxel-with-resources/README.md @@ -4,12 +4,11 @@ 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. Blaxel requires at least 256 MiB when memory is set. +Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Omitted memory keeps the Blaxel default. ## Edge cases -- Omitted memory keeps the Blaxel default. -- Fractional or sub-256 MiB values fail validation. +- Memory values that Blaxel rejects are returned as provider errors. ## Run diff --git a/sdks/go/providers/blaxel/README.md b/sdks/go/providers/blaxel/README.md index 14291e63..78a1af60 100644 --- a/sdks/go/providers/blaxel/README.md +++ b/sdks/go/providers/blaxel/README.md @@ -63,8 +63,8 @@ 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. 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). +Ports 80, 443, and 8080 are rejected (Blaxel-reserved). 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 @@ -85,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 and must be at least 256 when set. Blaxel derives -CPU from memory, so `cpu_cores` and disk MiB overrides are rejected. +Memory is allocated in MiB. 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 75a9a4c8..29c04ec7 100644 --- a/sdks/go/providers/blaxel/creation.go +++ b/sdks/go/providers/blaxel/creation.go @@ -215,6 +215,9 @@ func (c *creation) network() error { if binding == nil { continue } + if binding.Port == 80 || binding.Port == 443 || binding.Port == 8080 { + return providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldNetworkPortsPort, "ports 80, 443, and 8080 are reserved by Blaxel") + } protocol, err := blaxelPortProtocol(binding.Transport) if err != nil { return err diff --git a/sdks/go/providers/blaxel/errors.go b/sdks/go/providers/blaxel/errors.go index 272e7ca4..2504f528 100644 --- a/sdks/go/providers/blaxel/errors.go +++ b/sdks/go/providers/blaxel/errors.go @@ -52,6 +52,9 @@ func mapProviderError(err error, operation string) error { kind = sandbox.ErrorKindResourceExhausted case sdkblaxel.ErrBadRequest, sdkblaxel.ErrPolicyViolation: kind = sandbox.ErrorKindInvalidArgument + case "SANDBOX_ALREADY_EXISTS", "VOLUME_ALREADY_ATTACHED": + // Control-plane codes (not in gateway apierror constants). + kind = sandbox.ErrorKindAlreadyExists } } if kind == sandbox.ErrorKindUnknown { diff --git a/sdks/go/providers/blaxel/errors_test.go b/sdks/go/providers/blaxel/errors_test.go index 5fc414f8..98fb6568 100644 --- a/sdks/go/providers/blaxel/errors_test.go +++ b/sdks/go/providers/blaxel/errors_test.go @@ -35,3 +35,20 @@ func TestMapProviderErrorPreservesContext(t *testing.T) { t.Fatalf("error = %v detail = %#v", err, detail) } } + +func TestMapProviderErrorSandboxAlreadyExists(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_ALREADY_EXISTS", + 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) + } +} diff --git a/sdks/go/providers/blaxel/filesystem.go b/sdks/go/providers/blaxel/filesystem.go index 76dbd7b9..6356d5c0 100644 --- a/sdks/go/providers/blaxel/filesystem.go +++ b/sdks/go/providers/blaxel/filesystem.go @@ -122,6 +122,9 @@ func (b *backend) Stat(ctx context.Context, id, path string, _ *sandbox.StatOpti if err != nil { return nil, err } + // SandboxInstanceFSService has no public Get/Stat; metadata is on the lower + // SandboxFilesystemService. Rebuild with the instance base URL so Stat uses + // the same sandbox endpoint as instance.FS without private-field access. fs := b.filesystemService(instance) res, err := fs.Get(ctx, path, blaxel.SandboxFilesystemGetParams{}) if err != nil { diff --git a/sdks/go/providers/blaxel/provider_checks.go b/sdks/go/providers/blaxel/provider_checks.go index b5bddfc0..5c52e0ac 100644 --- a/sdks/go/providers/blaxel/provider_checks.go +++ b/sdks/go/providers/blaxel/provider_checks.go @@ -32,9 +32,6 @@ 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/rejection_test.go b/sdks/go/providers/blaxel/rejection_test.go index 5ceace45..4e7c8015 100644 --- a/sdks/go/providers/blaxel/rejection_test.go +++ b/sdks/go/providers/blaxel/rejection_test.go @@ -114,12 +114,16 @@ func TestRejectedCreateSettings(t *testing.T) { field: sandbox.CreateFieldLifetimeIdleTerminate, }, { - name: "memory below floor", + name: "reserved port", request: namedImage(&sandbox.CreateOptions{ - Resources: &sandbox.Resources{MemoryMib: sandbox.Value(uint64(255))}, + Network: &sandbox.NetworkConfig{ + Ports: []*sandbox.PortBinding{ + {Port: 443, Transport: sandbox.PortTransportTLS}, + }, + }, }), kind: sandbox.ErrorKindInvalidArgument, - field: sandbox.CreateFieldResourcesMemoryMib, + field: sandbox.CreateFieldNetworkPortsPort, }, } { t.Run(tc.name, func(t *testing.T) { @@ -215,8 +219,8 @@ func TestMappedNetworkPortsAndDomains(t *testing.T) { Network: &sandbox.NetworkConfig{ OutboundDomains: &sandbox.StringAllowlist{Entries: []string{"example.com"}}, Ports: []*sandbox.PortBinding{ - {Port: 443, Transport: sandbox.PortTransportTLS}, - {Port: 8080, Transport: sandbox.PortTransportPlain}, + {Port: 3000, Transport: sandbox.PortTransportTLS}, + {Port: 3001, Transport: sandbox.PortTransportPlain}, }, }, }), "us-pdx-1") @@ -227,10 +231,10 @@ func TestMappedNetworkPortsAndDomains(t *testing.T) { t.Fatalf("AllowedDomains = %#v", got) } ports := plan.params.Sandbox.Spec.Runtime.Ports - if len(ports) != 2 || ports[0].Target != 443 || ports[0].Protocol != blaxel.PortProtocolTls { + if len(ports) != 2 || ports[0].Target != 3000 || ports[0].Protocol != blaxel.PortProtocolTls { t.Fatalf("Ports = %#v", ports) } - if ports[1].Target != 8080 || ports[1].Protocol != blaxel.PortProtocolTcp { + if ports[1].Target != 3001 || ports[1].Protocol != blaxel.PortProtocolTcp { t.Fatalf("Ports[1] = %#v", ports[1]) } } diff --git a/sdks/typescript/providers/blaxel/README.md b/sdks/typescript/providers/blaxel/README.md index b8972df4..1a06aad6 100644 --- a/sdks/typescript/providers/blaxel/README.md +++ b/sdks/typescript/providers/blaxel/README.md @@ -55,8 +55,10 @@ 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 (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`. +disable is rejected), and a create timeout (1–50 whole seconds). +`provisioning.waitFor` Ready is unsupported (`sandbox.wait` is a deprecated +no-op in `@blaxel/core`). Creation calls native `SandboxInstance.create`, not +`createIfNotExists`. Ports 80, 443, and 8080 are rejected (Blaxel-reserved). **Lifecycle** on the handle: `get` / `refresh` (lookup by name), `delete`, and `close`. `close` does not delete the sandbox. Start, stop, pause, archive, @@ -78,9 +80,12 @@ command limit. 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. -When you set memory, Blaxel requires at least 256 MiB. Omitted memory keeps -the Blaxel default. CPU, CPU limit, memory limit, and disk overrides are -rejected. +Omitted memory keeps the Blaxel default. CPU, CPU limit, memory limit, and +disk overrides are rejected. + +`@blaxel/core` applies credentials through process-global `settings`. Two Kit +Blaxel clients in one process overwrite each other; construct one client per +process or reconfigure before each use. ## License diff --git a/sdks/typescript/providers/blaxel/src/blaxel.test.ts b/sdks/typescript/providers/blaxel/src/blaxel.test.ts index 174543c3..63a3de47 100644 --- a/sdks/typescript/providers/blaxel/src/blaxel.test.ts +++ b/sdks/typescript/providers/blaxel/src/blaxel.test.ts @@ -14,7 +14,8 @@ import { CreateFieldLifetimeIdleTerminate, CreateFieldLifetimeMaximumLifetime, CreateFieldName, - CreateFieldResourcesMemoryMib, + CreateFieldNetworkPortsPort, + CreateFieldProvisioningWaitFor, FileFieldLocalPath, } from "@sandbox-kit/sandbox"; import { mapClientConfig, mapCreateRequest } from "#mappings/mappings.js"; @@ -116,7 +117,6 @@ describe("Blaxel mappings", () => { }, provisioning: { timeout: 30_000, - waitFor: WaitCondition.Ready, }, labels: { purpose: "test" }, environment: { NODE_ENV: "test" }, @@ -124,8 +124,6 @@ describe("Blaxel mappings", () => { { scope: { workspace: "ws" }, region: "us-pdx-1" }, ); expect(plan).toMatchObject({ - waitUntilReady: true, - readyTimeoutMs: 30_000, options: { timeout: 30 }, params: { name: "example", @@ -146,8 +144,8 @@ describe("Blaxel mappings", () => { }); }); - it("rejects memory below the Blaxel floor", () => { - expect(() => + it("maps memory without a Kit-side floor", () => { + expect( mapCreateRequest( { name: "example", @@ -155,14 +153,48 @@ describe("Blaxel mappings", () => { resources: { memoryMib: 128n }, }, { scope: { workspace: "ws" } }, + ).params.memory, + ).toBe(128); + expect(mapResources({ memoryMib: 4096n })).toEqual({ memory: 4096 }); + }); + + it("rejects WaitCondition.Ready", () => { + expect(() => + mapCreateRequest( + { + name: "example", + source: { image: { reference: "blaxel/base-image:latest" } }, + provisioning: { + timeout: 30_000, + waitFor: WaitCondition.Ready, + }, + }, + { scope: { workspace: "ws" } }, + ), + ).toThrowError( + expect.objectContaining({ + kind: ErrorKind.Unsupported, + field: CreateFieldProvisioningWaitFor, + }), + ); + }); + + it("rejects Blaxel-reserved ports", () => { + expect(() => + mapCreateRequest( + { + name: "example", + source: { image: { reference: "blaxel/base-image:latest" } }, + network: { ports: [{ port: 443, transport: PortTransport.Tls }] }, + }, + { scope: { workspace: "ws" } }, ), ).toThrowError( expect.objectContaining({ kind: ErrorKind.InvalidArgument, - field: CreateFieldResourcesMemoryMib, + field: CreateFieldNetworkPortsPort, }), ); - expect(mapResources({ memoryMib: 4096n })).toEqual({ memory: 4096 }); }); it("rejects lifetime durations that are not whole seconds", () => { diff --git a/sdks/typescript/providers/blaxel/src/blaxel.ts b/sdks/typescript/providers/blaxel/src/blaxel.ts index 2f4bad22..039902be 100644 --- a/sdks/typescript/providers/blaxel/src/blaxel.ts +++ b/sdks/typescript/providers/blaxel/src/blaxel.ts @@ -83,10 +83,6 @@ interface NativeSandboxHandle { stderr?: string; }>; }; - wait(options?: { - maxWait?: number; - interval?: number; - }): Promise; delete(): Promise; } @@ -155,14 +151,6 @@ class BlaxelBackend extends BackendBase implements Backend { const plan = mapCreateRequest(request, this.config); throwIfAborted(options.signal); const sandbox = await this.native.create(plan.params, plan.options); - if (plan.waitUntilReady) { - throwIfAborted(options.signal); - await sandbox.wait({ - ...(plan.readyTimeoutMs === undefined - ? {} - : { maxWait: plan.readyTimeoutMs }), - }); - } return { sandbox: mapSandboxInfo(sandbox, request) }; } catch (error) { throw mapBlaxelError(error, "create"); diff --git a/sdks/typescript/providers/blaxel/src/checks/provider_checks.ts b/sdks/typescript/providers/blaxel/src/checks/provider_checks.ts index de2c3f86..b4aa2d5e 100644 --- a/sdks/typescript/providers/blaxel/src/checks/provider_checks.ts +++ b/sdks/typescript/providers/blaxel/src/checks/provider_checks.ts @@ -1,7 +1,6 @@ import { ErrorKind, CreateFieldEnvironment, - CreateFieldResourcesMemoryMib, validEnvName, } from "@sandbox-kit/sandbox"; import type { CreateOptions } from "@sandbox-kit/sandbox"; @@ -10,15 +9,6 @@ import { providerError } from "#errors.js"; // validateProviderCreate rejects creation settings this provider cannot honor. // Rules are inventoried in specs/providers/blaxel.yaml checks when present. export function validateProviderCreate(request: CreateOptions): void { - const memoryMib = request.resources?.memoryMib; - if (memoryMib !== undefined && memoryMib < BigInt("256")) { - providerError({ - kind: ErrorKind.InvalidArgument, - operation: "create", - field: CreateFieldResourcesMemoryMib, - message: "resources.memory_mib is outside supported values", - }); - } for (const value of Object.keys(request.environment ?? {})) { if (!validEnvName(value)) { providerError({ diff --git a/sdks/typescript/providers/blaxel/src/mappings/mappings.ts b/sdks/typescript/providers/blaxel/src/mappings/mappings.ts index 69d410ea..1d4f81aa 100644 --- a/sdks/typescript/providers/blaxel/src/mappings/mappings.ts +++ b/sdks/typescript/providers/blaxel/src/mappings/mappings.ts @@ -52,8 +52,6 @@ import { providerError } from "#errors.js"; export interface BlaxelCreatePlan { readonly params: SandboxCreateConfiguration; readonly options?: SandboxCreateOptions; - readonly waitUntilReady: boolean; - readonly readyTimeoutMs?: number; } export function mapClientConfig(config: Config): NativeClientConfig { @@ -91,15 +89,11 @@ export function mapCreateRequest( mapLifetime(request, params); mapReadiness(request); mapObservability(request); - const wait = mapProvisioning(request); + const options = mapProvisioning(request); return { params, - ...(wait.options === undefined ? {} : { options: wait.options }), - waitUntilReady: wait.waitUntilReady, - ...(wait.readyTimeoutMs === undefined - ? {} - : { readyTimeoutMs: wait.readyTimeoutMs }), + ...(options === undefined ? {} : { options }), }; } @@ -174,6 +168,11 @@ function mapNetwork(r: CreateOptions, p: SandboxCreateConfiguration): void { const port = x.port; if (port === undefined) invalid(CreateFieldNetworkPortsPort, "port is required"); + if (port === 80 || port === 443 || port === 8080) + invalid( + CreateFieldNetworkPortsPort, + "ports 80, 443, and 8080 are reserved by Blaxel", + ); ports.push({ target: port, protocol: blaxelPortProtocol( @@ -269,13 +268,12 @@ function mapObservability(r: CreateOptions): void { ); } -function mapProvisioning(r: CreateOptions): { - waitUntilReady: boolean; - readyTimeoutMs?: number; - options?: SandboxCreateOptions; -} { +// mapProvisioning maps create timeout only. WaitCondition.Ready is rejected: +// @blaxel/core sandbox.wait() is a deprecated no-op, and Ready previously +// conflated that with Blaxel's destructive creation deadline (1–50s). +function mapProvisioning(r: CreateOptions): SandboxCreateOptions | undefined { const v = r.provisioning; - if (!v) return { waitUntilReady: false }; + if (!v) return undefined; if (v.queueTimeout !== undefined) unsupported( CreateFieldProvisioningQueueTimeout, @@ -299,22 +297,7 @@ function mapProvisioning(r: CreateOptions): { const waitFor = v.waitFor ?? WaitCondition.Default; if (waitFor === WaitCondition.Default || waitFor === WaitCondition.Scheduled) - return { - waitUntilReady: false, - ...(options === undefined ? {} : { options }), - }; - if (waitFor === WaitCondition.Ready) { - if (v.timeout === undefined || v.timeout <= 0) - invalid( - CreateFieldProvisioningTimeout, - "ready wait requires a positive timeout", - ); - return { - waitUntilReady: true, - readyTimeoutMs: v.timeout, - ...(options === undefined ? {} : { options }), - }; - } + return options; unsupported( CreateFieldProvisioningWaitFor, "requested wait condition is unsupported", diff --git a/specs/providers/blaxel.yaml b/specs/providers/blaxel.yaml index 2f6fc233..99276338 100644 --- a/specs/providers/blaxel.yaml +++ b/specs/providers/blaxel.yaml @@ -8,7 +8,6 @@ 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: