diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1d478fd..204bf56 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,19 @@ jobs:
- name: cross-build linux amd64/arm64
run: make build-linux
+ # The adapter kinds left out of the default build (experimental_adapters
+ # tag) must keep compiling and passing their fake-server tests.
+ go-experimental:
+ name: go (experimental adapters)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ - name: build, vet and test with -tags experimental_adapters
+ run: make experimental
+
postgres:
name: postgres conformance
runs-on: ubuntu-latest
@@ -180,7 +193,7 @@ jobs:
# again. It approves what CI checked, not what a reviewer read.
approve:
name: approve when CI passes
- needs: [go, postgres, sdk-typescript, console, sdk-python, helm, images, dco, release-source]
+ needs: [go, go-experimental, postgres, sdk-typescript, console, sdk-python, helm, images, dco, release-source]
if: >-
always() &&
github.event_name == 'pull_request' &&
diff --git a/AGENTS.md b/AGENTS.md
index 2edab2e..f36e129 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -24,8 +24,9 @@ for humans).
make verify
```
-It runs gofmt, go vet, Go tests with `-race`, the SDK tests and the Helm lint,
-including the replay of recorded OpenSandbox runtimes. For changes touching
+It runs gofmt, go vet, Go tests with `-race`, the same build, vet and tests
+with `-tags experimental_adapters` (`make experimental`), the SDK tests and
+the Helm lint, including the replay of recorded OpenSandbox runtimes. For changes touching
`internal/store/postgres`, also run `make test-postgres`. For `console/`, run
`npm run typecheck && npm run build && npm run build:server` there; for
`site/` or any page it publishes from `docs/`, `npm run build` in `site/`
@@ -46,7 +47,12 @@ that covers it, and say in the PR what did not run.
(acceptance scenarios A–L) green.
- Backends: `internal/adapters/*` (`opensandbox` is the default private
runtime; Differently runs no sandboxes itself), `internal/wiring` (adapter
- factories), `internal/pools`. The real-stack suite is `test/e2e`
+ factories), `internal/pools`. The default build holds `opensandbox`,
+ `daytona`, `freestyle`, `remote`, `devhost` and `fake`. Every other
+ adapter, and the `cloudhttp`, `pyshell` and `shellcmd` helpers, carries
+ `//go:build experimental_adapters`: a plain `go build`, `go vet` or
+ `go test` does not see them, so use `make experimental` when you touch
+ them or anything they share (`httpx`, `pkg/adapter`). The real-stack suite is `test/e2e`
(`make e2e-prepare e2e-up e2e`, or against any install).
- Recorded runtimes: `internal/adapters/opensandbox/testdata/recordings`,
replayed by `TestReplayRecordings`; re-record with the live test after an
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d6bd03..136173e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,38 @@ All notable changes are recorded here. The format follows
## [Unreleased] — on `pre-release`
+### Changed (breaking): the default build holds only the verified adapters
+
+The published image, the release binaries and `make build` now contain the
+`opensandbox`, `daytona` and `freestyle` adapters, the `remote` plugin
+protocol and the dev-mode `devhost` and `fake`. The other 13 adapter kinds
+are experimental and compile in only with the Go build tag
+`experimental_adapters`: `agent-sandbox`, `cubesandbox`, `e2b`, `boat`,
+`morph`, `vercel`, `fly`, `blaxel`, `aws-agentcore`, `azure-sandboxes`,
+`azure-sessions`, `gcp-sandboxes` and `northflank`. Apart from
+`agent-sandbox`, which ran live on a kind cluster, they have only been
+tested against fake servers. No adapter code was removed, and CI still
+builds, vets and tests it with the tag.
+
+- **If you use one of the 13 kinds**, the next published image will not
+ start with a config file that names it: the error names the pool, the
+ kind and the build tag. A pool of such a kind added earlier through the
+ API stays registered and reports `unavailable` with the same reason.
+ Build the server with `go build -tags experimental_adapters`, or the image
+ with `--build-arg GO_TAGS=experimental_adapters`
+ ([provider catalog](docs/providers/README.md#what-the-server-contains)).
+- `POST /v1/admin/pools` answers 400 `invalid_request` for these kinds on a
+ default build.
+- Helm: pools of these kinds are refused unless `server.experimentalAdapters`
+ is `true`, which you set together with a `server.image` built with the
+ tag.
+- The dashboard, the console and `differently admin pool add --help` list
+ only the default kinds. Experimental pools are added in the config file,
+ with the CLI or through the API.
+- `make verify` and CI gained `make experimental` (build, vet and test with
+ the tag). The live tests of `agent-sandbox` and `cubesandbox` now need
+ `-tags live,experimental_adapters`.
+
### Fixed (placement, scheduler and clients)
Found by reading the code, each with a test that fails on the old behaviour.
diff --git a/Makefile b/Makefile
index c08dfbb..d34a748 100644
--- a/Makefile
+++ b/Makefile
@@ -4,18 +4,23 @@ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo 0.1.
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
LDFLAGS := -s -w -X github.com/different-ai/differently/internal/version.Version=$(VERSION) -X github.com/different-ai/differently/internal/version.Commit=$(COMMIT)
BINS := differently-server differently
+# Go build tags for `make build`, `build-linux` and `images`. The default (none)
+# is the published build: opensandbox, daytona, freestyle, remote and devhost.
+# GO_TAGS=experimental_adapters adds the other adapter kinds (docs/providers).
+GO_TAGS ?=
+EXPERIMENTAL_TAG := experimental_adapters
PG_PORT ?= 55432
-.PHONY: build build-linux test test-postgres fmt-check vet sdk-test helm-lint verify dev images clean
+.PHONY: build build-linux test test-postgres fmt-check vet experimental sdk-test helm-lint verify dev images clean
build:
@mkdir -p bin
- CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/ $(addprefix ./cmd/,$(BINS))
+ CGO_ENABLED=0 go build -trimpath -tags "$(GO_TAGS)" -ldflags "$(LDFLAGS)" -o bin/ $(addprefix ./cmd/,$(BINS))
build-linux:
@for arch in amd64 arm64; do \
mkdir -p bin/linux-$$arch; \
- CGO_ENABLED=0 GOOS=linux GOARCH=$$arch go build -trimpath -ldflags "$(LDFLAGS)" -o bin/linux-$$arch/ $(addprefix ./cmd/,$(BINS)) || exit 1; \
+ CGO_ENABLED=0 GOOS=linux GOARCH=$$arch go build -trimpath -tags "$(GO_TAGS)" -ldflags "$(LDFLAGS)" -o bin/linux-$$arch/ $(addprefix ./cmd/,$(BINS)) || exit 1; \
done
fmt-check:
@@ -27,6 +32,13 @@ vet:
test:
go test -race -count=1 ./...
+# The adapter kinds outside the default build must keep compiling and passing
+# their fake-server tests: build, vet and test everything with the tag.
+experimental:
+ go build -tags $(EXPERIMENTAL_TAG) ./...
+ go vet -tags $(EXPERIMENTAL_TAG) ./...
+ go test -race -count=1 -tags $(EXPERIMENTAL_TAG) ./...
+
# Runs the store conformance suite against a disposable Postgres container.
test-postgres:
@docker rm -f differently-pg-test >/dev/null 2>&1 || true
@@ -46,13 +58,13 @@ helm-lint:
helm template differently deploy/helm/differently -f $$f >/dev/null || exit 1; \
done
-verify: fmt-check vet test sdk-test helm-lint
+verify: fmt-check vet test experimental sdk-test helm-lint
dev: build
./bin/differently-server --dev
images:
- docker build -f build/server.Dockerfile -t ghcr.io/different-ai/differently-server:$(VERSION) .
+ docker build -f build/server.Dockerfile --build-arg GO_TAGS=$(GO_TAGS) -t ghcr.io/different-ai/differently-server:$(VERSION) .
docker build -f build/cli.Dockerfile -t ghcr.io/different-ai/differently-cli:$(VERSION) .
clean:
diff --git a/README.md b/README.md
index 99371ac..7c5aed6 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-
+
@@ -30,10 +30,12 @@ account of it:
- by default on [OpenSandbox](https://github.com/opensandbox-group/OpenSandbox)
on your own Docker hosts, Kubernetes clusters or microVM nodes;
-- on [15 other built-in provider adapters](docs/providers/README.md), from
- E2B, Daytona and Vercel to the AWS, Azure and Google Cloud sandbox
- services, or a plugin for any other backend, only where your policies
- allow it.
+- on Daytona or Freestyle, or a plugin for any other backend, only where
+ your policies allow it;
+- on 13 more [provider adapters](docs/providers/README.md#what-the-server-contains),
+ from E2B and Vercel to the AWS, Azure and Google Cloud sandbox services,
+ which are experimental: tested against fake servers and left out of the
+ published build unless you compile them in.
Apache-2.0, fully self-hosted, no telemetry. Built by the
[OpenWork](https://github.com/different-ai) team.
@@ -145,7 +147,8 @@ prerequisites, [day-2 operations](docs/deploy/day-2.md) and the
- the published `v0.1.0-alpha.1` chart and images install from public
artifacts alone;
- real OpenSandbox responses are recorded and replayed in every `go test`;
-- the `agent-sandbox` adapter against a Kubernetes cluster (kind);
+- the experimental `agent-sandbox` adapter against a Kubernetes cluster
+ (kind);
- the Daytona (container class) and Freestyle adapters against live
accounts: exec, files, port URLs, stop/start and snapshots on both, and
egress policy, pause/resume, memory snapshots and fork on Freestyle;
@@ -161,7 +164,10 @@ prerequisites, [day-2 operations](docs/deploy/day-2.md) and the
- node drains, and more than one KVM node;
- managed EKS, GKE and AKS;
- the other 12 provider adapters against live accounts: each is tested
- against a fake of its provider's published API.
+ against a fake of its provider's published API. These 12 and
+ `agent-sandbox` are experimental and not in the published image or
+ binaries; a server built with `-tags experimental_adapters` has them
+ ([what the server contains](docs/providers/README.md#what-the-server-contains)).
Each of the 26 providers has a page with its tested status, capabilities and
configuration in the [provider catalog](docs/providers/README.md). The
@@ -179,8 +185,11 @@ against global, organization and project policy, ranks what is left, and
records why. Adapters talk to the backends:
- `opensandbox`, the default private runtime;
-- 15 more provider adapters, from `agent-sandbox` and `cubesandbox` you run
- to `e2b`, `daytona`, `vercel` and the cloud providers' sandbox services
+- `daytona` and `freestyle`, managed providers that receive work only when
+ policy allows it;
+- 13 experimental adapters behind the `experimental_adapters` build tag,
+ from `agent-sandbox` and `cubesandbox` you run to `e2b`, `vercel` and the
+ cloud providers' sandbox services
([provider catalog](docs/providers/README.md));
- `remote`, any service speaking the [adapter protocol](docs/adapters/specification.md).
diff --git a/build/server.Dockerfile b/build/server.Dockerfile
index f9e8bc8..10efb14 100644
--- a/build/server.Dockerfile
+++ b/build/server.Dockerfile
@@ -1,11 +1,14 @@
# differently-server image: control plane plus the CLI (used by `helm test`).
+# The published image is built with no Go build tags. To include the
+# experimental adapter kinds (docs/providers/README.md):
+# docker build -f build/server.Dockerfile --build-arg GO_TAGS=experimental_adapters .
FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS build
-ARG TARGETOS TARGETARCH VERSION=0.1.0-dev COMMIT=unknown
+ARG TARGETOS TARGETARCH VERSION=0.1.0-dev COMMIT=unknown GO_TAGS=""
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath \
+RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -tags "${GO_TAGS}" \
-ldflags "-s -w -X github.com/different-ai/differently/internal/version.Version=${VERSION} -X github.com/different-ai/differently/internal/version.Commit=${COMMIT}" \
-o /out/ ./cmd/differently-server ./cmd/differently
diff --git a/cmd/differently/main.go b/cmd/differently/main.go
index 588f4e9..b5ec6d5 100644
--- a/cmd/differently/main.go
+++ b/cmd/differently/main.go
@@ -632,7 +632,7 @@ func poolAddCmd() *cobra.Command {
return raw("POST", "/v1/admin/pools", body)
}}
f := cmd.Flags()
- f.StringVar(&adapterKind, "adapter", "", "opensandbox, daytona, freestyle, remote, vercel, fly, agent-sandbox, cubesandbox, aws-agentcore, azure-sandboxes, azure-sessions, gcp-sandboxes, northflank, boat, e2b, morph or blaxel")
+ f.StringVar(&adapterKind, "adapter", "", "opensandbox, daytona, freestyle or remote; a server built with -tags experimental_adapters has more (docs/providers)")
f.StringVar(&trust, "trust", "", "external (someone else runs it) or private (you do); managed providers are always external, self-hostable kinds must set one")
f.StringArrayVar(&settings, "config", nil, "adapter setting key=value (repeatable), e.g. apiURL=https://app.daytona.io/api")
f.StringVar(®ion, "region", "", "region label")
diff --git a/console/src/lib/providers.ts b/console/src/lib/providers.ts
index 5eb0823..45a198b 100644
--- a/console/src/lib/providers.ts
+++ b/console/src/lib/providers.ts
@@ -1,6 +1,9 @@
-// Provider kinds the console can add, with their main config fields. Kept in
-// step with the built-in dashboard (internal/dashboard/static/app.js); the full
-// list of keys per provider is in docs/providers/.
+// Provider kinds the console can add, with their main config fields: the kinds
+// in the default server build. Experimental kinds (a server built with
+// -tags experimental_adapters) are added with the CLI, the API or the config
+// file. Kept in step with the built-in dashboard
+// (internal/dashboard/static/app.js); the full list of keys per provider is in
+// docs/providers/.
export interface ProviderField { key: string; label: string; choices?: string[]; required?: boolean }
export interface ProviderKind { kind: string; label: string; trust: "external" | null; fields: ProviderField[] }
@@ -59,141 +62,6 @@ export const providerKinds: ProviderKind[] = [
}
]
},
- {
- "kind": "boat",
- "label": "Boat",
- "trust": "external",
- "fields": [
- {
- "key": "apiURL",
- "label": "API URL (default https://boat.dev/api/v1)"
- },
- {
- "key": "type",
- "label": "smallest size",
- "choices": [
- "small",
- "default",
- "large",
- "xlarge"
- ]
- },
- {
- "key": "snapshot",
- "label": "saved snapshot"
- },
- {
- "key": "environment",
- "label": "environment"
- },
- {
- "key": "org",
- "label": "organization"
- },
- {
- "key": "autoStopSeconds",
- "label": "auto-stop seconds for sandboxes without a TTL"
- },
- {
- "key": "portAccess",
- "label": "port URLs",
- "choices": [
- "token",
- "public"
- ]
- }
- ]
- },
- {
- "kind": "e2b",
- "label": "E2B",
- "trust": null,
- "fields": [
- {
- "key": "apiURL",
- "label": "API URL (default https://api.e2b.app)"
- },
- {
- "key": "domain",
- "label": "domain (default e2b.app)"
- },
- {
- "key": "template",
- "label": "template (default base)"
- },
- {
- "key": "maxLifetimeSeconds",
- "label": "plan's maximum runtime in seconds (default 3600)"
- },
- {
- "key": "ports",
- "label": "ports",
- "choices": [
- "private",
- "public"
- ]
- },
- {
- "key": "egress",
- "label": "egress",
- "choices": [
- "public",
- "none"
- ]
- }
- ]
- },
- {
- "kind": "morph",
- "label": "Morph Cloud",
- "trust": "external",
- "fields": [
- {
- "key": "apiURL",
- "label": "API URL (default https://cloud.morph.so/api)"
- },
- {
- "key": "snapshot",
- "label": "snapshot"
- },
- {
- "key": "image",
- "label": "image (default morphvm-minimal)"
- },
- {
- "key": "portAuth",
- "label": "port auth",
- "choices": [
- "none",
- "api_key"
- ]
- }
- ]
- },
- {
- "kind": "blaxel",
- "label": "Blaxel",
- "trust": "external",
- "fields": [
- {
- "key": "workspace",
- "label": "workspace",
- "required": true
- },
- {
- "key": "apiURL",
- "label": "API URL (default https://api.blaxel.ai/v0)"
- },
- {
- "key": "region",
- "label": "region (e.g. us-pdx-1)"
- },
- {
- "key": "image",
- "label": "image (default blaxel/base-image:latest)"
- }
- ]
- },
{
"kind": "opensandbox",
"label": "OpenSandbox",
@@ -277,278 +145,5 @@ export const providerKinds: ProviderKind[] = [
"required": true
}
]
- },
- {
- "kind": "vercel",
- "label": "Vercel Sandbox",
- "trust": "external",
- "fields": [
- {
- "key": "project",
- "label": "project ID or name",
- "required": true
- },
- {
- "key": "team",
- "label": "team ID"
- },
- {
- "key": "region",
- "label": "region (default iad1)"
- },
- {
- "key": "image",
- "label": "image"
- },
- {
- "key": "architecture",
- "label": "architecture",
- "choices": [
- "amd64",
- "arm64"
- ]
- },
- {
- "key": "ports",
- "label": "ports (comma separated)"
- },
- {
- "key": "maxTimeoutSeconds",
- "label": "max session seconds (default 2700)"
- }
- ]
- },
- {
- "kind": "fly",
- "label": "Fly.io Machines",
- "trust": "external",
- "fields": [
- {
- "key": "app",
- "label": "Fly app name",
- "required": true
- },
- {
- "key": "region",
- "label": "region"
- },
- {
- "key": "image",
- "label": "image"
- },
- {
- "key": "cpuKind",
- "label": "CPU kind",
- "choices": [
- "shared",
- "performance"
- ]
- },
- {
- "key": "suspend",
- "label": "suspend for pause",
- "choices": [
- "true",
- "false"
- ]
- }
- ]
- },
- {
- "kind": "agent-sandbox",
- "label": "agent-sandbox (Kubernetes)",
- "trust": null,
- "fields": [
- {
- "key": "namespace",
- "label": "namespace (default default)"
- },
- {
- "key": "apiServer",
- "label": "API server URL (default: in-cluster)"
- },
- {
- "key": "caFile",
- "label": "API server CA file on the server"
- },
- {
- "key": "image",
- "label": "image (default ubuntu:24.04)"
- },
- {
- "key": "warmPool",
- "label": "warm pool (SandboxWarmPool name)"
- },
- {
- "key": "runtimeClassName",
- "label": "RuntimeClass (e.g. gvisor)"
- },
- {
- "key": "isolation",
- "label": "isolation",
- "choices": [
- "container",
- "microvm"
- ]
- },
- {
- "key": "volumeSize",
- "label": "volume size, e.g. 10Gi (enables stop/start)"
- }
- ]
- },
- {
- "kind": "cubesandbox",
- "label": "CubeSandbox",
- "trust": null,
- "fields": [
- {
- "key": "apiURL",
- "label": "CubeAPI URL",
- "required": true
- },
- {
- "key": "templateId",
- "label": "template id"
- },
- {
- "key": "sandboxURL",
- "label": "CubeProxy URL (header routing)"
- },
- {
- "key": "domain",
- "label": "sandbox domain (default: CubeAPI's)"
- },
- {
- "key": "proxyScheme",
- "label": "proxy scheme",
- "choices": [
- "https",
- "http"
- ]
- }
- ]
- },
- {
- "kind": "aws-agentcore",
- "label": "AWS AgentCore Code Interpreter",
- "trust": null,
- "fields": [
- {
- "key": "region",
- "label": "AWS region",
- "required": true
- },
- {
- "key": "codeInterpreter",
- "label": "code interpreter (default aws.codeinterpreter.v1)"
- }
- ]
- },
- {
- "kind": "azure-sandboxes",
- "label": "Azure Container Apps Sandboxes",
- "trust": null,
- "fields": [
- {
- "key": "endpoint",
- "label": "data-plane endpoint",
- "required": true
- },
- {
- "key": "subscriptionId",
- "label": "subscription ID",
- "required": true
- },
- {
- "key": "resourceGroup",
- "label": "resource group",
- "required": true
- },
- {
- "key": "sandboxGroup",
- "label": "sandbox group",
- "required": true
- },
- {
- "key": "diskImage",
- "label": "public disk image"
- },
- {
- "key": "egress",
- "label": "egress",
- "choices": [
- "none",
- "public"
- ]
- }
- ]
- },
- {
- "kind": "azure-sessions",
- "label": "Azure dynamic sessions (Python)",
- "trust": null,
- "fields": [
- {
- "key": "endpoint",
- "label": "pool management endpoint",
- "required": true
- },
- {
- "key": "outputLimit",
- "label": "output limit (default 1048576)"
- }
- ]
- },
- {
- "kind": "gcp-sandboxes",
- "label": "Google Cloud Agent Platform sandboxes",
- "trust": null,
- "fields": [
- {
- "key": "project",
- "label": "project ID",
- "required": true
- },
- {
- "key": "location",
- "label": "location",
- "required": true
- },
- {
- "key": "reasoningEngine",
- "label": "engine ID",
- "required": true
- },
- {
- "key": "template",
- "label": "shell template ID",
- "required": true
- }
- ]
- },
- {
- "kind": "northflank",
- "label": "Northflank",
- "trust": null,
- "fields": [
- {
- "key": "projectId",
- "label": "project ID",
- "required": true
- },
- {
- "key": "teamId",
- "label": "team ID (organization tokens)"
- },
- {
- "key": "image",
- "label": "image (default ubuntu:24.04)"
- },
- {
- "key": "plan",
- "label": "plan (default nf-compute-200)"
- }
- ]
}
];
diff --git a/console/src/pages/providers.astro b/console/src/pages/providers.astro
index d82cf80..a1661d0 100644
--- a/console/src/pages/providers.astro
+++ b/console/src/pages/providers.astro
@@ -19,7 +19,7 @@ import { providerKinds } from '../lib/providers';
Provider
{providerKinds.map((k) => {k.label} )}
- Pool name
+ Pool name
Trust
External: someone else runs it Private: you run it
diff --git a/deploy/helm/differently/templates/_helpers.tpl b/deploy/helm/differently/templates/_helpers.tpl
index 0a783f3..5d0a722 100644
--- a/deploy/helm/differently/templates/_helpers.tpl
+++ b/deploy/helm/differently/templates/_helpers.tpl
@@ -397,6 +397,9 @@ Validation. Fails the render with an actionable message. The server's
{{- if eq .adapter "devhost" -}}
{{- fail (printf "Pool %q: the devhost adapter has no isolation and is not supported by this chart." .name) -}}
{{- end -}}
+{{- if and (has .adapter (list "vercel" "fly" "agent-sandbox" "cubesandbox" "aws-agentcore" "azure-sandboxes" "azure-sessions" "gcp-sandboxes" "northflank" "boat" "e2b" "morph" "blaxel")) (not $.Values.server.experimentalAdapters) -}}
+{{- fail (printf "Pool %q uses the experimental %s adapter, which the default differently-server image does not contain. Build an image with -tags experimental_adapters, point server.image at it and set server.experimentalAdapters=true." .name .adapter) -}}
+{{- end -}}
{{- if eq .adapter "opensandbox" -}}
{{- if not (has (.trust | default "") (list "private" "external")) -}}
{{- fail (printf "Pool %q uses the opensandbox adapter: set trust to private (you operate the server) or external (someone else does)." .name) -}}
diff --git a/deploy/helm/differently/values.schema.json b/deploy/helm/differently/values.schema.json
index a126999..651bae4 100644
--- a/deploy/helm/differently/values.schema.json
+++ b/deploy/helm/differently/values.schema.json
@@ -62,7 +62,7 @@
"required": ["name", "adapter"],
"properties": {
"name": {"type": "string", "minLength": 1},
- "adapter": {"type": "string", "enum": ["opensandbox", "remote", "daytona", "freestyle", "fake", "vercel", "fly", "agent-sandbox", "cubesandbox", "aws-agentcore", "azure-sandboxes", "azure-sessions", "gcp-sandboxes", "northflank", "boat", "e2b", "morph", "blaxel"]},
+ "adapter": {"type": "string", "description": "opensandbox, remote, daytona and freestyle are in the default image; the other kinds need server.experimentalAdapters and an image built with -tags experimental_adapters.", "enum": ["opensandbox", "remote", "daytona", "freestyle", "fake", "vercel", "fly", "agent-sandbox", "cubesandbox", "aws-agentcore", "azure-sandboxes", "azure-sessions", "gcp-sandboxes", "northflank", "boat", "e2b", "morph", "blaxel"]},
"trust": {"type": "string", "enum": ["private", "external"]},
"region": {"type": "string"},
"labels": {"$ref": "#/definitions/stringMap"},
@@ -437,6 +437,7 @@
"additionalProperties": false,
"properties": {
"image": {"$ref": "#/definitions/image"},
+ "experimentalAdapters": {"type": "boolean", "description": "Set when server.image was built with -tags experimental_adapters; allows pools of the experimental adapter kinds."},
"replicas": {"type": "integer", "minimum": 0},
"containerPort": {"type": "integer", "minimum": 1, "maximum": 65535},
"preflight": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}}},
diff --git a/deploy/helm/differently/values.yaml b/deploy/helm/differently/values.yaml
index 1862942..668a648 100644
--- a/deploy/helm/differently/values.yaml
+++ b/deploy/helm/differently/values.yaml
@@ -394,6 +394,12 @@ server:
tag: ""
digest: ""
pullPolicy: IfNotPresent
+ # The default image holds the opensandbox, daytona, freestyle and remote
+ # adapters. Every other kind is experimental and needs an image built with
+ # `go build -tags experimental_adapters` (docs/providers/README.md): point
+ # server.image at that image and set this to true, or the chart refuses
+ # pools of those kinds.
+ experimentalAdapters: false
# Must be 1 with config.store.driver=memory.
replicas: 1
containerPort: 8080
diff --git a/docs/adapters/specification.md b/docs/adapters/specification.md
index f0d1a05..36f1a90 100644
--- a/docs/adapters/specification.md
+++ b/docs/adapters/specification.md
@@ -7,9 +7,10 @@
An **execution adapter** creates and manages sandbox instances on one backend.
The control plane holds one adapter per [pool](../architecture/README.md#4-the-three-layers).
-Adapters run in-process (16 provider adapters, listed in the
-[provider catalog](../providers/README.md), plus the development-only
-`devhost` and `fake`) or out of process behind the HTTP protocol in
+Adapters run in-process (`opensandbox`, `daytona` and `freestyle` in every
+build, 13 experimental provider adapters behind the `experimental_adapters`
+build tag, all listed in the [provider catalog](../providers/README.md),
+plus the development-only `devhost` and `fake`) or out of process behind the HTTP protocol in
`pkg/adapter/adapterhttp`, which `remote` pools use. The
default private runtime is an OpenSandbox server behind the `opensandbox`
adapter ([OpenSandbox pools](../guides/opensandbox-pools.md)).
diff --git a/docs/architecture/README.md b/docs/architecture/README.md
index d73f45c..b377d0d 100644
--- a/docs/architecture/README.md
+++ b/docs/architecture/README.md
@@ -3,7 +3,7 @@
| | |
|---|---|
| Status | As built, documented from the code on 2026-10-28, after the move to OpenSandbox removed Differently's own Firecracker node agent |
-| Scope | Control plane (`differently-server`), CLI, built-in adapters (16 provider adapters in the [provider catalog](../providers/README.md), `remote`, and the development-only `devhost` and `fake`), storage, HA, networking |
+| Scope | Control plane (`differently-server`), CLI, adapters (in the default build: `opensandbox`, `daytona`, `freestyle`, `remote`, and the development-only `devhost` and `fake`; 13 experimental provider adapters behind the `experimental_adapters` build tag, listed in the [provider catalog](../providers/README.md)), storage, HA, networking |
| Related | [Trust model](../security/trust-model.md) · [Adapter specification](../adapters/specification.md) · [Placement policy](../scheduling/placement-policy.md) · [Storage and lifecycle](../storage-and-lifecycle.md) · [OpenSandbox pools](../guides/opensandbox-pools.md) |
## 1. Summary
@@ -15,8 +15,9 @@ installation runs an [OpenSandbox](https://github.com/opensandbox-group/OpenSand
server with the runtime that fits (Docker or Kubernetes containers, or
Firecracker microVMs with Fast Sandbox) and registers it as a pool. When
policy explicitly allows it, work can also go to an external provider such as
-E2B, Daytona or a cloud provider's sandbox service, or to any backend behind a
-`remote` plugin ([provider catalog](../providers/README.md)).
+Daytona or Freestyle, or to any backend behind a `remote` plugin. Adapters
+for more providers are experimental and outside the default build
+([provider catalog](../providers/README.md)).
Applications never name a backend. They ask for resources, capabilities and
a data classification; the policy engine picks a pool.
@@ -46,7 +47,7 @@ proven in production".
| `opensandbox` adapter | Fake-server tests that cover the differences found between OpenSandbox's spec and its images (`internal/adapters/opensandbox/opensandbox_test.go`). Real Docker and Kubernetes responses recorded in `testdata/recordings` and replayed in every `go test`. `TestLiveOpenSandbox` (build tag `live`) passes against OpenSandbox release 1.2.0 on the Docker runtime (Colima, arm64) and the Kubernetes runtime (k3s on Azure) | The Fast Sandbox runtime (`runtime: fastsandbox`) is tested only with a fake server, so microVM isolation through Differently is unverified ([backlog E18](../backlog.md#e18-fast-sandbox-spike)) |
| End-to-end suite (`test/e2e`) | Postgres, `differently-server` and a real OpenSandbox 1.2.0 server: 14/14 on the Docker runtime (arm64 Colima) and on the Kubernetes runtime (single-node k3s on Azure, x86_64, with a snapshot registry, signed ports and egress policy), 2026-10-29 ([results](../../test/e2e/README.md)) | Fast Sandbox; multi-node clusters and node drains |
| Helm chart | `helm lint` and `helm template` pass for every CI value set; installed on single-node k3s on Azure next to OpenSandbox's charts, and the published `v0.1.0-alpha.1` chart installed from public artifacts alone ([single VM](../deploy/single-vm.md)) | Managed EKS, GKE and AKS; multi-node clusters |
-| Other provider adapters | `agent-sandbox` ran live against a kind cluster. The other 14 (Daytona, Freestyle, E2B, Vercel, the AWS, Azure and Google Cloud services and more) are written against each provider's published API and tested with fake HTTP servers ([provider catalog](../providers/README.md)) | No live-account verification ([POC 7](../proof-of-concepts.md#poc-7-external-provider-live-account-verification)) |
+| Other provider adapters | Daytona and Freestyle ran against live accounts and are in the default build. The other 13 are experimental, compiled in only with the `experimental_adapters` build tag: `agent-sandbox` ran live against a kind cluster; the other 12 (E2B, Vercel, the AWS, Azure and Google Cloud services and more) are written against each provider's published API and tested with fake HTTP servers ([provider catalog](../providers/README.md)) | No live-account verification of those 12 ([POC 7](../proof-of-concepts.md#poc-7-external-provider-live-account-verification)) |
| `remote` plugin protocol (`pkg/adapter/adapterhttp`) | Round-trip tests of client and handler against the fake adapter (`pkg/adapter/adapterhttp/adapterhttp_test.go`) | A conformance suite for third-party plugins ([backlog E0](../backlog.md#e0-correctness-and-security-fixes-from-the-code-review), E0.34) |
## 3. Goals and non-goals
diff --git a/docs/assets/differently-splash-dark.svg b/docs/assets/differently-splash-dark.svg
index e0d9a2c..c00598b 100644
--- a/docs/assets/differently-splash-dark.svg
+++ b/docs/assets/differently-splash-dark.svg
@@ -1,6 +1,6 @@
Differently: the control plane for your sandboxes
-Apps and agents call one Differently API. Differently handles tenancy, quotas, policy placement and audit, and places each sandbox on OpenSandbox or your own plugin inside your infrastructure, or on a managed provider such as E2B or Daytona only when policy allows.
+Apps and agents call one Differently API. Differently handles tenancy, quotas, policy placement and audit, and places each sandbox on OpenSandbox or your own plugin inside your infrastructure, or on a managed provider such as Daytona or Freestyle only when policy allows.
@@ -32,7 +32,7 @@
Managed providers
-E2B · Daytona · Vercel · cloud sandboxes
+Daytona · Freestyle · adapter plugins
only if allowed
fail-closed, by policy
diff --git a/docs/assets/differently-splash-light.svg b/docs/assets/differently-splash-light.svg
index 58a0f52..635bc6d 100644
--- a/docs/assets/differently-splash-light.svg
+++ b/docs/assets/differently-splash-light.svg
@@ -1,6 +1,6 @@
Differently: the control plane for your sandboxes
-Apps and agents call one Differently API. Differently handles tenancy, quotas, policy placement and audit, and places each sandbox on OpenSandbox or your own plugin inside your infrastructure, or on a managed provider such as E2B or Daytona only when policy allows.
+Apps and agents call one Differently API. Differently handles tenancy, quotas, policy placement and audit, and places each sandbox on OpenSandbox or your own plugin inside your infrastructure, or on a managed provider such as Daytona or Freestyle only when policy allows.
@@ -32,7 +32,7 @@
Managed providers
-E2B · Daytona · Vercel · cloud sandboxes
+Daytona · Freestyle · adapter plugins
only if allowed
fail-closed, by policy
diff --git a/docs/deploy/README.md b/docs/deploy/README.md
index 103d06a..68c600d 100644
--- a/docs/deploy/README.md
+++ b/docs/deploy/README.md
@@ -34,9 +34,11 @@ Pools are where sandboxes run:
- **`opensandbox`:** an OpenSandbox server. `trust` must be declared:
`private` if you operate the server and its runtime, `external` if someone
else does.
-- **Managed providers** (`daytona`, `freestyle`, `e2b`, `vercel` and the
- others in the [provider catalog](../providers/README.md)): optional
- capacity someone else runs. Managed kinds are always `trust=external`, and
+- **Managed providers** (`daytona` and `freestyle`; adapters for others,
+ such as `e2b` and `vercel`, are experimental and need a server built with
+ `-tags experimental_adapters`, see the
+ [provider catalog](../providers/README.md#what-the-server-contains)):
+ optional capacity someone else runs. Managed kinds are always `trust=external`, and
sandboxes are placed on them only when policy allows it.
- **`remote`:** an out-of-process adapter plugin speaking the
[adapter protocol](../adapters/specification.md). Its trust is the
diff --git a/docs/feature-parity.md b/docs/feature-parity.md
index f3ae3d1..ba6cc4f 100644
--- a/docs/feature-parity.md
+++ b/docs/feature-parity.md
@@ -144,9 +144,13 @@ Boat is the developer-experience reference. Boat paths are relative to
## 3. Backend capability matrix
-What each Differently adapter advertises today (from the adapters' code). A
-request that needs a capability is only placed on pools that advertise it; an
-operation a backend lacks returns `unsupported_operation` (501).
+What each adapter in the default build advertises today (from the adapters'
+code). A request that needs a capability is only placed on pools that
+advertise it; an operation a backend lacks returns `unsupported_operation`
+(501). The 13 experimental adapters, compiled in only with the
+`experimental_adapters` build tag and, apart from `agent-sandbox`, tested
+only against fake servers, are in the
+[provider catalog](providers/README.md#what-the-server-contains).
| Capability | `opensandbox` (default private runtime) | `daytona` | `freestyle` | `remote` (plugin) | `devhost` (dev only) |
|---|---|---|---|---|---|
diff --git a/docs/maintaining.md b/docs/maintaining.md
index 4252f41..1c4e096 100644
--- a/docs/maintaining.md
+++ b/docs/maintaining.md
@@ -24,7 +24,7 @@ control plane does with it.
| Quotas, admission, activation | `internal/store` (memory and Postgres) | `internal/store/storetest` through both stores; `make test-postgres` |
| Reconciliation and recovery | `internal/scheduler` (`Reconcile`, `retry`) | Server tests using the fake backend; the e2e drift test |
| A backend's behaviour | `internal/adapters/` | Its fake-server tests; for OpenSandbox also the recordings ([below](#recordings-real-backends-replayed-in-go-test)) |
-| A new provider kind | The adapter, then every registration list in [the provider catalog's checklist](providers/README.md#add-a-built-in-adapter) | `TestEveryAdapterKindHasATrustRule`; its fake-server tests |
+| A new provider kind | The adapter, then every registration list in [the provider catalog's checklist](providers/README.md#add-a-built-in-adapter) | `TestEveryAdapterKindHasATrustRule`, `TestRegisteredKindsMatchConfig`; its fake-server tests, which run only with `-tags experimental_adapters` (`make experimental`) |
| The adapter contract | `pkg/adapter`, `pkg/adapter/adapterhttp`, `internal/adapters/fake` | `adapterhttp` round-trip tests; [adapter specification](adapters/specification.md) |
| Stored credentials and keys | `internal/secrets`, `internal/server` | [keys-and-providers.md](guides/keys-and-providers.md) lists what is verified |
| Install and operations | `deploy/helm/differently` (values, schema, templates, NOTES), `cmd/differently-server` (`preflight`, `migrate`) | `make helm-lint`; the ci value sets; the deploy guides |
@@ -52,7 +52,7 @@ the one before; repeating a rung for reassurance adds nothing.
| Rung | Command | Use it for |
|---|---|---|
| Package tests | `go test ./internal/adapters/opensandbox/` (or the package you changed) | Every change, while iterating |
-| CI locally | `make verify` | Before every push: gofmt, vet, tests with `-race`, SDK tests, Helm lint |
+| CI locally | `make verify` | Before every push: gofmt, vet, tests with `-race`, the same again with the experimental adapters (`make experimental`: build, vet and test with `-tags experimental_adapters`), SDK tests, Helm lint |
| Postgres store | `make test-postgres` (needs Docker) | Changes in `internal/store` |
| Recorded runtimes | part of `go test` (`TestReplayRecordings`) | Any OpenSandbox adapter change: real responses from the Docker and Kubernetes runtimes, no server |
| Live adapter | `OPENSANDBOX_URL=… go test -tags live -run TestLiveOpenSandbox ./internal/adapters/opensandbox/`; for the managed providers `DAYTONA_API_KEY=… go test -tags live -run TestLiveDaytona ./internal/adapters/daytona/` and `FREESTYLE_API_KEY=… go test -tags live -run TestLiveFreestyle ./internal/adapters/freestyle/` (a few small sandboxes, all deleted) | A new OpenSandbox call or version; re-recording; any Daytona or Freestyle adapter change |
diff --git a/docs/providers/README.md b/docs/providers/README.md
index 6a6194c..b5bfceb 100644
--- a/docs/providers/README.md
+++ b/docs/providers/README.md
@@ -20,20 +20,52 @@ code and published documentation.
## Providers
-26 providers are covered: 16 with a built-in adapter, 8 reachable through a
-[remote plugin](remote.md), and 2 that cannot be supported. Four of the 16
-have run live; the rest are implemented from published APIs and tested
-against fakes of them. "Persists" means stop/start keeps the disk or pause
-keeps memory; "Snapshots" are disk unless marked memory.
+26 providers are covered: 16 with an adapter in this repository, 8 reachable
+through a [remote plugin](remote.md), and 2 that cannot be supported.
+
+### What the server contains
+
+The published image, the release binaries and `make build` contain three
+provider adapters, each verified against a real backend: `opensandbox`,
+`daytona` and `freestyle`. They also contain [`remote`](remote.md), the
+plugin protocol, and the dev-mode `devhost`.
+
+The other 13 adapters are **experimental**. Their code and tests are in the
+repository and CI builds and tests them, but they are compiled into the
+server only with the Go build tag `experimental_adapters`. Twelve have been
+tested only against fake servers; `agent-sandbox` also ran live once, on a
+kind cluster. A server built without the tag refuses a pool of one of these
+kinds, in the config file and through the API, with an error that names the
+tag.
+
+To use an experimental adapter, build the server yourself:
+
+```sh
+go build -tags experimental_adapters ./cmd/differently-server
+# or the image:
+docker build -f build/server.Dockerfile --build-arg GO_TAGS=experimental_adapters \
+ -t registry.example.com/differently-server:experimental .
+```
+
+With the Helm chart, point `server.image` at that image and set
+`server.experimentalAdapters=true`; otherwise the chart refuses pools of
+these kinds. The dashboard and the console offer only the default kinds, so
+add an experimental pool in the config file, with
+`differently admin pool add` or with `POST /v1/admin/pools`.
-### Built-in adapters
+### In the default build
| Provider | Status | Trust |
|---|---|---|
| [OpenSandbox](opensandbox.md) `opensandbox` | **Verified live**: Docker and Kubernetes runtimes, 2026-10-29 | declared |
-| [agent-sandbox](agent-sandbox.md) `agent-sandbox` | **Verified live**: kind cluster, runc, 2026-10-28 | declared |
| [Daytona](daytona.md) `daytona` | **Verified live**: container class, 2026-10-29 (VM class not run) | external |
| [Freestyle](freestyle.md) `freestyle` | **Verified live**: default snapshot, 2026-10-29 | external |
+
+### Experimental: build tag `experimental_adapters`
+
+| Provider | Status | Trust |
+|---|---|---|
+| [agent-sandbox](agent-sandbox.md) `agent-sandbox` | **Verified live**: kind cluster, runc, 2026-10-28 | declared |
| [E2B](e2b.md) `e2b` | Tested with a fake server | declared |
| [Boat](boat.md) `boat` | Tested with a fake server | external |
| [Morph Cloud](morph.md) `morph` | Tested with a fake server | external |
@@ -57,26 +89,27 @@ Cloud, Northflank BYOC).
"Keeps state" is stop/start that keeps the disk, or pause that keeps memory.
"Snapshots" is what a snapshot captures; "+ fork" means a new sandbox can
-start from one.
-
-| Provider | Isolation | Keeps state | Snapshots | Port URLs |
-|---|---|---|---|---|
-| OpenSandbox | container or microVM | stop/start or pause, by runtime | disk + fork | signed |
-| agent-sandbox | RuntimeClass | stop/start (volume) | — | in-cluster |
-| Daytona | container or VM | stop/start; pause on VMs | disk + fork (memory on VMs) | signed |
-| Freestyle | microVM | stop/start, pause | memory + fork | public |
-| E2B | microVM | pause | memory + fork | public, opt-in |
-| Boat | VM | stop/start | disk + fork | token or public |
-| Morph Cloud | VM | pause | memory + fork | public |
-| Vercel Sandbox | microVM | stop/start | disk + fork | public |
-| Fly.io Machines | microVM | stop/start, suspend | — | — |
-| Blaxel | microVM | auto standby | — | token |
-| CubeSandbox | microVM | pause | memory + fork | opt-in |
-| AWS AgentCore | microVM | — | — | — |
-| Azure Sandboxes | managed | stop/start | disk + fork | public |
-| Azure sessions | Hyper-V | — | — | — |
-| Google Cloud sandboxes | container | stop/start | memory + fork | — |
-| Northflank | microVM or gVisor | — | — | public |
+start from one. "Build" is `default` for the adapters in every server and
+`tag` for the experimental ones.
+
+| Provider | Build | Isolation | Keeps state | Snapshots | Port URLs |
+|---|---|---|---|---|---|
+| OpenSandbox | default | container or microVM | stop/start or pause, by runtime | disk + fork | signed |
+| agent-sandbox | tag | RuntimeClass | stop/start (volume) | — | in-cluster |
+| Daytona | default | container or VM | stop/start; pause on VMs | disk + fork (memory on VMs) | signed |
+| Freestyle | default | microVM | stop/start, pause | memory + fork | public |
+| E2B | tag | microVM | pause | memory + fork | public, opt-in |
+| Boat | tag | VM | stop/start | disk + fork | token or public |
+| Morph Cloud | tag | VM | pause | memory + fork | public |
+| Vercel Sandbox | tag | microVM | stop/start | disk + fork | public |
+| Fly.io Machines | tag | microVM | stop/start, suspend | — | — |
+| Blaxel | tag | microVM | auto standby | — | token |
+| CubeSandbox | tag | microVM | pause | memory + fork | opt-in |
+| AWS AgentCore | tag | microVM | — | — | — |
+| Azure Sandboxes | tag | managed | stop/start | disk + fork | public |
+| Azure sessions | tag | Hyper-V | — | — | — |
+| Google Cloud sandboxes | tag | container | stop/start | memory + fork | — |
+| Northflank | tag | microVM or gVisor | — | — | public |
Every adapter offers exec and files. Streamed exec, stdin, egress control and
size limits differ: each page has the full table.
@@ -132,26 +165,38 @@ would wrap.
image or request, `insufficient_capacity` for 429 and capacity errors, and
`provider_unavailable` for outages and rejected credentials. Advertise
only features that work.
-2. Register it:
- - the factory in `internal/wiring/wiring.go`;
- - `KnownAdapters` in `internal/config/config.go`, and **exactly one**
- trust list there: `externalKinds` for a managed provider (its pools are
- always `external`), or `declaredTrustKinds` for a self-hostable runtime
- (its pools must say who operates it). `TestEveryAdapterKindHasATrustRule`
- fails until you choose, so a new kind can never be treated as private
- by omission;
- - the adapter enum in `deploy/helm/differently/values.schema.json`;
- - the `--adapter` help in `cmd/differently/main.go`, the `kinds` table
- in `internal/dashboard/static/app.js` and `console/src/lib/providers.ts`,
- so operators can add it without a config file.
-
- Adapters written in parallel all edit these lists, so their PRs conflict
- with each other. The resolution is always the union of both sides.
+2. Register it. A new adapter starts as experimental, behind the build tag:
+ - `//go:build experimental_adapters` as the first line of every file in
+ the package, tests included;
+ - the factory in `internal/wiring/wiring_experimental.go`;
+ - `ExperimentalAdapters` in `internal/config/config.go`, and **exactly
+ one** trust list there: `externalKinds` for a managed provider (its
+ pools are always `external`), or `declaredTrustKinds` for a
+ self-hostable runtime (its pools must say who operates it).
+ `TestEveryAdapterKindHasATrustRule` fails until you choose, so a new
+ kind can never be treated as private by omission, and
+ `TestRegisteredKindsMatchConfig` fails if the factory and the list
+ disagree;
+ - the adapter enum in `deploy/helm/differently/values.schema.json` and
+ the experimental list in the chart's `differently.validate` helper
+ (`templates/_helpers.tpl`).
+
+ `make experimental` builds, vets and tests with the tag; `make verify`
+ and CI run it. Adapters written in parallel all edit these lists, so
+ their PRs conflict with each other. The resolution is always the union
+ of both sides.
+
+ Moving an adapter into the default build is a separate change and a
+ maintainers' decision; it needs **Verified live** status first. Drop the
+ build tag, move the factory to `internal/wiring/wiring.go` and the kind to
+ `DefaultAdapters`, and add it to the `--adapter` help in
+ `cmd/differently/main.go`, the `kinds` table in
+ `internal/dashboard/static/app.js` and `console/src/lib/providers.ts`.
3. Write `docs/providers/.md` from the template below, and add a row
here.
4. When you have an account or a running install, run the adapter's live
- test (`go test -tags live`, as `agent-sandbox`, `daytona` and `freestyle`
- have) and drive a pool on it through a Differently server, or run the
+ test (`go test -tags live,experimental_adapters`; `daytona` and
+ `freestyle` have one to follow) and drive a pool on it through a Differently server, or run the
[end-to-end suite](../../test/e2e/README.md) against it where it fits.
Record what ran on the page. Until then, the status stays
**Tested with a fake server**.
@@ -187,7 +232,7 @@ sandboxes go with `differently admin pool use `.
| | |
|---|---|
| Status | one of the four statuses, with the date and what ran |
-| Adapter | `` (`internal/adapters/`), or none |
+| Adapter | `` (`internal/adapters/`), or none; say if it is experimental (build tag) |
| Trust | external (provider-managed) or declared (self-hostable) |
| Isolation | what the provider documents (microVM, gVisor, container…) |
| API checked | docs or OpenAPI URL, and the date read |
diff --git a/docs/providers/agent-sandbox.md b/docs/providers/agent-sandbox.md
index eca9c90..e114f2e 100644
--- a/docs/providers/agent-sandbox.md
+++ b/docs/providers/agent-sandbox.md
@@ -1,5 +1,10 @@
# agent-sandbox (Kubernetes)
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Verified live** (2026-10-28): the adapter's live test and a Differently server, against agent-sandbox v1.0.8 on a single-node kind cluster (Kubernetes 1.37, runc, arm64) |
@@ -192,7 +197,8 @@ template and rejects `image`, `runtimeClassName`, `volumeSize`,
lists, exec arguments and exit codes, stdin, streaming, files, stop/start,
warm-pool claims, token rotation and that credentials are not echoed.
`kube_test.go` covers the WebSocket framing.
-- **Live** (2026-10-28, `go test -tags live -run TestLiveAgentSandbox`):
+- **Live** (2026-10-28, `go test -tags live -run TestLiveAgentSandbox`; the
+ test now also needs `-tags live,experimental_adapters`):
agent-sandbox v1.0.8 (`registry.k8s.io/agent-sandbox/agent-sandbox-controller:v1.0.8`,
`sandbox-with-extensions.yaml`) on kind v0.33.0 (Kubernetes 1.37.0,
containerd 2.3.4, runc) on an arm64 Colima VM, with a service account
diff --git a/docs/providers/aws-agentcore.md b/docs/providers/aws-agentcore.md
index b7b749f..9174a4f 100644
--- a/docs/providers/aws-agentcore.md
+++ b/docs/providers/aws-agentcore.md
@@ -1,5 +1,10 @@
# Amazon Bedrock AgentCore Code Interpreter
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (implemented from the published API, not run against AWS) |
diff --git a/docs/providers/azure-sandboxes.md b/docs/providers/azure-sandboxes.md
index fda74ec..073ba9d 100644
--- a/docs/providers/azure-sandboxes.md
+++ b/docs/providers/azure-sandboxes.md
@@ -1,5 +1,10 @@
# Azure Container Apps Sandboxes
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (implemented from the published API, not run against Azure) |
diff --git a/docs/providers/azure-sessions.md b/docs/providers/azure-sessions.md
index 20536e0..5b432f2 100644
--- a/docs/providers/azure-sessions.md
+++ b/docs/providers/azure-sessions.md
@@ -1,5 +1,10 @@
# Azure Container Apps dynamic sessions
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** for Python code-interpreter pools (implemented from the published API, not run against Azure). Custom container and shell pools: **Not supported** by this adapter (see [below](#custom-container-and-shell-pools)) |
diff --git a/docs/providers/blaxel.md b/docs/providers/blaxel.md
index 63951ec..2de063a 100644
--- a/docs/providers/blaxel.md
+++ b/docs/providers/blaxel.md
@@ -1,5 +1,10 @@
# Blaxel
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (implemented from Blaxel's published OpenAPI specs, not run against Blaxel) |
diff --git a/docs/providers/boat.md b/docs/providers/boat.md
index c71a703..21dc735 100644
--- a/docs/providers/boat.md
+++ b/docs/providers/boat.md
@@ -1,5 +1,10 @@
# Boat
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (2026-10-28): implemented from Boat's published API, not run against Boat |
diff --git a/docs/providers/cubesandbox.md b/docs/providers/cubesandbox.md
index 31fe49c..f926502 100644
--- a/docs/providers/cubesandbox.md
+++ b/docs/providers/cubesandbox.md
@@ -1,5 +1,10 @@
# CubeSandbox
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (implemented from CubeSandbox's API source and OpenAPI document, not run against CubeSandbox; see [How it was tested](#how-it-was-tested)) |
@@ -122,8 +127,8 @@ With Differently's Helm chart, server egress is restricted by default
field (proto3 JSON), a missing binary (127), a timeout (124), stdin,
streaming, files, host and path port URLs, health with a missing template
and a rejected key.
-- **Live:** not run. `live_test.go` (`go test -tags live -run
- TestLiveCubeSandbox`, settings in the file) is ready for a host. The
+- **Live:** not run. `live_test.go` (`go test -tags live,experimental_adapters
+ -run TestLiveCubeSandbox`, settings in the file) is ready for a host. The
one-click install needs a dedicated Linux host with `/dev/kvm`, at least
7.5 GiB of memory (its preflight), XFS at `/data/cubelet`, about 50 GB of
disk, root and Docker. On arm64 the bundle is installed by hand, and
diff --git a/docs/providers/e2b.md b/docs/providers/e2b.md
index 36bdaf6..25d601b 100644
--- a/docs/providers/e2b.md
+++ b/docs/providers/e2b.md
@@ -1,5 +1,10 @@
# E2B
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (2026-10-28): implemented from E2B's published API specs, not run against E2B |
diff --git a/docs/providers/fly.md b/docs/providers/fly.md
index 66b28a2..0f35ba8 100644
--- a/docs/providers/fly.md
+++ b/docs/providers/fly.md
@@ -1,5 +1,10 @@
# Fly.io Machines
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server**: implemented from the Machines API OpenAPI document, not run against a Fly.io account |
diff --git a/docs/providers/gcp-sandboxes.md b/docs/providers/gcp-sandboxes.md
index 75d117a..a092b58 100644
--- a/docs/providers/gcp-sandboxes.md
+++ b/docs/providers/gcp-sandboxes.md
@@ -1,5 +1,10 @@
# Google Cloud Agent Platform sandboxes
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (implemented from the published API, not run against Google Cloud) |
diff --git a/docs/providers/morph.md b/docs/providers/morph.md
index 97289e5..398cfa1 100644
--- a/docs/providers/morph.md
+++ b/docs/providers/morph.md
@@ -1,5 +1,10 @@
# Morph Cloud
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (2026-10-28): implemented from Morph Cloud's published API, not run against Morph Cloud |
diff --git a/docs/providers/northflank.md b/docs/providers/northflank.md
index 0fb2275..edbe0bc 100644
--- a/docs/providers/northflank.md
+++ b/docs/providers/northflank.md
@@ -1,5 +1,10 @@
# Northflank
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server** (implemented from the published API, not run against Northflank) |
diff --git a/docs/providers/vercel.md b/docs/providers/vercel.md
index ac1ba69..304860d 100644
--- a/docs/providers/vercel.md
+++ b/docs/providers/vercel.md
@@ -1,5 +1,10 @@
# Vercel Sandbox
+> **Experimental adapter.** It is not in the default `differently-server`
+> build or the published image. It needs a server built with
+> `-tags experimental_adapters`: see
+> [what the server contains](README.md#what-the-server-contains).
+
| | |
|---|---|
| Status | **Tested with a fake server**: implemented from Vercel's REST API reference and OpenAPI document, not run against a Vercel account |
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 08843d5..e0a2c7c 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -3,7 +3,7 @@
| | |
|---|---|
| Status date | 2026-10-29 |
-| Evidence rule | **Done** means implemented and covered by automated tests, most of which run in-process with fakes. **Validated** means run against real infrastructure or live accounts: so far the `opensandbox` adapter on OpenSandbox's Docker, Kubernetes and Fast Sandbox runtimes, `agent-sandbox` on kind, and Daytona and Freestyle on live accounts. |
+| Evidence rule | **Done** means implemented and covered by automated tests, most of which run in-process with fakes. **Validated** means run against real infrastructure or live accounts: so far the `opensandbox` adapter on OpenSandbox's Docker, Kubernetes and Fast Sandbox runtimes, `agent-sandbox` on kind, and Daytona and Freestyle on live accounts. The default build holds only the validated `opensandbox`, `daytona` and `freestyle` adapters (plus `remote` and `devhost`); the other 13 are experimental and need the `experimental_adapters` build tag ([provider catalog](providers/README.md#what-the-server-contains)). |
| Detail | Tasks and acceptance criteria in [backlog.md](backlog.md); experiments in [proof-of-concepts.md](proof-of-concepts.md) |
| Runtime change | Differently's own Firecracker node agent was removed; OpenSandbox is the default private runtime. |
@@ -99,7 +99,7 @@ an operator-visible record of what left private infrastructure.
| OpenSandbox as an external pool | ✅ | `trust: external` pools follow the usual approval rules; trust must be declared |
| HTTP plugin protocol and `remote` pools | 🟡 | `pkg/adapter/adapterhttp`; untested since the agent tests, its only coverage, were removed ([E0.34](backlog.md#e0-correctness-and-security-fixes-from-the-code-review)); no protocol-version check yet |
| Usage and cost estimates | ✅ | `GET /v1/usage`; estimates only, no invoice reconciliation |
-| **Live-account verification** | 🟡 | Daytona and Freestyle done; the other 12 provider adapters are tested against fakes of their published APIs ([POC 7](proof-of-concepts.md#poc-7-external-provider-live-account-verification), [E2](backlog.md#e2-external-adapters-on-live-accounts)) |
+| **Live-account verification** | 🟡 | Daytona and Freestyle done; the other 12 provider adapters are tested against fakes of their published APIs and are outside the default build (`experimental_adapters` tag) ([POC 7](proof-of-concepts.md#poc-7-external-provider-live-account-verification), [E2](backlog.md#e2-external-adapters-on-live-accounts)) |
**Exit criteria.** POC 7 complete; Daytona and Freestyle rows in
[feature-parity.md](feature-parity.md) moved from 🧪 to ✅. The second part
diff --git a/internal/adapters/agentcore/agentcore.go b/internal/adapters/agentcore/agentcore.go
index 8a625d8..49fb693 100644
--- a/internal/adapters/agentcore/agentcore.go
+++ b/internal/adapters/agentcore/agentcore.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package agentcore adapts Amazon Bedrock AgentCore Code Interpreter: each
// Differently sandbox is one code-interpreter session.
//
diff --git a/internal/adapters/agentcore/agentcore_test.go b/internal/adapters/agentcore/agentcore_test.go
index 08fe144..89cb30b 100644
--- a/internal/adapters/agentcore/agentcore_test.go
+++ b/internal/adapters/agentcore/agentcore_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package agentcore
import (
diff --git a/internal/adapters/agentsandbox/agentsandbox.go b/internal/adapters/agentsandbox/agentsandbox.go
index 03c19d9..15eaec5 100644
--- a/internal/adapters/agentsandbox/agentsandbox.go
+++ b/internal/adapters/agentsandbox/agentsandbox.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package agentsandbox adapts kubernetes-sigs/agent-sandbox
// (github.com/kubernetes-sigs/agent-sandbox, Apache-2.0): a Sandbox custom
// resource that a controller turns into one stateful pod, with optional warm
diff --git a/internal/adapters/agentsandbox/agentsandbox_test.go b/internal/adapters/agentsandbox/agentsandbox_test.go
index ca1e3ca..cecf2f8 100644
--- a/internal/adapters/agentsandbox/agentsandbox_test.go
+++ b/internal/adapters/agentsandbox/agentsandbox_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package agentsandbox
import (
diff --git a/internal/adapters/agentsandbox/kube.go b/internal/adapters/agentsandbox/kube.go
index 8a6e2cc..4b41a7c 100644
--- a/internal/adapters/agentsandbox/kube.go
+++ b/internal/adapters/agentsandbox/kube.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package agentsandbox
// A small Kubernetes API client on the standard library: JSON requests with
diff --git a/internal/adapters/agentsandbox/kube_test.go b/internal/adapters/agentsandbox/kube_test.go
index f786f46..7ba6bfe 100644
--- a/internal/adapters/agentsandbox/kube_test.go
+++ b/internal/adapters/agentsandbox/kube_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package agentsandbox
import (
diff --git a/internal/adapters/agentsandbox/live_test.go b/internal/adapters/agentsandbox/live_test.go
index 986fbae..0232b53 100644
--- a/internal/adapters/agentsandbox/live_test.go
+++ b/internal/adapters/agentsandbox/live_test.go
@@ -1,4 +1,4 @@
-//go:build live
+//go:build live && experimental_adapters
package agentsandbox
@@ -7,7 +7,7 @@ package agentsandbox
//
// AGENTSANDBOX_API_SERVER=https://127.0.0.1:6443 AGENTSANDBOX_CA_FILE=ca.crt \
// AGENTSANDBOX_TOKEN=$(kubectl -n dly create token differently) \
-// go test -tags live -v -run TestLiveAgentSandbox ./internal/adapters/agentsandbox/
+// go test -tags live,experimental_adapters -v -run TestLiveAgentSandbox ./internal/adapters/agentsandbox/
//
// Settings: AGENTSANDBOX_NAMESPACE (default dly), AGENTSANDBOX_IMAGE (default
// debian:bookworm-slim), AGENTSANDBOX_VOLUME_SIZE (enables stop/start, default
diff --git a/internal/adapters/azuresandboxes/azuresandboxes.go b/internal/adapters/azuresandboxes/azuresandboxes.go
index 1942463..661c5a4 100644
--- a/internal/adapters/azuresandboxes/azuresandboxes.go
+++ b/internal/adapters/azuresandboxes/azuresandboxes.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package azuresandboxes adapts Azure Container Apps Sandboxes: each
// Differently sandbox is one sandbox in an Azure sandbox group.
//
diff --git a/internal/adapters/azuresandboxes/azuresandboxes_test.go b/internal/adapters/azuresandboxes/azuresandboxes_test.go
index 1d18747..aa908f0 100644
--- a/internal/adapters/azuresandboxes/azuresandboxes_test.go
+++ b/internal/adapters/azuresandboxes/azuresandboxes_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package azuresandboxes
import (
diff --git a/internal/adapters/azuresessions/azuresessions.go b/internal/adapters/azuresessions/azuresessions.go
index d1ee573..973e31b 100644
--- a/internal/adapters/azuresessions/azuresessions.go
+++ b/internal/adapters/azuresessions/azuresessions.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package azuresessions adapts Azure Container Apps dynamic sessions in a
// Python code-interpreter session pool: each Differently sandbox is one
// session, named by its identifier.
diff --git a/internal/adapters/azuresessions/azuresessions_test.go b/internal/adapters/azuresessions/azuresessions_test.go
index 62bd589..42a9666 100644
--- a/internal/adapters/azuresessions/azuresessions_test.go
+++ b/internal/adapters/azuresessions/azuresessions_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package azuresessions
import (
diff --git a/internal/adapters/blaxel/blaxel.go b/internal/adapters/blaxel/blaxel.go
index 86c259d..7333236 100644
--- a/internal/adapters/blaxel/blaxel.go
+++ b/internal/adapters/blaxel/blaxel.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package blaxel adapts Blaxel's hosted sandbox API.
//
// Blaxel is hosted-only, so pools on this adapter are always TrustExternal.
diff --git a/internal/adapters/blaxel/blaxel_test.go b/internal/adapters/blaxel/blaxel_test.go
index 0ca6d0f..f6155cd 100644
--- a/internal/adapters/blaxel/blaxel_test.go
+++ b/internal/adapters/blaxel/blaxel_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package blaxel
import (
diff --git a/internal/adapters/boat/boat.go b/internal/adapters/boat/boat.go
index 11d57a1..4daf185 100644
--- a/internal/adapters/boat/boat.go
+++ b/internal/adapters/boat/boat.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package boat adapts Boat's hosted sandbox API (/api/v1).
//
// Boat is hosted-only, so pools on this adapter are always TrustExternal.
diff --git a/internal/adapters/boat/boat_test.go b/internal/adapters/boat/boat_test.go
index 796633d..708d828 100644
--- a/internal/adapters/boat/boat_test.go
+++ b/internal/adapters/boat/boat_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package boat
import (
diff --git a/internal/adapters/cloudhttp/auth.go b/internal/adapters/cloudhttp/auth.go
index 5c21b9d..09f9079 100644
--- a/internal/adapters/cloudhttp/auth.go
+++ b/internal/adapters/cloudhttp/auth.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package cloudhttp
import (
diff --git a/internal/adapters/cloudhttp/client.go b/internal/adapters/cloudhttp/client.go
index b55e7d4..b132929 100644
--- a/internal/adapters/cloudhttp/client.go
+++ b/internal/adapters/cloudhttp/client.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package cloudhttp calls cloud providers' sandbox APIs with cloud IAM
// credentials: Microsoft Entra ID tokens, Google OAuth 2.0 tokens and AWS
// Signature Version 4. It uses only the standard library, so the cloud
diff --git a/internal/adapters/cloudhttp/cloudhttp_test.go b/internal/adapters/cloudhttp/cloudhttp_test.go
index 40228a5..6acd162 100644
--- a/internal/adapters/cloudhttp/cloudhttp_test.go
+++ b/internal/adapters/cloudhttp/cloudhttp_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package cloudhttp
import (
diff --git a/internal/adapters/cloudhttp/eventstream.go b/internal/adapters/cloudhttp/eventstream.go
index 2d9eb8b..baa71fc 100644
--- a/internal/adapters/cloudhttp/eventstream.go
+++ b/internal/adapters/cloudhttp/eventstream.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package cloudhttp
import (
diff --git a/internal/adapters/cloudhttp/sigv4.go b/internal/adapters/cloudhttp/sigv4.go
index 2ce4788..7fd2b01 100644
--- a/internal/adapters/cloudhttp/sigv4.go
+++ b/internal/adapters/cloudhttp/sigv4.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package cloudhttp
import (
diff --git a/internal/adapters/cubesandbox/cubesandbox.go b/internal/adapters/cubesandbox/cubesandbox.go
index a033e88..4967955 100644
--- a/internal/adapters/cubesandbox/cubesandbox.go
+++ b/internal/adapters/cubesandbox/cubesandbox.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package cubesandbox adapts CubeSandbox (github.com/TencentCloud/CubeSandbox,
// Apache-2.0): KVM microVMs (cube-hypervisor, a Cloud Hypervisor fork) behind
// an E2B-compatible API.
diff --git a/internal/adapters/cubesandbox/cubesandbox_test.go b/internal/adapters/cubesandbox/cubesandbox_test.go
index f2a692a..d67bddb 100644
--- a/internal/adapters/cubesandbox/cubesandbox_test.go
+++ b/internal/adapters/cubesandbox/cubesandbox_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package cubesandbox
import (
diff --git a/internal/adapters/cubesandbox/envd.go b/internal/adapters/cubesandbox/envd.go
index a63240a..547141c 100644
--- a/internal/adapters/cubesandbox/envd.go
+++ b/internal/adapters/cubesandbox/envd.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package cubesandbox
// envd is E2B's agent inside each sandbox (CubeSandbox builds it from
diff --git a/internal/adapters/cubesandbox/live_test.go b/internal/adapters/cubesandbox/live_test.go
index b399dfd..92e58f3 100644
--- a/internal/adapters/cubesandbox/live_test.go
+++ b/internal/adapters/cubesandbox/live_test.go
@@ -1,4 +1,4 @@
-//go:build live
+//go:build live && experimental_adapters
package cubesandbox
@@ -6,7 +6,7 @@ package cubesandbox
// docs/providers/cubesandbox.md):
//
// CUBE_API_URL=http://:3000 CUBE_TEMPLATE_ID= \
-// CUBE_SANDBOX_URL=http://:80 go test -tags live -v -run TestLiveCubeSandbox ./internal/adapters/cubesandbox/
+// CUBE_SANDBOX_URL=http://:80 go test -tags live,experimental_adapters -v -run TestLiveCubeSandbox ./internal/adapters/cubesandbox/
//
// Settings: CUBE_API_KEY (when CubeAPI has auth on), CUBE_SANDBOX_URL
// (CubeProxy, for header routing; without it, host routing through
diff --git a/internal/adapters/e2b/e2b.go b/internal/adapters/e2b/e2b.go
index 3289b40..eb40336 100644
--- a/internal/adapters/e2b/e2b.go
+++ b/internal/adapters/e2b/e2b.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package e2b adapts E2B's sandbox API: the hosted service, or an E2B the
// operator runs (its infrastructure is open source), so pools declare trust.
//
diff --git a/internal/adapters/e2b/e2b_test.go b/internal/adapters/e2b/e2b_test.go
index 8411a88..16d8781 100644
--- a/internal/adapters/e2b/e2b_test.go
+++ b/internal/adapters/e2b/e2b_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package e2b
import (
diff --git a/internal/adapters/fly/fly.go b/internal/adapters/fly/fly.go
index fefb7a4..a3d62df 100644
--- a/internal/adapters/fly/fly.go
+++ b/internal/adapters/fly/fly.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package fly adapts Fly.io's Machines API.
//
// Fly.io runs each machine as a Firecracker microVM on its own hosts, so
diff --git a/internal/adapters/fly/fly_test.go b/internal/adapters/fly/fly_test.go
index ec366c1..18ce9ca 100644
--- a/internal/adapters/fly/fly_test.go
+++ b/internal/adapters/fly/fly_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package fly
import (
diff --git a/internal/adapters/gcpsandboxes/gcpsandboxes.go b/internal/adapters/gcpsandboxes/gcpsandboxes.go
index 27fd262..62b3e41 100644
--- a/internal/adapters/gcpsandboxes/gcpsandboxes.go
+++ b/internal/adapters/gcpsandboxes/gcpsandboxes.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package gcpsandboxes adapts Google Cloud's Agent Platform sandbox
// environments (formerly Vertex AI Agent Engine code execution) in their
// shell flavour: each Differently sandbox is one sandbox environment under
diff --git a/internal/adapters/gcpsandboxes/gcpsandboxes_test.go b/internal/adapters/gcpsandboxes/gcpsandboxes_test.go
index 7eedd93..975c875 100644
--- a/internal/adapters/gcpsandboxes/gcpsandboxes_test.go
+++ b/internal/adapters/gcpsandboxes/gcpsandboxes_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package gcpsandboxes
import (
diff --git a/internal/adapters/morph/morph.go b/internal/adapters/morph/morph.go
index e8d9d97..bf0a8f2 100644
--- a/internal/adapters/morph/morph.go
+++ b/internal/adapters/morph/morph.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package morph adapts Morph Cloud's hosted VM API (/api).
//
// Morph Cloud is hosted-only, so pools on this adapter are always
diff --git a/internal/adapters/morph/morph_test.go b/internal/adapters/morph/morph_test.go
index 6e04fce..7546b42 100644
--- a/internal/adapters/morph/morph_test.go
+++ b/internal/adapters/morph/morph_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package morph
import (
diff --git a/internal/adapters/northflank/northflank.go b/internal/adapters/northflank/northflank.go
index 83ee0b7..592f64e 100644
--- a/internal/adapters/northflank/northflank.go
+++ b/internal/adapters/northflank/northflank.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package northflank adapts Northflank sandboxes: each Differently sandbox
// is one deployment service in a Northflank project, kept alive with
// `sleep infinity` so commands can run in it.
diff --git a/internal/adapters/northflank/northflank_test.go b/internal/adapters/northflank/northflank_test.go
index cc8b3de..7c4ab4a 100644
--- a/internal/adapters/northflank/northflank_test.go
+++ b/internal/adapters/northflank/northflank_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package northflank
import (
diff --git a/internal/adapters/northflank/websocket.go b/internal/adapters/northflank/websocket.go
index d9b9dba..5adc625 100644
--- a/internal/adapters/northflank/websocket.go
+++ b/internal/adapters/northflank/websocket.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package northflank
import (
diff --git a/internal/adapters/pyshell/pyshell.go b/internal/adapters/pyshell/pyshell.go
index 3797f3f..a71ce17 100644
--- a/internal/adapters/pyshell/pyshell.go
+++ b/internal/adapters/pyshell/pyshell.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package pyshell runs Differently exec requests on code-interpreter
// backends that execute Python rather than shell commands. The request
// becomes a short Python program that runs the command with subprocess,
diff --git a/internal/adapters/pyshell/pyshell_test.go b/internal/adapters/pyshell/pyshell_test.go
index bfe8b53..ac0c85c 100644
--- a/internal/adapters/pyshell/pyshell_test.go
+++ b/internal/adapters/pyshell/pyshell_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package pyshell
import (
diff --git a/internal/adapters/shellcmd/shellcmd.go b/internal/adapters/shellcmd/shellcmd.go
index 6beca95..1b89c52 100644
--- a/internal/adapters/shellcmd/shellcmd.go
+++ b/internal/adapters/shellcmd/shellcmd.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package shellcmd builds POSIX shell scripts for backends whose only
// primitive is "run this command line": the exec request's working
// directory, environment, stdin and timeout become part of the script, and
diff --git a/internal/adapters/shellcmd/shellcmd_test.go b/internal/adapters/shellcmd/shellcmd_test.go
index b347313..86704e2 100644
--- a/internal/adapters/shellcmd/shellcmd_test.go
+++ b/internal/adapters/shellcmd/shellcmd_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package shellcmd
import (
diff --git a/internal/adapters/vercel/vercel.go b/internal/adapters/vercel/vercel.go
index 8085163..8478ccb 100644
--- a/internal/adapters/vercel/vercel.go
+++ b/internal/adapters/vercel/vercel.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
// Package vercel adapts Vercel Sandbox's REST API.
//
// Vercel runs every sandbox as a Firecracker microVM it operates, so pools on
diff --git a/internal/adapters/vercel/vercel_test.go b/internal/adapters/vercel/vercel_test.go
index 144d464..cae8b26 100644
--- a/internal/adapters/vercel/vercel_test.go
+++ b/internal/adapters/vercel/vercel_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package vercel
import (
diff --git a/internal/config/build_default.go b/internal/config/build_default.go
new file mode 100644
index 0000000..190d9f5
--- /dev/null
+++ b/internal/config/build_default.go
@@ -0,0 +1,7 @@
+//go:build !experimental_adapters
+
+package config
+
+// experimentalBuilt is false in the default build, which leaves the
+// ExperimentalAdapters out.
+const experimentalBuilt = false
diff --git a/internal/config/build_experimental.go b/internal/config/build_experimental.go
new file mode 100644
index 0000000..0f6d500
--- /dev/null
+++ b/internal/config/build_experimental.go
@@ -0,0 +1,7 @@
+//go:build experimental_adapters
+
+package config
+
+// experimentalBuilt is true when the server is built with
+// -tags experimental_adapters.
+const experimentalBuilt = true
diff --git a/internal/config/cloud_kinds_test.go b/internal/config/cloud_kinds_test.go
index e79ed26..d8e4632 100644
--- a/internal/config/cloud_kinds_test.go
+++ b/internal/config/cloud_kinds_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package config
import (
diff --git a/internal/config/config.go b/internal/config/config.go
index 58cf00c..62346b7 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -237,8 +237,45 @@ func (d DashboardConfig) On() bool { return d.Enabled == nil || *d.Enabled }
// Adapter kinds that always run on third-party infrastructure.
var externalKinds = []string{"daytona", "freestyle", "vercel", "fly", "boat", "morph", "blaxel"}
-// KnownAdapters lists adapter kinds the server can build.
-var KnownAdapters = []string{"opensandbox", "daytona", "freestyle", "remote", "devhost", "fake", "vercel", "fly", "agent-sandbox", "cubesandbox", "aws-agentcore", "azure-sandboxes", "azure-sessions", "gcp-sandboxes", "northflank", "boat", "e2b", "morph", "blaxel"}
+// DefaultAdapters lists the adapter kinds in every build: the backends
+// Differently has run against for real, the remote plugin protocol, and the
+// two dev-mode kinds.
+var DefaultAdapters = []string{"opensandbox", "daytona", "freestyle", "remote", "devhost", "fake"}
+
+// ExperimentalTag is the Go build tag that compiles ExperimentalAdapters in.
+const ExperimentalTag = "experimental_adapters"
+
+// ExperimentalAdapters lists the adapter kinds left out of the default build.
+// Apart from agent-sandbox, which ran live against a kind cluster, they are
+// tested only against fakes of each provider's published API. A server built
+// with -tags experimental_adapters has them.
+var ExperimentalAdapters = []string{"vercel", "fly", "agent-sandbox", "cubesandbox", "aws-agentcore", "azure-sandboxes", "azure-sessions", "gcp-sandboxes", "northflank", "boat", "e2b", "morph", "blaxel"}
+
+// KnownAdapters lists adapter kinds this binary can build.
+var KnownAdapters = knownAdapters()
+
+func knownAdapters() []string {
+ if experimentalBuilt {
+ return slices.Concat(DefaultAdapters, ExperimentalAdapters)
+ }
+ return slices.Clone(DefaultAdapters)
+}
+
+// ExperimentalBuilt reports whether this binary has the experimental adapters.
+func ExperimentalBuilt() bool { return experimentalBuilt }
+
+// UnavailableAdapter says why this binary cannot build an adapter of kind:
+// an experimental kind names the build tag that adds it. It returns "" for a
+// kind in KnownAdapters.
+func UnavailableAdapter(kind string) string {
+ switch {
+ case slices.Contains(KnownAdapters, kind):
+ return ""
+ case slices.Contains(ExperimentalAdapters, kind):
+ return fmt.Sprintf("the %s adapter is experimental and not in this build of differently-server; build the server with -tags %s to use it (docs/providers/README.md)", kind, ExperimentalTag)
+ }
+ return fmt.Sprintf("unknown adapter %q (known: %s)", kind, strings.Join(KnownAdapters, ", "))
+}
// Adapter kinds that run where the operator put them (a plugin they deploy,
// the dev host, the test fake), so they are private unless declared.
@@ -426,8 +463,8 @@ func (c Config) Validate() error {
bad("duplicate pool %s", p.Name)
}
names[p.Name] = true
- if !slices.Contains(KnownAdapters, p.Adapter) {
- bad("pool %s: unknown adapter %q (known: %s)", p.Name, p.Adapter, strings.Join(KnownAdapters, ", "))
+ if why := UnavailableAdapter(p.Adapter); why != "" {
+ bad("pool %s: %s", p.Name, why)
}
switch {
case p.Trust == "":
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 96ec7a4..06b57b0 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -37,8 +37,8 @@ pools:
func TestManagedProvidersAreAlwaysExternal(t *testing.T) {
c, err := Parse([]byte(`
pools:
- - {name: bl, adapter: blaxel, trust: private, config: {workspace: ws}, credentials: {env: BL_API_KEY}}
- - {name: bl2, adapter: blaxel, config: {workspace: ws}}
+ - {name: fs, adapter: freestyle, trust: private, credentials: {env: FREESTYLE_API_KEY}}
+ - {name: fs2, adapter: freestyle}
`))
if err != nil {
t.Fatal(err)
@@ -103,10 +103,10 @@ func TestNewAdapterKindsMustDeclareTrust(t *testing.T) {
}
}
-// Every adapter kind the server can build sits in exactly one trust list, so
-// adding a kind means deciding who operates it.
+// Every adapter kind, in this build or behind the experimental tag, sits in
+// exactly one trust list, so adding a kind means deciding who operates it.
func TestEveryAdapterKindHasATrustRule(t *testing.T) {
- for _, kind := range KnownAdapters {
+ for _, kind := range slices.Concat(DefaultAdapters, ExperimentalAdapters) {
n := 0
for _, list := range [][]string{externalKinds, privateByDefaultKinds, declaredTrustKinds} {
if slices.Contains(list, kind) {
@@ -119,32 +119,6 @@ func TestEveryAdapterKindHasATrustRule(t *testing.T) {
}
}
-// Managed providers are external whatever a pool says; E2B, which can also
-// be self-hosted, must declare who operates it and is private only when it
-// says so.
-func TestProviderKindsTrust(t *testing.T) {
- pool := func(kind, trust string) []byte {
- return []byte("pools:\n - name: p\n adapter: " + kind + "\n" + trust + " credentials: {env: KEY}\n")
- }
- for _, kind := range []string{"boat", "morph"} {
- for _, trust := range []string{"", " trust: private\n"} {
- c, err := Parse(pool(kind, trust))
- if err != nil || c.Pools[0].Trust != api.TrustExternal {
- t.Errorf("%s with %q: %+v %v", kind, trust, c.Pools, err)
- }
- }
- }
- if _, err := Parse(pool("e2b", "")); err == nil || !strings.Contains(err.Error(), "set trust explicitly") {
- t.Fatalf("e2b without trust must be rejected, got %v", err)
- }
- for _, trust := range []string{api.TrustExternal, api.TrustPrivate} {
- c, err := Parse(pool("e2b", " trust: "+trust+"\n"))
- if err != nil || c.Pools[0].Trust != trust {
- t.Errorf("e2b declared %s: %+v %v", trust, c.Pools, err)
- }
- }
-}
-
// CORS origins must be exact origins, never wildcards or URLs with paths.
func TestCORSOriginsMustBeExact(t *testing.T) {
for _, o := range []string{"*", "https://*.example.com", "console.example.com", "https://console.example.com/app", "ftp://x.example"} {
diff --git a/internal/config/default_kinds_test.go b/internal/config/default_kinds_test.go
new file mode 100644
index 0000000..5f1d640
--- /dev/null
+++ b/internal/config/default_kinds_test.go
@@ -0,0 +1,36 @@
+//go:build !experimental_adapters
+
+package config
+
+import (
+ "slices"
+ "strings"
+ "testing"
+)
+
+// The default build holds only the proven kinds. A pool of an experimental
+// kind is refused with the build tag that adds it, never accepted and left to
+// fail later.
+func TestDefaultBuildRefusesExperimentalKinds(t *testing.T) {
+ if ExperimentalBuilt() {
+ t.Fatal("built without the tag, but ExperimentalBuilt is true")
+ }
+ if want := []string{"opensandbox", "daytona", "freestyle", "remote", "devhost", "fake"}; !slices.Equal(KnownAdapters, want) {
+ t.Fatalf("default build knows %v, want %v", KnownAdapters, want)
+ }
+ for _, kind := range ExperimentalAdapters {
+ _, err := Parse([]byte("pools:\n - {name: p, adapter: " + kind + ", trust: external, config: {apiURL: \"http://x\"}, credentials: {env: KEY}}\n"))
+ if err == nil {
+ t.Errorf("%s: accepted by a build that has no such adapter", kind)
+ continue
+ }
+ for _, want := range []string{"the " + kind + " adapter is experimental", "-tags experimental_adapters"} {
+ if !strings.Contains(err.Error(), want) {
+ t.Errorf("%s: error %q does not say %q", kind, err, want)
+ }
+ }
+ if strings.Contains(err.Error(), "unknown adapter") {
+ t.Errorf("%s: reported as unknown, not as left out of the build: %v", kind, err)
+ }
+ }
+}
diff --git a/internal/config/experimental_kinds_test.go b/internal/config/experimental_kinds_test.go
new file mode 100644
index 0000000..5a82f85
--- /dev/null
+++ b/internal/config/experimental_kinds_test.go
@@ -0,0 +1,49 @@
+//go:build experimental_adapters
+
+package config
+
+import (
+ "slices"
+ "strings"
+ "testing"
+
+ "github.com/different-ai/differently/pkg/api"
+)
+
+// With the tag, every experimental kind is one the server can build.
+func TestExperimentalKindsAreKnown(t *testing.T) {
+ if !ExperimentalBuilt() {
+ t.Fatal("built with the tag, but ExperimentalBuilt is false")
+ }
+ for _, kind := range ExperimentalAdapters {
+ if !slices.Contains(KnownAdapters, kind) || UnavailableAdapter(kind) != "" {
+ t.Errorf("%s should be available in a build with -tags %s", kind, ExperimentalTag)
+ }
+ }
+}
+
+// Managed providers are external whatever a pool says; E2B, which can also
+// be self-hosted, must declare who operates it and is private only when it
+// says so.
+func TestProviderKindsTrust(t *testing.T) {
+ pool := func(kind, trust string) []byte {
+ return []byte("pools:\n - name: p\n adapter: " + kind + "\n" + trust + " credentials: {env: KEY}\n")
+ }
+ for _, kind := range []string{"boat", "morph"} {
+ for _, trust := range []string{"", " trust: private\n"} {
+ c, err := Parse(pool(kind, trust))
+ if err != nil || c.Pools[0].Trust != api.TrustExternal {
+ t.Errorf("%s with %q: %+v %v", kind, trust, c.Pools, err)
+ }
+ }
+ }
+ if _, err := Parse(pool("e2b", "")); err == nil || !strings.Contains(err.Error(), "set trust explicitly") {
+ t.Fatalf("e2b without trust must be rejected, got %v", err)
+ }
+ for _, trust := range []string{api.TrustExternal, api.TrustPrivate} {
+ c, err := Parse(pool("e2b", " trust: "+trust+"\n"))
+ if err != nil || c.Pools[0].Trust != trust {
+ t.Errorf("e2b declared %s: %+v %v", trust, c.Pools, err)
+ }
+ }
+}
diff --git a/internal/config/platform_adapters_test.go b/internal/config/platform_adapters_test.go
index aa087b5..cfdc3ce 100644
--- a/internal/config/platform_adapters_test.go
+++ b/internal/config/platform_adapters_test.go
@@ -1,3 +1,5 @@
+//go:build experimental_adapters
+
package config
import (
diff --git a/internal/dashboard/static/app.js b/internal/dashboard/static/app.js
index 0477e1b..51e187d 100644
--- a/internal/dashboard/static/app.js
+++ b/internal/dashboard/static/app.js
@@ -28,26 +28,15 @@ function table(cols, rows) {
const status = (s) => `${esc(s)} `;
const tokenBox = (t) => `Store it now; it is not shown again. ${esc(t)}Copy
`;
-// Provider kinds the console can add: label, fixed trust (empty: the operator
-// declares it), and main config fields as [key, label, choices?, required?].
+// Provider kinds the console can add (those in the default server build; the
+// experimental kinds are added with the CLI, the API or the config file):
+// label, fixed trust (empty: the operator declares it), and main config fields
+// as [key, label, choices?, required?].
const kinds = {
daytona: ["Daytona", "external", [["apiURL", "API URL (default https://app.daytona.io/api)"], ["target", "target region"], ["snapshot", "snapshot"], ["class", "class", ["container", "linux-vm"]]]],
freestyle: ["Freestyle", "external", [["apiURL", "API URL (default https://api.freestyle.sh)"], ["snapshot", "snapshot"], ["egress", "egress", ["none", "public"]], ["domain", "domain (default style.dev)"]]],
- boat: ["Boat", "external", [["apiURL", "API URL (default https://boat.dev/api/v1)"], ["type", "smallest size", ["small", "default", "large", "xlarge"]], ["snapshot", "saved snapshot"], ["environment", "environment"], ["org", "organization"], ["autoStopSeconds", "auto-stop seconds for sandboxes without a TTL"], ["portAccess", "port URLs", ["token", "public"]]]],
- e2b: ["E2B", "", [["apiURL", "API URL (default https://api.e2b.app)"], ["domain", "domain (default e2b.app)"], ["template", "template (default base)"], ["maxLifetimeSeconds", "plan's maximum runtime in seconds (default 3600)"], ["ports", "ports", ["private", "public"]], ["egress", "egress", ["public", "none"]]]],
- morph: ["Morph Cloud", "external", [["apiURL", "API URL (default https://cloud.morph.so/api)"], ["snapshot", "snapshot"], ["image", "image (default morphvm-minimal)"], ["portAuth", "port auth", ["none", "api_key"]]]],
- blaxel: ["Blaxel", "external", [["workspace", "workspace", null, true], ["apiURL", "API URL (default https://api.blaxel.ai/v0)"], ["region", "region (e.g. us-pdx-1)"], ["image", "image (default blaxel/base-image:latest)"]]],
opensandbox: ["OpenSandbox", "", [["apiURL", "lifecycle API URL", null, true], ["image", "image"], ["templateId", "template id"], ["isolation", "isolation", ["container", "microvm"]], ["runtime", "runtime", ["docker", "kubernetes"]], ["lineEndings", "line endings", ["kept", "stripped"]], ["ports", "port URLs", ["signed"]], ["egress", "egress", ["none", "public", "allowlist"]], ["egressAllow", "egress allowlist (comma-separated hosts)"], ["useServerProxy", "server proxy", ["true", "false"]]]],
remote: ["Remote plugin", "", [["endpoint", "plugin endpoint URL", null, true]]],
- vercel: ["Vercel Sandbox", "external", [["project", "project ID or name", null, true], ["team", "team ID"], ["region", "region (default iad1)"], ["image", "image"], ["architecture", "architecture", ["amd64", "arm64"]], ["ports", "ports (comma separated)"], ["maxTimeoutSeconds", "max session seconds (default 2700)"]]],
- fly: ["Fly.io Machines", "external", [["app", "Fly app name", null, true], ["region", "region"], ["image", "image"], ["cpuKind", "CPU kind", ["shared", "performance"]], ["suspend", "suspend for pause", ["true", "false"]]]],
- "agent-sandbox": ["agent-sandbox (Kubernetes)", "", [["namespace", "namespace (default default)"], ["apiServer", "API server URL (default: in-cluster)"], ["caFile", "API server CA file on the server"], ["image", "image (default ubuntu:24.04)"], ["warmPool", "warm pool (SandboxWarmPool name)"], ["runtimeClassName", "RuntimeClass (e.g. gvisor)"], ["isolation", "isolation", ["container", "microvm"]], ["volumeSize", "volume size, e.g. 10Gi (enables stop/start)"]]],
- cubesandbox: ["CubeSandbox", "", [["apiURL", "CubeAPI URL", null, true], ["templateId", "template id"], ["sandboxURL", "CubeProxy URL (header routing)"], ["domain", "sandbox domain (default: CubeAPI's)"], ["proxyScheme", "proxy scheme", ["https", "http"]]]],
- "aws-agentcore": ["AWS AgentCore Code Interpreter", "", [["region", "AWS region", null, true], ["codeInterpreter", "code interpreter (default aws.codeinterpreter.v1)"]]],
- "azure-sandboxes": ["Azure Container Apps Sandboxes", "", [["endpoint", "data-plane endpoint", null, true], ["subscriptionId", "subscription ID", null, true], ["resourceGroup", "resource group", null, true], ["sandboxGroup", "sandbox group", null, true], ["diskImage", "public disk image"], ["egress", "egress", ["none", "public"]]]],
- "azure-sessions": ["Azure dynamic sessions (Python)", "", [["endpoint", "pool management endpoint", null, true], ["outputLimit", "output limit (default 1048576)"]]],
- "gcp-sandboxes": ["Google Cloud Agent Platform sandboxes", "", [["project", "project ID", null, true], ["location", "location", null, true], ["reasoningEngine", "engine ID", null, true], ["template", "shell template ID", null, true]]],
- northflank: ["Northflank", "", [["projectId", "project ID", null, true], ["teamId", "team ID (organization tokens)"], ["image", "image (default ubuntu:24.04)"], ["plan", "plan (default nf-compute-200)"]]],
};
function pickKind() {
const [, trust, fields] = kinds[$("#pk-kind").value], t = $("#pk-trust");
diff --git a/internal/pools/experimental_test.go b/internal/pools/experimental_test.go
new file mode 100644
index 0000000..e04fccc
--- /dev/null
+++ b/internal/pools/experimental_test.go
@@ -0,0 +1,29 @@
+//go:build !experimental_adapters
+
+package pools
+
+import (
+ "io"
+ "log/slog"
+ "strings"
+ "testing"
+
+ "github.com/different-ai/differently/pkg/api"
+)
+
+// A pool stored by a build that had an experimental adapter stays registered
+// in a build without it: the server starts, the pool is unavailable, and the
+// reason names the build tag.
+func TestStoredPoolOfExperimentalKindNamesTheBuildTag(t *testing.T) {
+ m := NewManager(Deps{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))})
+ m.Sync([]api.Pool{{Name: "burst", Adapter: "e2b", Trust: api.TrustExternal, Enabled: true}})
+ _, err := m.Adapter("burst")
+ if !api.IsCode(err, api.CodeProviderUnavailable) || !strings.Contains(err.Error(), "-tags experimental_adapters") {
+ t.Fatalf("want provider_unavailable naming the build tag, got %v", err)
+ }
+ pool := api.Pool{Name: "burst"}
+ m.Decorate(&pool, 0)
+ if pool.Health == nil || pool.Health.Status != "unavailable" || !strings.Contains(pool.Health.Message, "experimental_adapters") {
+ t.Fatalf("pool health should say why it cannot run: %+v", pool.Health)
+ }
+}
diff --git a/internal/pools/pools.go b/internal/pools/pools.go
index cce3bfb..4582574 100644
--- a/internal/pools/pools.go
+++ b/internal/pools/pools.go
@@ -7,13 +7,13 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
- "fmt"
"io/fs"
"log/slog"
"reflect"
"sync"
"time"
+ "github.com/different-ai/differently/internal/config"
"github.com/different-ai/differently/internal/policy"
"github.com/different-ai/differently/internal/store"
"github.com/different-ai/differently/pkg/adapter"
@@ -117,7 +117,9 @@ func (m *Manager) Sync(pools []api.Pool) {
}
e := &entry{pool: p, credFP: fps[p.Name]}
if f := factoryFor(p.Adapter); f == nil {
- e.buildErr = fmt.Errorf("adapter kind %q is not available in this build", p.Adapter)
+ // A pool stored by a build that had the kind (an experimental
+ // adapter, say) stays registered and reports why it cannot run.
+ e.buildErr = errors.New(config.UnavailableAdapter(p.Adapter))
} else if a, err := f(p, m.deps); err != nil {
e.buildErr = err
} else {
diff --git a/internal/server/experimental_pool_test.go b/internal/server/experimental_pool_test.go
new file mode 100644
index 0000000..5a4036a
--- /dev/null
+++ b/internal/server/experimental_pool_test.go
@@ -0,0 +1,32 @@
+//go:build !experimental_adapters
+
+package server_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/different-ai/differently/internal/config"
+ "github.com/different-ai/differently/internal/wiring"
+ "github.com/different-ai/differently/pkg/api"
+)
+
+// A server built without the experimental adapters refuses a pool of one of
+// those kinds through the API, and says which build tag adds it.
+func TestDefaultBuildRefusesExperimentalPoolThroughAPI(t *testing.T) {
+ wiring.Register()
+ h := newHarness(t, api.PlacementPolicy{})
+ for _, kind := range config.ExperimentalAdapters {
+ var raw string
+ pool := map[string]any{"name": "p-" + kind, "adapter": kind, "trust": "external", "config": map[string]string{"apiURL": "http://backend.invalid"}, "credentials": map[string]string{"env": "PROVIDER_KEY"}}
+ if got := h.call("POST", "/v1/admin/pools", operatorToken, pool, &raw); got != 400 {
+ t.Fatalf("%s: status %d, want 400: %s", kind, got, raw)
+ }
+ if !strings.Contains(raw, api.CodeInvalidRequest) || !strings.Contains(raw, "-tags experimental_adapters") {
+ t.Errorf("%s: the refusal does not name the build tag: %s", kind, raw)
+ }
+ if got := h.call("GET", "/v1/admin/pools/p-"+kind, operatorToken, nil, &raw); got != 404 {
+ t.Errorf("%s: the refused pool was stored (status %d): %s", kind, got, raw)
+ }
+ }
+}
diff --git a/internal/wiring/wiring.go b/internal/wiring/wiring.go
index 76afd7d..c60d175 100644
--- a/internal/wiring/wiring.go
+++ b/internal/wiring/wiring.go
@@ -1,28 +1,20 @@
// Package wiring registers the built-in adapter kinds with the pool manager.
+//
+// The default build holds the kinds that have run against real backends
+// (opensandbox, daytona, freestyle), the remote plugin protocol, and the two
+// dev-mode kinds (devhost, fake). The rest are experimental: they compile in
+// only with the experimental_adapters build tag (wiring_experimental.go).
package wiring
import (
"fmt"
"path/filepath"
- "github.com/different-ai/differently/internal/adapters/agentcore"
- "github.com/different-ai/differently/internal/adapters/agentsandbox"
- "github.com/different-ai/differently/internal/adapters/azuresandboxes"
- "github.com/different-ai/differently/internal/adapters/azuresessions"
- "github.com/different-ai/differently/internal/adapters/blaxel"
- "github.com/different-ai/differently/internal/adapters/boat"
- "github.com/different-ai/differently/internal/adapters/cubesandbox"
"github.com/different-ai/differently/internal/adapters/daytona"
"github.com/different-ai/differently/internal/adapters/devhost"
- "github.com/different-ai/differently/internal/adapters/e2b"
"github.com/different-ai/differently/internal/adapters/fake"
- "github.com/different-ai/differently/internal/adapters/fly"
"github.com/different-ai/differently/internal/adapters/freestyle"
- "github.com/different-ai/differently/internal/adapters/gcpsandboxes"
- "github.com/different-ai/differently/internal/adapters/morph"
- "github.com/different-ai/differently/internal/adapters/northflank"
"github.com/different-ai/differently/internal/adapters/opensandbox"
- "github.com/different-ai/differently/internal/adapters/vercel"
"github.com/different-ai/differently/internal/config"
"github.com/different-ai/differently/internal/pools"
"github.com/different-ai/differently/pkg/adapter"
@@ -30,8 +22,9 @@ import (
"github.com/different-ai/differently/pkg/api"
)
-// Register installs every built-in adapter factory.
+// Register installs the adapter factories this binary was built with.
func Register() {
+ registerExperimental()
pools.Register("remote", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
token, err := resolve(p, d)
if err != nil {
@@ -53,137 +46,6 @@ func Register() {
}
return freestyle.New(freestyle.FromConfig(p.Config, key)), nil
})
- pools.Register("vercel", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- key, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := vercel.FromConfig(p.Config, key)
- if err != nil {
- return nil, err
- }
- return vercel.New(opts), nil
- })
- pools.Register("fly", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- key, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := fly.FromConfig(p.Config, key)
- if err != nil {
- return nil, err
- }
- return fly.New(opts), nil
- })
- pools.Register("blaxel", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- key, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := blaxel.FromConfig(p.Config, key)
- if err != nil {
- return nil, err
- }
- return blaxel.New(opts), nil
- })
- pools.Register("boat", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- key, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- return boat.New(boat.FromConfig(p.Config, key)), nil
- })
- pools.Register("e2b", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- key, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- return e2b.New(e2b.FromConfig(p.Config, p.Trust, key)), nil
- })
- pools.Register("morph", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- key, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- return morph.New(morph.FromConfig(p.Config, key)), nil
- })
- pools.Register("aws-agentcore", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- cred, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := agentcore.FromConfig(p.Config, p.Region, cred)
- if err != nil {
- return nil, err
- }
- return agentcore.New(opts), nil
- })
- pools.Register("azure-sandboxes", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- cred, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := azuresandboxes.FromConfig(p.Config, p.Region, cred)
- if err != nil {
- return nil, err
- }
- return azuresandboxes.New(opts), nil
- })
- pools.Register("azure-sessions", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- cred, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := azuresessions.FromConfig(p.Config, p.Region, cred)
- if err != nil {
- return nil, err
- }
- return azuresessions.New(opts), nil
- })
- pools.Register("gcp-sandboxes", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- cred, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := gcpsandboxes.FromConfig(p.Config, p.Region, cred)
- if err != nil {
- return nil, err
- }
- return gcpsandboxes.New(opts), nil
- })
- pools.Register("northflank", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- token, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := northflank.FromConfig(p.Config, token)
- if err != nil {
- return nil, err
- }
- return northflank.New(opts), nil
- })
- pools.Register("agent-sandbox", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- token, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := agentsandbox.FromConfig(p.Config, p.Trust, token)
- if err != nil {
- return nil, err
- }
- return agentsandbox.New(opts)
- })
- pools.Register("cubesandbox", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
- key, err := resolve(p, d)
- if err != nil {
- return nil, err
- }
- opts, err := cubesandbox.FromConfig(p.Config, p.Trust, key)
- if err != nil {
- return nil, err
- }
- return cubesandbox.New(opts)
- })
pools.Register("opensandbox", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
key, err := resolve(p, d)
if err != nil {
diff --git a/internal/wiring/wiring_default.go b/internal/wiring/wiring_default.go
new file mode 100644
index 0000000..66366d0
--- /dev/null
+++ b/internal/wiring/wiring_default.go
@@ -0,0 +1,7 @@
+//go:build !experimental_adapters
+
+package wiring
+
+// registerExperimental is empty in the default build: the experimental
+// adapter kinds are compiled in only with -tags experimental_adapters.
+func registerExperimental() {}
diff --git a/internal/wiring/wiring_experimental.go b/internal/wiring/wiring_experimental.go
new file mode 100644
index 0000000..efc6d4c
--- /dev/null
+++ b/internal/wiring/wiring_experimental.go
@@ -0,0 +1,160 @@
+//go:build experimental_adapters
+
+package wiring
+
+import (
+ "github.com/different-ai/differently/internal/adapters/agentcore"
+ "github.com/different-ai/differently/internal/adapters/agentsandbox"
+ "github.com/different-ai/differently/internal/adapters/azuresandboxes"
+ "github.com/different-ai/differently/internal/adapters/azuresessions"
+ "github.com/different-ai/differently/internal/adapters/blaxel"
+ "github.com/different-ai/differently/internal/adapters/boat"
+ "github.com/different-ai/differently/internal/adapters/cubesandbox"
+ "github.com/different-ai/differently/internal/adapters/e2b"
+ "github.com/different-ai/differently/internal/adapters/fly"
+ "github.com/different-ai/differently/internal/adapters/gcpsandboxes"
+ "github.com/different-ai/differently/internal/adapters/morph"
+ "github.com/different-ai/differently/internal/adapters/northflank"
+ "github.com/different-ai/differently/internal/adapters/vercel"
+ "github.com/different-ai/differently/internal/pools"
+ "github.com/different-ai/differently/pkg/adapter"
+ "github.com/different-ai/differently/pkg/api"
+)
+
+// registerExperimental installs the adapter kinds listed in
+// config.ExperimentalAdapters. Apart from agent-sandbox, which ran live
+// against a kind cluster, they are tested only against fakes of each
+// provider's published API, so they stay out of the default build.
+func registerExperimental() {
+ pools.Register("vercel", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ key, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := vercel.FromConfig(p.Config, key)
+ if err != nil {
+ return nil, err
+ }
+ return vercel.New(opts), nil
+ })
+ pools.Register("fly", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ key, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := fly.FromConfig(p.Config, key)
+ if err != nil {
+ return nil, err
+ }
+ return fly.New(opts), nil
+ })
+ pools.Register("blaxel", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ key, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := blaxel.FromConfig(p.Config, key)
+ if err != nil {
+ return nil, err
+ }
+ return blaxel.New(opts), nil
+ })
+ pools.Register("boat", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ key, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ return boat.New(boat.FromConfig(p.Config, key)), nil
+ })
+ pools.Register("e2b", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ key, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ return e2b.New(e2b.FromConfig(p.Config, p.Trust, key)), nil
+ })
+ pools.Register("morph", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ key, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ return morph.New(morph.FromConfig(p.Config, key)), nil
+ })
+ pools.Register("aws-agentcore", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ cred, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := agentcore.FromConfig(p.Config, p.Region, cred)
+ if err != nil {
+ return nil, err
+ }
+ return agentcore.New(opts), nil
+ })
+ pools.Register("azure-sandboxes", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ cred, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := azuresandboxes.FromConfig(p.Config, p.Region, cred)
+ if err != nil {
+ return nil, err
+ }
+ return azuresandboxes.New(opts), nil
+ })
+ pools.Register("azure-sessions", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ cred, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := azuresessions.FromConfig(p.Config, p.Region, cred)
+ if err != nil {
+ return nil, err
+ }
+ return azuresessions.New(opts), nil
+ })
+ pools.Register("gcp-sandboxes", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ cred, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := gcpsandboxes.FromConfig(p.Config, p.Region, cred)
+ if err != nil {
+ return nil, err
+ }
+ return gcpsandboxes.New(opts), nil
+ })
+ pools.Register("northflank", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ token, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := northflank.FromConfig(p.Config, token)
+ if err != nil {
+ return nil, err
+ }
+ return northflank.New(opts), nil
+ })
+ pools.Register("agent-sandbox", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ token, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := agentsandbox.FromConfig(p.Config, p.Trust, token)
+ if err != nil {
+ return nil, err
+ }
+ return agentsandbox.New(opts)
+ })
+ pools.Register("cubesandbox", func(p api.Pool, d pools.Deps) (adapter.ExecutionAdapter, error) {
+ key, err := resolve(p, d)
+ if err != nil {
+ return nil, err
+ }
+ opts, err := cubesandbox.FromConfig(p.Config, p.Trust, key)
+ if err != nil {
+ return nil, err
+ }
+ return cubesandbox.New(opts)
+ })
+}
diff --git a/internal/wiring/wiring_test.go b/internal/wiring/wiring_test.go
new file mode 100644
index 0000000..04385a7
--- /dev/null
+++ b/internal/wiring/wiring_test.go
@@ -0,0 +1,22 @@
+package wiring
+
+import (
+ "slices"
+ "testing"
+
+ "github.com/different-ai/differently/internal/config"
+ "github.com/different-ai/differently/internal/pools"
+)
+
+// The kinds config accepts and the kinds with a factory are the same set in
+// every build, so a config never passes validation for a kind the binary
+// cannot build, with or without the experimental_adapters tag.
+func TestRegisteredKindsMatchConfig(t *testing.T) {
+ Register()
+ got, want := pools.Kinds(), slices.Clone(config.KnownAdapters)
+ slices.Sort(got)
+ slices.Sort(want)
+ if !slices.Equal(got, want) {
+ t.Fatalf("registered kinds %v, config.KnownAdapters %v (experimental built: %v)", got, want, config.ExperimentalBuilt())
+ }
+}
diff --git a/site/pages-src/features.md b/site/pages-src/features.md
index e938f5d..6257601 100644
--- a/site/pages-src/features.md
+++ b/site/pages-src/features.md
@@ -3,7 +3,7 @@ title: Features
description: What OpenSandbox provides, what Differently adds on top, and how that compares to using a single provider.
---
-Differently is an orchestration and resource-management layer. It does not run sandboxes itself: it decides where each one runs and keeps account of it. Its default private runtime is [OpenSandbox](https://github.com/opensandbox-group/OpenSandbox), run by the operator; other pools can be any of the [built-in provider adapters](/differently/docs/providers/), from E2B and Daytona to the cloud providers' sandbox services, or any service that speaks the [adapter protocol](/differently/docs/adapters/specification/).
+Differently is an orchestration and resource-management layer. It does not run sandboxes itself: it decides where each one runs and keeps account of it. Its default private runtime is [OpenSandbox](https://github.com/opensandbox-group/OpenSandbox), run by the operator; other pools can be Daytona or Freestyle, one of the [experimental provider adapters](/differently/docs/providers/) compiled in with a build tag (E2B, the cloud providers' sandbox services and more), or any service that speaks the [adapter protocol](/differently/docs/adapters/specification/).
:::caution[Pre-release]
The end-to-end suite passes 14/14 against real OpenSandbox servers on the Docker runtime and on the Kubernetes runtime (single-node k3s), and on a Fast Sandbox microVM pool on a two-node k3s cluster (14 of 14 run). Daytona and Freestyle ran against live accounts. Node drains, more than one KVM node, managed clusters and live accounts for the other managed providers are not verified yet. See the [roadmap](/differently/docs/roadmap/).
diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro
index 8830b04..9cd9010 100644
--- a/site/src/pages/index.astro
+++ b/site/src/pages/index.astro
@@ -131,14 +131,14 @@ const verified = [
['Recorded runtimes', 'Real OpenSandbox responses from the Docker, Kubernetes and Fast Sandbox runtimes are replayed in every go test.'],
['OpenWork', "Workbot computers run through Differently as a sandbox gateway, behind a flag: commands with Python tools, files, pause and resume, and Workbot's desktop on Fast Sandbox microVMs."],
['Failure isolation', 'An image that cannot be pulled fails that sandbox only; the shared pool stays healthy.'],
- ['agent-sandbox', 'The adapter runs against a Kubernetes cluster (kind, runc).'],
+ ['agent-sandbox', 'The experimental adapter runs against a Kubernetes cluster (kind, runc).'],
['Daytona and Freestyle', 'The adapters run against live accounts: exec, files, port URLs, stop/start and snapshots on both, and egress policy, pause/resume, memory snapshots and fork on Freestyle.'],
['Fast Sandbox', "Firecracker microVMs on Azure: the end-to-end suite passes on the microVM pool, and Workbot's desktop streams over a port URL, pauses and resumes with its memory (a pause takes minutes), idles and wakes on a request."],
];
const notVerified = [
'Node drains, and more than one KVM node',
'Managed EKS, GKE and AKS clusters',
- 'The other 12 provider adapters against live accounts; each is tested against a fake of its published API',
+ 'The other 12 provider adapters against live accounts; each is tested against a fake of its published API, is experimental and is left out of the published build',
];
const inProgress: string[] = [];
@@ -152,7 +152,7 @@ const features = [
{
k: '01',
title: 'Provider adapters',
- body: '16 built-in adapters, from self-hosted OpenSandbox to E2B, Vercel and the AWS, Azure and Google Cloud sandbox services, plus a plugin protocol for any other backend. Operators change where work goes; applications keep the same API.',
+ body: 'OpenSandbox, Daytona and Freestyle adapters in every build, a plugin protocol for any other backend, and 13 experimental adapters (E2B, Vercel, the AWS, Azure and Google Cloud sandbox services and more) you can compile in. Operators change where work goes; applications keep the same API.',
href: '/docs/providers/',
cta: 'Provider catalog',
},
@@ -204,7 +204,7 @@ const added = [
['Multi-tenancy', 'Organizations, projects and scoped API keys. OpenSandbox has one server-wide key.'],
['Quotas and fair queueing', 'Usage derived from state, enforced at admission, queued fairly across tenants.'],
['Policy-gated placement', 'Global, org and project policy plus data classification decide which pool may run each sandbox. External pools fail closed.'],
- ['Many backends, one API', '16 built-in provider adapters and plugins behind one contract and one SDK.'],
+ ['Many backends, one API', 'OpenSandbox, Daytona, Freestyle and plugins behind one contract and one SDK, with 13 more experimental adapters.'],
['Placement explanations', 'Every decision records the pools considered, the ones rejected and why.'],
['Audit and trust report', 'A tamper-evident audit chain and one report of endpoints, credentials by reference, external placements and limits.'],
['Capacity that follows use', 'A paused microVM gives its pool slot back; resumes queue on a full pool, and preemption makes room for waiting work.'],
@@ -225,7 +225,7 @@ const compare = [
Differently · Open-source sandbox orchestration
-
+
@@ -266,7 +266,7 @@ const compare = [
Everyone builds sandboxes.We orchestrate them.
Differently is an open-source control plane for sandboxes. It runs on any cloud or on bare metal
- and places each sandbox on OpenSandbox, E2B, Daytona, a cloud provider's sandbox service or a plugin,
+ and places each sandbox on OpenSandbox, Daytona, Freestyle or a plugin,
according to placement policy. Applications use one API; quotas, placement decisions and audit live in the control plane.
@@ -315,7 +315,7 @@ const compare = [
Postgres
OpenSandbox Docker · Kubernetes runtime
OpenSandbox Fast Sandbox microVMs
-
Providers E2B · Daytona · cloud
+
Providers Daytona · Freestyle
one API