Connectors
A provider is a vendor that will rent you a computer. A connector is the module in the gateway that talks to one. Fly Machines, exe.dev and AWS EC2 are providers; there is one connector for each, plus two in-memory ones the test suite runs against.
The contract
A connector is six things it does and six things it says about itself.
| Callback | What it must do |
|---|---|
create/1 | Build a machine from the spec and return it, running. |
get/1 | One environment by the opaque id create/1 returned. |
list/0 | Every environment this gateway owns on that backend, and nothing else. |
destroy/1 | Delete the machine and everything that only existed for it. |
start/1 | Bring a stopped machine back. |
stop/1 | Stop a machine without losing what :durable_disk promised. |
| Callback | What it must say |
|---|---|
capabilities/0 | Which of the optional behaviours are real here. |
sizes/0 | The guests this backend offers, by size key — its own table. |
regions/0 | Where it will place an environment, or [] if the caller has no say. |
default_image/1 | What it boots when the caller asked for nothing. |
prepare_image/1 | What to boot for this spec, and how it got there. |
platform/0 | The OCI platform to wrap an image for. |
Three rules that keep them swappable
- Names are short and provider-agnostic. The caller says
beam1; a connector may storeenv-beam1internally, and the prefix never leaks. - Everything except
create/1is addressed by the opaque id, which the gateway gets fromlist/0. - A connector states what it can do rather than being assumed to. Nothing in the gateway, the idle sweep, the MCP layer or the sharing layer branches on which module it is holding, and the grep that proves it is one of the repo's own gates.
What is deliberately not a callback
There is no exec. Running a command is a gateway mechanism,
not a provider feature: the gateway dials the machine's own envd over the control
tunnel it already holds. envd is the only party holding the workspace declaration,
so it is the only party that can put a command through the exec prefix — and a backend with no
command API at all can still host sandboxes. The timeout is the gateway's, because it is the
only party that knows what the caller asked for.
There is no publish, and no capability for one. A backend never
publishes a sandbox's ports. env_share serves one on
demand over the gateway's own tunnel, on the gateway's own domain, and that is the same
mechanism everywhere — so there is nothing for a connector to differ about.
There is no envd contract here either. One shared module holds
every variable a machine is booted with and the encode/decode pair for each. Every connector
calls it; none of them owns it. What a connector decides is only how to deliver that
map into the machine, and where to keep the declaration so list/0 can read it
back. See the envd contract.
The capability vocabulary
Closed, and short on purpose: a capability nothing reads is not a capability, and a fact about a backend that nothing acts on belongs in that connector's own documentation. Each entry says what the gateway does when it is absent — see Concepts for the table with the per-backend answers.
:oci_images · :sleep · :durable_disk ·
:persistent_root · :snapshots · :public_url
Seven more used to be on that list and are gone, because nothing read them. Every sandbox on
every backend serves ssh and files through the tunnel and only through the tunnel, so there was
nothing for a connector to claim; :wake was :sleep said twice; and the
rest described callbacks that are mandatory anyway.
Claiming :durable_disk or :persistent_root is a promise the
gateway repeats to agents verbatim. Under-promising is the safe direction, so a connector
unsure of its own answer should leave the capability off.
Per-connector facts
The four size letters mean four different computers on four different backends, and one backend does not offer all four.
| Fly | exe.dev | AWS | |
|---|---|---|---|
sizes/0 | s/m/l/xl, shared guests plus a volume | m/l/xl — exe.dev refuses a VM under 2 GB | s/m/l/xl, one instance family plus an EBS volume |
regions/0 | one, per deployment | [] — the account decides | one, per deployment |
default_image/1 | a constant in this app's registry | a setting, or the same sandbox image | a live lookup against Canonical's published parameter, per architecture |
prepare_image/1 | wrap, or boot as is | wrap, or boot as is | an AMI — envd is installed at first boot |
| transport | REST | one POST whose body is a CLI command line | the EC2 Query API |
| declaration read-back | machine metadata and config env | a file on the gateway's disk — exe.dev stores nothing for us | instance tags |
Asking for a size a backend does not have is refused with that backend's own table rather
than with a list of guests it does not offer. region is an opaque placement label
the gateway only ever prints — a Fly region code, an availability zone, whatever the account
defaulted to — and nothing is addressed by it.
Images
prepare_image/1 answers with a reference plus the connector's own account of
how it got there: as is (it already runs envd),
wrapped (pulled, given the envd layer, pushed under a new tag), or
envd installed at first boot (the backend boots something else entirely, like an
AMI). The gateway never wraps an image itself: whether an image needs a layer before it can be
a sandbox is a question only the thing that will boot it can answer.
A backend without :oci_images refuses an image: argument in a
sentence naming the provider, before anything is built — wrapping an image is minutes
of pulling and pushing, and a connector that cannot boot the result should not be paid for
first.
Two things a connector must get right
The machine environment must be present at every boot, not only the first. That is the load-bearing assumption behind declared services coming back after a sleep. One connector gets it from machine config, one writes a file its init system reads at every start. A connector that used a run-once mechanism — a cloud-init user-data script, say — would produce sandboxes whose services came back exactly once.
Errors become sentences at the source. A connector is the only party that knows what its vendor's codes mean, so it turns its own failure into a sentence and tags it as a provider error. The gateway has one clause for all of them and prints the sentence. There is no vendor-shaped error tuple and no vendor's credential variable named anywhere in the gateway's prose.
Testing one without an account
Both real connectors are unit-tested against a stubbed HTTP layer: the assertions are about the request that would have been sent — the URL, the body, the machine environment map — rather than about a machine that came back. That is how the AWS connector exists at all. It is complete and tested and has never been run against a real account, which is why it is called hypothetical. It was written to find out where the gateway had been secretly shaped like its first provider, and it found twelve places; every one of them is now resolved, and the list is kept in the repository as history.