Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/components/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ A production-grade, FastAPI-based service for managing the lifecycle of containe
- **Resource quotas**: CPU/memory limits with Kubernetes-style specs
- **Observability**: Unified status with transition tracking
- **Registry support**: Public and private images
- **Read-only root filesystems**: Optional per-sandbox hardening for Docker and Kubernetes template workloads

### Extended capabilities
- **Async provisioning**: Background creation to reduce latency
Expand All @@ -30,6 +31,27 @@ A production-grade, FastAPI-based service for managing the lifecycle of containe
- **Port resolution**: Dynamic endpoint generation
- **Structured errors**: Standard error codes and messages

## Read-only root filesystem

Set `readOnlyRootFilesystem: true` on a sandbox create request to make the main
workload container's root filesystem read-only. The setting is supported by the
Linux Docker provider and Kubernetes template workloads. The response field of
the same name is read from Docker inspect or the final Kubernetes workload
manifest, so it reports the effective runtime policy rather than only echoing
the request.

The OpenSandbox runtime directory remains writable: Docker provisions a
server-managed writable tmpfs at `/opt/opensandbox`, and Kubernetes keeps the
`opensandbox-bin` volume writable for the execd init container and runtime
artifacts. Explicit user volumes retain their existing `readOnly` setting, so a
declared read-write volume remains writable and a declared read-only volume
remains read-only.

Omitting the field or setting it to `false` preserves provider and operator
template defaults. Request-level changes are not supported with
`extensions.poolRef`, because Pool Pods are pre-created; configure the Pool
template's main-container `securityContext.readOnlyRootFilesystem` instead.

::: warning
Metadata keys under the reserved prefix `opensandbox.io/` are system-managed and cannot be supplied by users.
:::
Expand Down
12 changes: 12 additions & 0 deletions docs/kubernetes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,18 @@ allocated warm pod cannot gain new volumes. See the
[Kubernetes PVC guide](/examples/kubernetes-pvc-volume-mount#pool-mode-pre-mount-a-shared-pvc).
:::

::: tip Read-only root filesystem
For template-mode sandboxes, set `readOnlyRootFilesystem: true` in the
lifecycle request. The main container receives
`securityContext.readOnlyRootFilesystem: true`; the execd init container and
the `opensandbox-bin` runtime volume remain writable, and explicit user volume
mounts keep their individual `readOnly` values. Pool allocations reject this
request-level field because their Pods already exist. Set the equivalent
security context in the Pool template when all warm Pods should use a
read-only root filesystem. The lifecycle response returns `null` when a Pool
workload's effective policy cannot be confirmed.
:::

#### Pooled Sandbox with Heterogeneous Tasks
Create a batch of sandboxes with process-based heterogeneous tasks. For task execution to work properly, the task-executor must be deployed as a sidecar container in the pool template and share the process namespace with the sandbox container:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ private static SandboxInfo ParseSandboxInfo(JsonElement element)
State = allocation.GetProperty("state").GetString() ?? throw new SandboxApiException("Missing allocation.state in response")
}
: null,
ReadOnlyRootFilesystem = element.TryGetProperty("readOnlyRootFilesystem", out var readOnlyRootFilesystem)
&& readOnlyRootFilesystem.ValueKind != JsonValueKind.Null
? readOnlyRootFilesystem.GetBoolean()
: null,
Entrypoint = element.GetProperty("entrypoint").EnumerateArray().Select(e => e.GetString() ?? string.Empty).ToList(),
Metadata = ParseStringMap(element, "metadata"),
Extensions = ParseStringMap(element, "extensions"),
Expand Down Expand Up @@ -385,7 +389,11 @@ private static CreateSandboxResponse ParseCreateSandboxResponse(JsonElement elem
ExpiresAt = element.TryGetProperty("expiresAt", out var expiresAtElement)
? ParseOptionalIsoDate("expiresAt", expiresAtElement)
: null,
Entrypoint = element.GetProperty("entrypoint").EnumerateArray().Select(e => e.GetString() ?? string.Empty).ToList()
Entrypoint = element.GetProperty("entrypoint").EnumerateArray().Select(e => e.GetString() ?? string.Empty).ToList(),
ReadOnlyRootFilesystem = element.TryGetProperty("readOnlyRootFilesystem", out var readOnlyRootFilesystem)
&& readOnlyRootFilesystem.ValueKind != JsonValueKind.Null
? readOnlyRootFilesystem.GetBoolean()
: null
};
}

Expand Down
19 changes: 19 additions & 0 deletions sdks/sandbox/csharp/src/OpenSandbox/Models/Sandboxes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,12 @@ public class SandboxInfo
[JsonPropertyName("allocation")]
public AllocationSummary? Allocation { get; set; }

/// <summary>
/// Gets or sets the actual read-only root filesystem state confirmed by the runtime.
/// </summary>
[JsonPropertyName("readOnlyRootFilesystem")]
public bool? ReadOnlyRootFilesystem { get; set; }

/// <summary>
/// Gets or sets the sandbox creation time.
/// </summary>
Expand Down Expand Up @@ -876,6 +882,13 @@ public class CreateSandboxRequest
[JsonPropertyName("secureAccess")]
public bool? SecureAccess { get; set; }

/// <summary>
/// Gets or sets whether to request a read-only root filesystem for the main container.
/// </summary>
[JsonPropertyName("readOnlyRootFilesystem")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? ReadOnlyRootFilesystem { get; set; }

/// <summary>
/// Gets or sets the custom metadata tags.
/// </summary>
Expand Down Expand Up @@ -965,6 +978,12 @@ public class CreateSandboxResponse
/// </summary>
[JsonPropertyName("entrypoint")]
public required IReadOnlyList<string> Entrypoint { get; set; }

/// <summary>
/// Gets or sets the actual read-only root filesystem state confirmed by the runtime.
/// </summary>
[JsonPropertyName("readOnlyRootFilesystem")]
public bool? ReadOnlyRootFilesystem { get; set; }
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions sdks/sandbox/csharp/src/OpenSandbox/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ public class SandboxCreateOptions
/// </summary>
public bool SecureAccess { get; set; }

/// <summary>
/// Gets or sets whether to request a read-only root filesystem for the main container.
/// Null preserves provider/template defaults.
/// </summary>
public bool? ReadOnlyRootFilesystem { get; set; }

/// <summary>
/// Gets or sets the resource limits.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions sdks/sandbox/csharp/src/OpenSandbox/Sandbox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public static async Task<Sandbox> CreateAsync(
ResourceRequests = options.ResourceRequests,
Env = options.Env,
SecureAccess = options.SecureAccess,
ReadOnlyRootFilesystem = options.ReadOnlyRootFilesystem,
Metadata = options.Metadata,
Platform = options.Platform,
NetworkPolicy = options.NetworkPolicy != null
Expand Down
30 changes: 30 additions & 0 deletions sdks/sandbox/csharp/tests/OpenSandbox.Tests/ModelsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,36 @@ public void Volume_WithOssfs_ShouldSerializeExpectedPayload()
json.Should().Contain("\"platform\":{\"os\":\"linux\",\"arch\":\"arm64\"}");
}

[Fact]
public void CreateSandboxRequest_ReadOnlyRootFilesystemPreservesOmittedAndFalse()
{
var omitted = new CreateSandboxRequest
{
ResourceLimits = new Dictionary<string, string>(),
Entrypoint = new List<string> { "python" }
};
var explicitFalse = new CreateSandboxRequest
{
ResourceLimits = new Dictionary<string, string>(),
Entrypoint = new List<string> { "python" },
ReadOnlyRootFilesystem = false
};

JsonSerializer.Serialize(omitted).Should().NotContain("readOnlyRootFilesystem");
JsonSerializer.Serialize(explicitFalse).Should().Contain("\"readOnlyRootFilesystem\":false");
}

[Fact]
public void CreateSandboxResponse_ReadOnlyRootFilesystemDeserializesNull()
{
const string json = "{\"id\":\"sbx-1\",\"status\":{\"state\":\"Running\"},\"createdAt\":\"2026-03-14T12:00:00Z\",\"entrypoint\":[\"python\"],\"readOnlyRootFilesystem\":null}";

var response = JsonSerializer.Deserialize<CreateSandboxResponse>(json);

response.Should().NotBeNull();
response!.ReadOnlyRootFilesystem.Should().BeNull();
}

[Fact]
public void SandboxMetrics_ShouldStoreProperties()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,30 @@ public async Task CreateSandboxAsync_ShouldTreatMissingExpiresAtAsNull()
.WhoseValue.Should().Be("中文数据");
}

[Fact]
public async Task CreateSandboxAsync_ShouldParseNullReadOnlyRootFilesystem()
{
const string payload = """
{
"id": "sbx-2",
"status": { "state": "Pending" },
"createdAt": "2026-03-14T12:00:00Z",
"entrypoint": ["python"],
"readOnlyRootFilesystem": null
}
""";
var adapter = CreateAdapterWithJsonResponse(payload);

CreateSandboxResponse response = await adapter.CreateSandboxAsync(new CreateSandboxRequest
{
Image = new ImageSpec { Uri = "python:3.11" },
ResourceLimits = new Dictionary<string, string>(),
Entrypoint = new List<string> { "python" }
});

response.ReadOnlyRootFilesystem.Should().BeNull();
}

[Fact]
public async Task CreateSandboxAsync_ShouldSerializeSecureAccess()
{
Expand Down
17 changes: 17 additions & 0 deletions sdks/sandbox/go/allocation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ func TestSandboxInfoAllocationAbsentIsOmitted(t *testing.T) {
}
}

func TestReadOnlyRootFilesystemJSONPreservesFalseAndNull(t *testing.T) {
falseValue := false
req, err := json.Marshal(CreateSandboxRequest{
ResourceLimits: ResourceLimits{},
ReadOnlyRootFilesystem: &falseValue,
})
require.NoError(t, err)
assert.Contains(t, string(req), `"readOnlyRootFilesystem":false`)

var info SandboxInfo
err = json.Unmarshal([]byte(`{"id":"sbx-1","readOnlyRootFilesystem":null}`), &info)
require.NoError(t, err)
if info.ReadOnlyRootFilesystem != nil {
t.Fatalf("null readOnlyRootFilesystem should unmarshal to nil, got %v", *info.ReadOnlyRootFilesystem)
}
}

func TestLifecycleClientAllocationInGetAndListResponses(t *testing.T) {
_, client := newLifecycleServer(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
Expand Down
33 changes: 19 additions & 14 deletions sdks/sandbox/go/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ type SandboxCreateOptions struct {
// "windows", "arch": "amd64"}). When nil the server applies its default.
Platform *PlatformSpec

// ReadOnlyRootFilesystem requests a read-only root filesystem for the main container.
// Nil preserves provider/template defaults; false is sent explicitly when provided.
ReadOnlyRootFilesystem *bool

// SkipHealthCheck skips the WaitUntilReady call after creation.
SkipHealthCheck bool

Expand Down Expand Up @@ -138,20 +142,21 @@ func CreateSandbox(ctx context.Context, config ConnectionConfig, opts SandboxCre
started := time.Now()

req := CreateSandboxRequest{
Image: nil,
SnapshotID: opts.SnapshotID,
Entrypoint: entrypoint,
ResourceLimits: limits,
ResourceRequests: opts.ResourceRequests,
Timeout: timeout,
Env: opts.Env,
SecureAccess: opts.SecureAccess,
Metadata: opts.Metadata,
NetworkPolicy: opts.NetworkPolicy,
CredentialProxy: opts.CredentialProxy,
Volumes: opts.Volumes,
Extensions: opts.Extensions,
Platform: opts.Platform,
Image: nil,
SnapshotID: opts.SnapshotID,
Entrypoint: entrypoint,
ResourceLimits: limits,
ResourceRequests: opts.ResourceRequests,
Timeout: timeout,
Env: opts.Env,
SecureAccess: opts.SecureAccess,
Metadata: opts.Metadata,
NetworkPolicy: opts.NetworkPolicy,
CredentialProxy: opts.CredentialProxy,
Volumes: opts.Volumes,
Extensions: opts.Extensions,
Platform: opts.Platform,
ReadOnlyRootFilesystem: opts.ReadOnlyRootFilesystem,
}
if opts.Image != "" {
req.Image = &ImageSpec{URI: opts.Image, Auth: opts.ImageAuth}
Expand Down
52 changes: 27 additions & 25 deletions sdks/sandbox/go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,21 @@ type CredentialProxyConfig struct {

// CreateSandboxRequest is the request body for creating a new sandbox.
type CreateSandboxRequest struct {
Image *ImageSpec `json:"image,omitempty"`
SnapshotID string `json:"snapshotId,omitempty"`
Timeout *int `json:"timeout,omitempty"`
ResourceLimits ResourceLimits `json:"resourceLimits"`
ResourceRequests ResourceLimits `json:"resourceRequests,omitempty"`
Env map[string]string `json:"env,omitempty"`
SecureAccess bool `json:"secureAccess,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Entrypoint []string `json:"entrypoint,omitempty"`
NetworkPolicy *NetworkPolicy `json:"networkPolicy,omitempty"`
CredentialProxy *CredentialProxyConfig `json:"credentialProxy,omitempty"`
Volumes []Volume `json:"volumes,omitempty"`
Extensions map[string]string `json:"extensions,omitempty"`
Platform *PlatformSpec `json:"platform,omitempty"`
Image *ImageSpec `json:"image,omitempty"`
SnapshotID string `json:"snapshotId,omitempty"`
Timeout *int `json:"timeout,omitempty"`
ResourceLimits ResourceLimits `json:"resourceLimits"`
ResourceRequests ResourceLimits `json:"resourceRequests,omitempty"`
Env map[string]string `json:"env,omitempty"`
SecureAccess bool `json:"secureAccess,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Entrypoint []string `json:"entrypoint,omitempty"`
NetworkPolicy *NetworkPolicy `json:"networkPolicy,omitempty"`
CredentialProxy *CredentialProxyConfig `json:"credentialProxy,omitempty"`
Volumes []Volume `json:"volumes,omitempty"`
Extensions map[string]string `json:"extensions,omitempty"`
Platform *PlatformSpec `json:"platform,omitempty"`
ReadOnlyRootFilesystem *bool `json:"readOnlyRootFilesystem,omitempty"`
}

// AllocationMode identifies how the runtime allocated a sandbox.
Expand Down Expand Up @@ -193,17 +194,18 @@ type AllocationSummary struct {
// SandboxInfo represents a runtime execution environment provisioned from a
// container image, as returned by the lifecycle API.
type SandboxInfo struct {
ID string `json:"id"`
Image *ImageSpec `json:"image,omitempty"`
SnapshotID string `json:"snapshotId,omitempty"`
Status SandboxStatus `json:"status"`
Metadata map[string]string `json:"metadata,omitempty"`
Extensions map[string]string `json:"extensions,omitempty"`
Entrypoint []string `json:"entrypoint"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
Platform *PlatformSpec `json:"platform,omitempty"`
Allocation *AllocationSummary `json:"allocation,omitempty"`
ID string `json:"id"`
Image *ImageSpec `json:"image,omitempty"`
SnapshotID string `json:"snapshotId,omitempty"`
Status SandboxStatus `json:"status"`
Metadata map[string]string `json:"metadata,omitempty"`
Extensions map[string]string `json:"extensions,omitempty"`
Entrypoint []string `json:"entrypoint"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
Platform *PlatformSpec `json:"platform,omitempty"`
Allocation *AllocationSummary `json:"allocation,omitempty"`
ReadOnlyRootFilesystem *bool `json:"readOnlyRootFilesystem,omitempty"`
}

type SnapshotState string
Expand Down
19 changes: 19 additions & 0 deletions sdks/sandbox/javascript/src/api/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,12 @@ export interface components {
* this is restored from the snapshot.
*/
entrypoint: string[];
/**
* @description Actual read-only root filesystem state confirmed from the runtime.
* Null means the runtime could not confirm the policy, such as an
* opaque pre-created Pool workload.
*/
readOnlyRootFilesystem?: boolean | null;
};
/** @description Optional settings for creating a sandbox snapshot. */
CreateSnapshotRequest: {
Expand Down Expand Up @@ -945,6 +951,12 @@ export interface components {
* from the snapshot.
*/
entrypoint: string[];
/**
* @description Actual read-only root filesystem state confirmed from the runtime.
* Null means the runtime could not confirm the policy, such as an
* opaque pre-created Pool workload.
*/
readOnlyRootFilesystem?: boolean | null;
/**
* Format: date-time
* @description Timestamp when sandbox will auto-terminate. Omitted when manual cleanup is enabled.
Expand Down Expand Up @@ -1224,6 +1236,13 @@ export interface components {
* @default false
*/
secureAccess: boolean;
/**
* @description Request a read-only root filesystem for the sandbox main container.
* Set to true to enable it. When omitted or false, the provider and
* template defaults are preserved. This field is not supported with
* `extensions.poolRef`; configure the Pool template instead.
*/
readOnlyRootFilesystem?: boolean | null;
/**
* @description Storage mounts for the sandbox. Each volume entry specifies a named backend-specific
* storage source and common mount settings. Exactly one backend type must be specified
Expand Down
Loading
Loading