Skip to content

OpenWork on Differently

OpenWork’s sandbox jobs are written against one provider contract, @openwork/sandbox. The Differently provider (@openwork/sandbox-differently) implements that contract against this API. OpenWork then holds a single Differently key. Differently holds the OpenSandbox, Daytona and Freestyle credentials, and decides by policy where each computer runs.

Status: run end to end on 2026-10-29. OpenWork’s headless runner, run locally, drove Workbot computers through an install on Azure VMs (azure-vms.md): a model turn used the computer on a Fast Sandbox microVM, Workbot opened a page in the desktop’s browser, the viewer streamed it, and OpenWork’s proof:live passed through the gateway, pause and resume included. The runner fell back to its own provider when the gateway could not start a computer.

What OpenWork needs, and where Differently provides it

Section titled “What OpenWork needs, and where Differently provides it”
OpenWork contract Differently Notes
idempotencyKey names one live instance Idempotency-Key header, plus the label openwork.sandbox.key A key whose sandbox has ended creates a new one. A replay of a live key is reported to OpenWork as conflict.
labels, find, list labels; GET /v1/sandboxes?label=k=v
image a template, or a snapshot ID to boot from
resources resources (cpu, memoryMiB, diskGiB)
create-time env env
lifecycle.autoDeleteMinutes ttlSeconds, extended each time the computer resumes PATCH ttlSeconds also moves OpenSandbox’s own expiry.
lifecycle.autoStopMinutes, autoPauseMinutes not provided Workbot pauses idle computers itself.
run (exact stdout, stderr, exit code) POST /v1/sandboxes/{id}/exec Exact on OpenSandbox with execd release-1.2.0 and lineEndings: kept.
files read, write, stat files/content GET and PUT; directory listing The adapter derives stat from the parent listing and applies mode with chmod.
pause, start /pause, /resume (or /stop, /start)
snapshots POST /v1/sandboxes/{id}/snapshots, DELETE /v1/snapshots/{id} A snapshot boots only on the pool that took it.
endpoint(port) POST /v1/sandboxes/{id}/ports Depends on the backend (see the backend matrix).
storage volumes, detached exec not yet OpenWork Cloud workers (Den) need both, so they stay on their Daytona provider. Workbot computers do not.

Errors map onto OpenWork’s taxonomy by code, never by message:

Differently code OpenWork code
not_found not_found
conflict conflict
invalid_state, unsupported_operation, unsupported_capability, invalid_request invalid_state (not retried)
quota_exceeded, no_eligible_pool capacity
rate_limited rate_limited
unauthenticated, permission_denied, suspended auth
provider_unavailable, HTTP 5xx transient

1. Deploy Differently with the pools OpenWork may use

Section titled “1. Deploy Differently with the pools OpenWork may use”

Follow deploy/kubernetes.md. For OpenWork:

  • OpenSandbox (private). Run its server with execd_image = "opensandbox/execd:release-1.2.0" or later, and set opensandbox.lineEndings: kept in the chart. With execd 1.1, the output of commands that end without a newline gains one, and OpenWork’s exact run contract fails.
  • Daytona or Freestyle (external, optional). Enable providers.daytona or providers.freestyle with existingSecret, or add them at runtime with their key stored in Differently (keys and providers). Their API keys live in Kubernetes Secrets that only the Differently server reads, and never reach OpenWork. See hybrid-overflow.md.
  • Policy. Registering a provider authorizes nothing. To route work there, set config.policy.external to overflow (only when private pools are full) or allowed, and list the pools in config.policy.externalPools. A narrower org or project policy can only tighten this.

On OpenSandbox’s Kubernetes runtime (AKS, k3s)

Section titled “On OpenSandbox’s Kubernetes runtime (AKS, k3s)”

Observed on a single-node k3s install on Azure (OpenSandbox server, controller and execd release-1.2.0, BatchSandbox): the end-to-end suite passes 8 of 8 with pause and snapshots left out. What that means for OpenWork computers:

  • Exact output. Its Kubernetes runtime already uses execd release-1.2.0, so set opensandbox.lineEndings: kept.
  • Pause and snapshots need a registry. On this runtime, OpenSandbox pauses by committing the sandbox’s filesystem to an OCI image and resumes by re-creating it from that image (controller.snapshot.registry plus push and pull secrets). That is slower than Docker’s memory freeze, and processes don’t survive it. Without a registry, leave lifecycle.pause-resume and snapshots.disk out of opensandbox.features. Workbot then can’t pause idle computers: the pause fails quietly and the computer runs until its TTL.
  • Port URLs need OpenSandbox’s ingress gateway (server.gateway.enabled), so a desktop viewer needs it, or a Daytona or Freestyle pool.
  • First image pull. Pulling a new image can take longer than OpenSandbox’s 60-second pod-ready timeout. OpenSandbox then deletes the sandbox and answers the create with a 504. Pre-pull large images, such as Workbot’s computer image, on the nodes, or raise that timeout (OpenSandbox pools).

Templates: route one image name to every provider

Section titled “Templates: route one image name to every provider”

Workbot boots a prepared image, and each provider names its own copy differently (the runner’s SANDBOX_GATEWAY_TEMPLATE names the template). A Differently template maps one name to each pool’s image, so the same computer can be placed on any of them:

config:
templates:
- name: workbot-computer
description: Workbot's Linux computer
pools: [opensandbox-microvm, freestyle]
images: # keyed by pool name
opensandbox-microvm: tpl-... # a Fast Sandbox template ID (the pool's templateId)
freestyle: workbot-desktop-freestyle-0123456789abcdef # the Freestyle snapshot's slug
# opensandbox: <registry>/workbot-computer:<tag> # an image, on the container runtimes
# daytona: <snapshot name>

A template with images runs only on the pools it lists, and placement explanations say so. A template name with no configured entry is passed to the backend unchanged, as before. Freestyle’s VM create takes the snapshot’s slug (the name OpenWork’s snapshot builder prints), not its sh-... id. Workbot’s desktop needs Daytona’s linux-vm class, so leave Daytona out of the template where the account has none.

Terminal window
differently admin org create openwork --default-project workbot
differently key create workbot-runner --org <org-id> --key-project <project-id>

The key holds the default sandboxes:read, sandboxes:write and sandboxes:exec scopes and works only in that project. Set project quotas (maxActiveSandboxes, CPU, memory) to cap what the runner can hold.

To replace the runner’s key without downtime, rotate it with a grace period (differently key rotate <key-id> --grace 1h), set the new token as the runner’s SANDBOX_GATEWAY_API_KEY, and let the old key lapse (rotating API keys). Provider keys can be stored in Differently, and which provider new computers use can be switched there (differently admin pool use NAME), with no change to OpenWork (keys and providers).

Differently is a sandbox gateway for Workbot, configured on the runner apart from its own provider (HEADLESS_COMPUTER, which stays Freestyle or Daytona). This describes OpenWork’s pull request #6649; check it for the current state before relying on the details.

Terminal window
SANDBOX_GATEWAY=differently
SANDBOX_GATEWAY_URL=https://differently.example.com # HTTPS only
SANDBOX_GATEWAY_API_KEY=<the project key>
SANDBOX_GATEWAY_TEMPLATE=workbot-computer # the Differently template

Then turn on the sandboxGateway feature flag for the organizations that should use it; every other organization keeps the runner’s own provider.

  • Health decides, per new computer. A flagged conversation’s computer goes through Differently when GET /v1/capabilities answers with an enabled, healthy pool (checked at most every 30 s). If Differently is unreachable, times out, answers 5xx or cannot start the sandbox, the computer is made on the runner’s own provider instead.

  • Refusals are not routed around. quota_exceeded, no_eligible_pool, permission and invalid or unsupported requests fail the call, so the policies in this project still hold.

  • Computers stay where they were made. One made through Differently errors while Differently is down; one made on the fallback is kept until it is gone, and the next computer goes through Differently again. The runner records which side made each computer.

  • Size and pause. Computers ask for their own size, so the project’s quotas must allow it. The runner pauses idle computers only when every pool its key may use can pause (or every one can stop), which it reads from GET /v1/capabilities. That answer lists only the pools the key’s placement policies allow, so give OpenWork’s organization a policy that allows just the pools Workbot uses, each with pause and snapshots (on the Kubernetes runtime, a snapshot registry):

    Terminal window
    echo '{"allowedPools":["opensandbox-microvm","freestyle"]}' > workbot.json
    differently admin policy org:<org-id> workbot.json
  • Idle deletion is built from TTL extensions: a computer expires HEADLESS_COMPUTER_KEEP_DAYS after it was last resumed, not after its last command. Differently has no idle-based lifecycle of its own yet.
  • Backstops. On Daytona and Freestyle, a TTL change does not yet move their own deletion deadline (backlog E0.20). Keep their TTLs generous or unset there.
  • Desktop viewers need a pool whose backend exposes ports without the backend’s API key: Freestyle (public TLS host), Daytona (signed preview URL), or OpenSandbox behind its ingress gateway (signed URLs on the Kubernetes runtime, route scopes on Fast Sandbox). OpenSandbox’s proxied endpoints are not handed out.
  • OpenSandbox’s maximum timeout. The adapter sends a computer’s TTL as OpenSandbox’s own expiry, and the runner keeps computers HEADLESS_COMPUTER_KEEP_DAYS (14 by default). Set the OpenSandbox servers’ max_sandbox_timeout_seconds above that, or every create is refused and the runner falls back to its own provider.
  • Fast Sandbox pause takes minutes (a full checkpoint: about three for a 4 GiB desktop). Differently finishes it even when the runner stops waiting; OpenWork’s runner gives pauses five minutes.
  • Den’s Cloud workers need detached processes and persistent volumes; both are in the backlog and Den stays on Daytona until then.

Source: docs/guides/openwork.md