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 @@

- Differently: your apps and agents call one API; differently-server handles tenancy, quotas, policy and audit, and places each sandbox on OpenSandbox or your own backend inside your infrastructure, or on a managed provider such as E2B or Daytona only if policy allows + Differently: your apps and agents call one API; differently-server handles tenancy, quotas, policy and audit, and places each sandbox on OpenSandbox or your own backend inside your infrastructure, or on a managed provider such as Daytona or Freestyle only if policy allows

@@ -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'; - +