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