The current project is a Python CLI tool that helps automate the deployment of VMs at various hosting providers (Digitalocean, Vultr, Google at present), along with the configuration of accociated DNS records and specification of cloud.init scripts. I noticed there is a new hosting provider called fly.io that looks to provide similar hosting services but on bare metal machines. Can you evaluate whether it would feasible to add support for fly.io? Their documentation is at : https://fly.io/docs/machines/api/
I'll evaluate this in two parts: understand the provider abstraction in this codebase, and research the fly.io Machines API. Let me start.
[
{
"type": "tool_reference",
"tool_name": "WebFetch"
},
{
"type": "tool_reference",
"tool_name": "WebSearch"
}
]1 from abc import ABC, abstractmethod 2 from dataclasses import dataclass 3 4 5 @dataclass 6 class VM: 7 id: str 8 name: str 9 tags: list 10 region: str 11 ip_address: str 12 status: str 13 14 15 @dataclass 16 class SSHKey: 17 id: str 18 name: str 19 fingerprint: str 20 public_key: str 21 22 23 @dataclass 24 class DNSRecord: 25 id: str 26 name: str 27 type: str 28 data: str 29 ttl: int 30 31 32 class CloudProvider(ABC): 33 @abstractmethod 34 def create_vm(self, name, region, image, size, ssh_key_names, tags, user_data) -> VM: 35 pass 36 37 @abstractmethod 38 def get_vm(self, vm_id) -> VM: 39 pass 40 41 @abstractmethod 42 def destroy_vm(self, vm_id) -> bool: 43 pass 44 45 @abstractmethod 46 def list_vms(self, tag=None) -> list: 47 pass 48 49 @abstractmethod 50 def get_ssh_key(self, name) -> SSHKey: 51 pass 52 53 @abstractmethod 54 def list_ssh_keys(self) -> list: 55 pass 56 57 @abstractmethod 58 def create_dns_record(self, zone, record_type, name, data, ttl, tag=None) -> str: 59 pass 60 61 @abstractmethod 62 def delete_dns_record(self, zone, record_name) -> bool: 63 pass 64 65 @abstractmethod 66 def get_dns_records(self, zone) -> list: 67 pass 68 69 @abstractmethod 70 def list_domains(self) -> list: 71 pass 72 73 def list_projects(self) -> list: 74 return [] 75 76 def assign_to_project(self, project_name, vm_id): 77 pass 78 79 def validate_region(self, region): 80 pass 81 82 def validate_image(self, image): 83 pass 84 85 @property 86 def provider_name(self) -> str: 87 return self.__class__.__name__ 88
{
"file_path": "/home/david/projects/stirlingbridge/machine/machine/providers/__init__.py"
}1 from machine.log import fatal_error
2 from machine.provider import CloudProvider
3
4 KNOWN_PROVIDERS = ["digital-ocean", "vultr", "gcp"]
5
6
7 def create_provider(provider_name, provider_config) -> CloudProvider:
8 if provider_name == "digital-ocean":
9 from machine.providers.digitalocean import DigitalOceanProvider
10
11 return DigitalOceanProvider(provider_config)
12 elif provider_name == "vultr":
13 from machine.providers.vultr import VultrProvider
14
15 return VultrProvider(provider_config)
16 elif provider_name == "gcp":
17 from machine.providers.gcp import GcpProvider
18
19 return GcpProvider(provider_config)
20 else:
21 fatal_error(f"Unknown provider: '{provider_name}'. Known providers: {', '.join(KNOWN_PROVIDERS)}")
22 {
"url": "https://fly.io/docs/machines/api/",
"prompt": "Summarize the fly.io Machines API: what resources can be managed (machines, apps, volumes), how machines are created (required fields, image types — are these Docker containers or full VMs?), authentication method, whether cloud-init/user-data is supported, how SSH keys work, how public IP addresses are assigned, and anything about DNS management. List the main API endpoints."
}{
"file_path": "/home/david/projects/stirlingbridge/machine/machine/providers/vultr.py"
}1 import base64
2 import time
3
4 from vultr import Vultr, VultrException
5
6 from machine.log import fatal_error, info
7 from machine.provider import CloudProvider, VM, SSHKey, DNSRecord
8
9
10 VALID_REGIONS = [
11 "ewr", "ord", "dfw", "sea", "lax", "atl", "ams", "lhr", "fra",
12 "sjc", "syd", "nrt", "cdg", "icn", "mia", "sgp", "sto",
13 "mex", "mad", "sao", "del", "hnl", "yto", "blr", "jnb",
14 "bom", "tlv",
15 ]
16
17
18 def _instance_to_vm(instance) -> VM:
19 return VM(
20 id=instance.get("id", ""),
21 name=instance.get("label", ""),
22 tags=instance.get("tags", []),
23 region=instance.get("region", ""),
24 ip_address=instance.get("main_ip", ""),
25 status=instance.get("status", ""),
26 )
27
28
29 class VultrProvider(CloudProvider):
30 def __init__(self, provider_config):
31 info("WARNING: Vultr support is experimental and has not been fully verified. Use with caution.")
32 if "api-key" not in provider_config:
33 fatal_error("Required key 'api-key' not found in 'vultr' section of config file")
34 self._api_key = provider_config["api-key"]
35 self._client = Vultr(self._api_key)
36
37 def create_vm(self, name, region, image, size, ssh_key_names, tags, user_data) -> VM:
38 sshkey_ids = []
39 for ssh_key_name in ssh_key_names:
40 ssh_key = self._get_vultr_ssh_key(ssh_key_name)
41 if not ssh_key:
42 fatal_error(f"Error: SSH key '{ssh_key_name}' not found in Vultr")
43 sshkey_ids.append(ssh_key["id"])
44
45 kwargs = {
46 "os_id": int(image),
47 "label": name,
48 "hostname": name,
49 "sshkey_id": sshkey_ids,
50 "tags": tags,
51 "backups": "disabled",
52 }
53 if user_data:
54 kwargs["user_data"] = base64.b64encode(user_data.encode()).decode()
55
56 try:
57 result = self._client.create_instance(region, size, **kwargs)
58 except VultrException as e:
59 fatal_error(f"Error creating instance: {e}")
60
61 return _instance_to_vm(result)
62
63 def get_vm(self, vm_id) -> VM:
64 try:
65 result = self._client.get_instance(vm_id)
66 except VultrException as e:
67 fatal_error(f"Error: machine with id {vm_id} not found: {e}")
68 return _instance_to_vm(result)
69
70 def destroy_vm(self, vm_id) -> bool:
71 # Vultr returns HTTP 500 if the instance is still pending or locked
72 # (e.g. during provisioning). Retry deletion with backoff.
73 for attempt in range(24):
74 try:
75 self._client.delete_instance(vm_id)
76 return True
77 except VultrException as e:
78 error_msg = str(e)
79 if "500" in error_msg and ("not currently active" in error_msg or "currently locked" in error_msg):
80 info("Waiting for instance to become ready before destroying...")
81 time.sleep(5)
82 elif "404" in error_msg:
83 return True # already gone
84 else:
85 fatal_error(f"Error: machine with id {vm_id} not found: {e}")
86 fatal_error(f"Error: timed out waiting to destroy instance {vm_id}")
87 return False
88
89 def list_vms(self, tag=None) -> list:
90 try:
91 params = {"tag": tag} if tag else None
92 result = self._client.list_instances(params=params)
93 except VultrException as e:
94 fatal_error(f"Error listing instances: {e}")
95 return [_instance_to_vm(i) for i in result]
96
97 def get_ssh_key(self, name) -> SSHKey:
98 key = self._get_vultr_ssh_key(name)
99 if not key:
100 return None
101 return SSHKey(
102 id=key["id"],
103 name=key["name"],
104 fingerprint=key.get("fingerprint", ""),
105 public_key=key.get("ssh_key", ""),
106 )
107
108 def list_ssh_keys(self) -> list:
109 try:
110 result = self._client.list_keys()
111 except VultrException as e:
112 fatal_error(f"Error listing SSH keys: {e}")
113 return [
114 SSHKey(
115 id=k["id"],
116 name=k["name"],
117 fingerprint=k.get("fingerprint", ""),
118 public_key=k.get("ssh_key", ""),
119 )
120 for k in result
121 ]
122
123 def create_dns_record(self, zone, record_type, name, data, ttl, tag=None) -> str:
124 try:
125 result = self._client.post(
126 f"/domains/{zone}/records",
127 type=record_type, name=name, data=data, ttl=ttl,
128 )
129 except VultrException:
130 info(f"Warning: DNS zone '{zone}' not found in Vultr, DNS record not set")
131 return None
132 record = result.get("record", result)
133 return record.get("id")
134
135 def delete_dns_record(self, zone, record_name) -> bool:
136 records = self.get_dns_records(zone)
137 for record in records:
138 if record.name == record_name:
139 try:
140 self._client.delete(f"/domains/{zone}/records/{record.id}")
141 except VultrException:
142 return False
143 return True
144 return False
145
146 def get_dns_records(self, zone) -> list:
147 try:
148 result = self._client.get(f"/domains/{zone}/records")
149 except VultrException:
150 info(f"Warning: DNS zone '{zone}' not found in Vultr")
151 return []
152 records = result.get("records", [])
153 return [
154 DNSRecord(
155 id=str(r.get("id", "")),
156 name=r.get("name", ""),
157 type=r.get("type", ""),
158 data=r.get("data", ""),
159 ttl=r.get("ttl", 0),
160 )
161 for r in records
162 ]
163
164 def list_domains(self) -> list:
165 try:
166 result = self._client.get("/domains")
167 except VultrException as e:
168 fatal_error(f"Error listing domains: {e}")
169 domains = result.get("domains", [])
170 return [d.get("domain", "") for d in domains]
171
172 def validate_region(self, region):
173 if region is not None and region.lower() not in VALID_REGIONS:
174 fatal_error(f"Error: region {region} is not one of {VALID_REGIONS}")
175
176 def validate_image(self, image):
177 try:
178 int(image)
179 except (ValueError, TypeError):
180 info(f"Warning: Vultr image (os_id) should be a numeric ID. Got: {image}")
181
182 def _get_vultr_ssh_key(self, name):
183 try:
184 result = self._client.list_keys()
185 except VultrException as e:
186 fatal_error(f"Error listing SSH keys: {e}")
187 for key in result:
188 if key.get("name") == name:
189 return key
190 return None
191
192 @property
193 def provider_name(self) -> str:
194 return "Vultr"
195 # Fly Machines API Summary Based on the provided documentation page, here's what can be determined: ## Manageable Resources The Machines API manages three primary resource types: 1. **Fly Machines** - the core compute units 2. **Fly Apps** - organizational containers grouping machines 3. **Fly Volumes** - persistent storage Additional resources include certificates (SSL/TLS) and tokens (OpenID Connect). ## Information Gaps Unfortunately, the page content provided is primarily a navigation/index page rather than detailed API documentation. It does not contain specific information about: - **Machine creation requirements** - No required fields or configuration details are specified - **Machine type** - Whether these are Docker containers or virtual machines is not mentioned - **Authentication methods** - Not described on this page - **Cloud-init/user-data support** - Not addressed - **SSH key management** - Not discussed - **Public IP assignment** - Not covered - **DNS management** - Not mentioned - **API endpoints** - The page references an "OpenAPI 3.0 specification for the Machines API" but the actual endpoints aren't listed here ## What the Page Confirms The documentation indicates that the "Machines resource" is described as "the core of the Machines API" used to "fully control Machines at speed," but detailed specifications would be found in the linked sub-pages or the OpenAPI specification itself. To get comprehensive answers, you would need to consult the detailed API documentation pages referenced in the navigation.
{
"url": "https://fly.io/docs/machines/api/machines-resource/",
"prompt": "How is a machine created? What is the image field (Docker/OCI image or OS image?), what config options exist (guest cpu/memory, env, services, metadata)? Is there support for cloud-init user-data? What endpoints exist (create, get, delete, list, start, stop)? What authentication is used?"
}{
"url": "https://fly.io/docs/networking/services/",
"prompt": "How do fly.io machines get public IP addresses? Are dedicated public IPv4 addresses available and how are they allocated (per app or per machine)? How does SSH access to a machine work — can you ssh directly with your own key? Anything about DNS hosting or DNS record management offered by fly.io?"
}# Fly Machines Creation and Configuration
## Creating a Machine
Machines are created via POST request to `/v1/apps/{app_name}/machines`. The operation requires a container image URI and optional configuration parameters. If successful, the Machine boots automatically unless `skip_launch` is set to true.
## Image Field
The `image` field specifies a "container registry path to the image that defines this Machine" such as `"registry-1.docker.io/library/ubuntu:latest"`. This is a Docker/OCI container image, not an OS image. Cloud-init user-data is not mentioned in the documentation.
## Configuration Options
Key config properties include:
- **Guest Resources**: `cpu_kind`, `cpus`, `gpus`, `memory_mb`
- **Environment**: `env` object for key/value pairs
- **Services**: Array defining network services with ports and handlers
- **Metadata**: `metadata` object for key/value pairs
- **Files**: Optional array for writing files into the Machine
- **Init**: `exec`, `entrypoint`, `cmd` for startup commands
- **Restart Policy**: Defines behavior after process exits
- **Health Checks**: Named checks supporting TCP and HTTP protocols
The documentation does not mention cloud-init support.
## API Endpoints
- `GET /v1/apps/{app_name}/machines` - List Machines
- `POST /v1/apps/{app_name}/machines` - Create
- `GET /v1/apps/{app_name}/machines/{id}` - Get details
- `POST /v1/apps/{app_name}/machines/{id}` - Update
- `DELETE /v1/apps/{app_name}/machines/{id}` - Delete
- `POST /v1/apps/{app_name}/machines/{id}/start` - Start
- `POST /v1/apps/{app_name}/machines/{id}/stop` - Stop
## Authentication
All requests require the `Authorization: Bearer ${FLY_API_TOKEN}` header.# Fly.io Public IP Addresses and Access ## Public IP Address Allocation Fly.io machines receive public IP addresses automatically: - **IPv6**: "A new Fly App running a public service configured in `fly.toml` automatically gets a dedicated Anycast IPv6 address when it's first deployed." - **Shared IPv4**: Apps are automatically assigned a shared Anycast IPv4 address on first deployment if configured for HTTP (port 80) or TLS/HTTP (port 443). These are free and routing is "based on your app's domain." ## Dedicated IPv4 Addresses Dedicated IPv4 addresses are available but cost monthly. They're allocated **per app**, not per machine. Use `fly ips allocate-v4` to request one. The document notes that "Shared IPv4 addresses are recommended unless you have an explicit need for your own IP," such as non-HTTP protocols, UDP services, or raw TCP handling. ## SSH Access The documentation doesn't address SSH key management or direct SSH access mechanisms. It only briefly mentions `fly ssh console -s` for accessing machine shells to check outbound IP addresses. ## DNS Hosting The document contains no information about Fly.io offering DNS hosting or DNS record management services. It only discusses custom domain configuration and DNS record setup as prerequisites for routing.
Bottom line: technically possible to wire in, but fly.io is a poor fit for what machine does. Roughly half of the CloudProvider interface has no native fly.io equivalent, and the tool's core workflow — boot an OS image, inject SSH keys, run a cloud-init script, point a DNS record at the VM's IP — doesn't map onto fly.io's model.
One premise worth correcting first: fly.io isn't a bare-metal provider. They run their own bare metal, but what they sell is Firecracker microVMs ("Machines") that boot from Docker/OCI container images (e.g. registry-1.docker.io/library/ubuntu:latest), not full OS images. That difference drives most of the friction below.
POST /v1/apps/{app}/machines, plus get/list/delete/start/stop) with Authorization: Bearer token auth. No SDK needed — plain requests would do, so no new dependency risk for the Python ≥3.8 / shiv build.create_vm / get_vm / destroy_vm / list_vms — all have direct endpoints. tags maps to machine metadata, region maps to fly regions, size maps to guest config (cpu_kind, cpus, memory_mb).validate_region / validate_image — straightforward.user_data — not supported at all. Machines boot a container entrypoint, not a cloud-init-enabled OS. The closest emulation is the files config (write files into the machine) plus a custom entrypoint, but that's a different contract than the cloud.init scripts this tool is built around. This is the biggest blocker since cloud-init specification is one of your three stated core features.fly ssh console over their WireGuard mesh with their own agent (hallpass); to get real sshd with your keys, you'd have to bake sshd and the public key into the container image. get_ssh_key/list_ssh_keys and the ssh_key_names argument to create_vm can't be implemented natively.VM.ip_address — which the DNS workflow presumably feeds — has no clean per-VM answer..fly.dev subdomains and certificate management for custom domains). create_dns_record, delete_dns_record, get_dns_records, and list_domains — four of the ten abstract methods — would have to be stubbed out or delegated to another provider.If the goal is "manage fly.io machines from this CLI," a provider could be written in a day or two using the four VM CRUD methods and stubs elsewhere — similar in spirit to how GCP presumably omits some pieces. But users would get machines they can't cloud-init, can't ssh into with their configured keys, and can't point DNS at a per-machine IP. I'd recommend against adding it unless you're willing to reframe fly.io support as a container-deployment mode rather than a VM provider. If you want a provider with a similar "modern API, runs its own hardware" flavor that does fit the abstraction (real VMs, cloud-init, SSH keys, per-VM IPs), Hetzner Cloud or Linode would slot into the existing interface almost 1:1.
Sources: Machines API index, Machines resource, Networking services
Ok that's interesting, and swiftly cut through the marketing fog for me! So fly.io basically offers a kind of "remoted WSL2" service for desktop users?
That analogy is half right, and the half that's right is actually the clever part of their architecture — but the "for desktop users" half is off.
Where the analogy works: like WSL2, fly.io gives you something that feels like a container (you ship a Docker image, you get a filesystem and a process) but is actually a lightweight hardware-virtualized VM underneath (Firecracker microVMs in their case, Hyper-V in WSL2's). In both cases the point is container ergonomics with real VM isolation boundaries. So mechanically, yes: "container UX on a hidden microVM" is exactly the trick both play.
Where it breaks down: the audience and purpose. WSL2 is an interactive environment — you get a shell, you develop in it, it's a pet. Fly.io machines are meant to be cattle: you deploy a server application image, their anycast network routes user traffic to whichever region the request lands near, machines auto-stop when idle and cold-start in milliseconds when a request arrives. The whole product is "run your web app close to your users in 30+ regions without doing ops," competing with Heroku's ghost, Render, Railway, and Cloudflare Workers — not with a remote dev box.
That said, people do bend it into the remote-dev-environment shape you're describing (bake sshd into an image and treat it as a disposable cloud workstation), and the per-second billing plus stop-when-idle makes that cheap. It's just not what the product is aimed at — which circles back to yesterday's conclusion: it's an app-deployment platform wearing VM clothing, not a VM provider wearing container clothing, and that's why it fights your tool's abstraction.
Ah ok. I understand better, but still not quite getting it. I get the "VM from docker image" aspect -- I've worked with several products that supported that in the past, and it's a good move in terms of beefing up isolation but retaining familiar tooling and artifacts. I'm still not totally seeing how you'd use this for a production service -- if there's no DNS support how is client traffic routed to my FAAS firecracker instances?
Good question — the answer is that fly.io moves the routing job out of DNS entirely and into anycast IP routing plus their edge proxy. DNS becomes a one-time, nearly-static pointer instead of the load-balancing mechanism.
To be precise about what I said earlier: fly.io has no DNS hosting — no API to manage record zones, which is why your tool's create_dns_record methods have nothing to call. Your apps absolutely are reachable by name; it just works differently:
Every app gets one anycast IP (plus a free yourapp.fly.dev hostname pointing at it). "Anycast" means that same IP address is BGP-advertised from every fly.io edge location simultaneously. There's nothing to keep updated — the IP never changes no matter where your machines run.
The internet does the geo-routing. When a client in Tokyo connects to that IP, BGP naturally delivers the packets to fly's Tokyo edge; a client in Frankfurt hits the Frankfurt edge. This is the same trick CDNs and public DNS resolvers (1.1.1.1, 8.8.8.8) use. No GeoDNS, no per-region hostnames.
fly-proxy at the edge does the last hop. The proxy terminates TLS, reads the SNI/Host header to figure out which app the request is for (this is how thousands of apps share one IPv4), then forwards the connection over fly's private WireGuard backbone to the nearest region where you have a machine. If your machines there are stopped, it cold-starts one (sub-second, since Firecracker + a pre-pulled image) and then delivers the request. That proxy-triggered wake-up is the FaaS-like scale-to-zero behavior.
Your custom domain is a single static record at whatever DNS provider you already use — an A/AAAA record pointing at the anycast IP, or a CNAME to yourapp.fly.dev. You set it once; fly issues a Let's Encrypt cert for it. That's why they never built DNS hosting: in their architecture DNS carries no operational load worth managing.
Contrast with the model your machine tool automates: there, each VM has its own IP, so DNS records are per-machine state that must be created and destroyed with the VM. On fly.io the machine fleet can scale, move regions, or stop entirely and the public addressing never changes — the proxy layer absorbs all of that churn.
The trade-off is that fly-proxy sits in front of everything, so you're getting HTTP/TLS/TCP-handler routing whether you want it or not. For raw protocols or "I just want a box with an IP," you're back to paying for a dedicated IPv4 per app — and back to the awkward fit we identified for your tool.
Interesting. They're marketing the "sprites" service as "to run your AI agents", which seems quite different from that edge FAAS use case you described.
Web search results for query: "fly.io Sprites AI agents product sprites.dev"
Links: [{"title":"Fly.io debuts Sprites: Persistent VMs that let AI agents keep their state - SDxCentral","url":"https://www.sdxcentral.com/news/flyio-debuts-sprites-persistent-vms-that-let-ai-agents-keep-their-state/"},{"title":"Fly’s new Sprites.dev addresses both developer sandboxes and API sandboxes at the same time","url":"https://simonwillison.net/2026/Jan/9/sprites-dev/"},{"title":"Fly.io introduces Sprites: lightweight, persistent VMs to isolate agentic AI","url":"https://devclass.com/2026/01/13/fly-io-introduces-sprites-lightweight-persistent-vms-to-isolate-agentic-ai/"},{"title":"Sprites (Fly.io) | Ry Walker Research | Ry Walker","url":"https://rywalker.com/research/sprites"},{"title":"Fly's new Sprites.dev addresses both developer sandboxes and API sandboxes at the same time","url":"https://www.alldevblogs.com/article/simon-willison/flys-new-spritesdev-addresses-both-developer-sandboxes-and-api-sandboxes-at-the-same-time"},{"title":"Let’s talk about Fly.io Sprites aka stateful sandboxes | by Kevin Tewouda | Medium","url":"https://lewoudar.medium.com/lets-talk-about-fly-io-sprites-aka-stateful-sandboxes-509796942fdd"},{"title":"Turn And Face The Strange · The Fly Blog","url":"https://fly.io/blog/kurt-scott-money-sprites/"},{"title":"Sprites — Stateful sandbox environments","url":"https://sprites.dev/"}]
Based on the search results, here's what I found about Fly.io Sprites:
## Overview
Fly.io has introduced Sprites, VMs (virtual machines) based on the AWS-sponsored Firecracker which are designed to isolate coding agents while also providing long-running environments within which they can work.
## Key Features
A Sprite-based AI agent can install packages once, work on multiple tasks, access logs from running apps, and pick up exactly where it left off, even days after initial switch-on. A new Sprite comes online in 1-12 seconds.
Sprites are Fly.io's stateful sandbox environments with instant creation, ~300ms checkpoints, native MCP support, and object-storage-backed persistence for AI agents.
## Use Cases
Fly.io's new Sprites.dev product provides stateful, checkpointable sandbox environments designed to solve two key problems: securely running AI coding agents in 'YOLO' mode and providing an API for executing untrusted code.
## Launch
Launched in January 2026, they are publicly available with usage-based billing.
REMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.Your instinct from two messages ago turns out to be spot-on — Sprites (launched January 2026) is essentially fly.io productizing the "remoted WSL2" use case I said their main platform wasn't aimed at.
It's the same substrate — Firecracker microVMs on their hardware — but the operating model is flipped from the edge-FaaS story:
apt install things, clone a repo, leave logs lying around, and come back days later to exactly where it left off. Cold-start is 1–12 seconds, and instead of scale-to-zero-by-discarding, it checkpoints the whole environment (~300ms) to object storage and restores on demand. So you get persistent-VM semantics while only paying for actual compute time.The reason it's marketed at AI agents specifically is that agents created a sudden mass market for exactly this shape of thing: you want to let a coding agent run with dangerous permissions ("YOLO mode") or execute untrusted generated code, so you need hard VM-grade isolation — but the agent also needs a durable working environment across sessions, not a fresh stateless container per request. Interactive terminal/SSH-style access matters more than an anycast IP, and per-second billing with instant suspend matters because agents are bursty. Sandboxed + stateful + cheap-when-idle is precisely the gap between "serverless function" and "rented VM," and Simon Willison's writeup notes it doubles as a general untrusted-code-execution API, not just agents.
So the corrected picture of fly.io: one microVM substrate, two products on top — the anycast/proxy edge platform for serving production traffic, and Sprites for isolated, persistent, interactive workspaces. Notably, Sprites is even less of a fit for your machine tool than the Machines API — no public per-VM IP or DNS story at all — but it's a much better fit for the thing your earlier analogy described.
Sources: Sprites.dev, Simon Willison's review, SDxCentral coverage, devclass coverage, fly.io blog
Ah ok, it's all coming into focus now. Like many things it's easier to understand from the provenance and history of how it came to be. They began with the edge/FAAS service and someone had the brilliant idea later that they could make some small changes and thereby have the "Linux on a rope" service to address malicious LLM concerns.
That's exactly the shape of it — "Linux on a rope" is a better tagline than anything in their marketing. A couple of refinements to the provenance story, because it makes the picture even sharper:
The pivot pattern actually runs twice. Fly.io originally started (circa 2017) as an edge proxy/CDN-ish product — run your JavaScript at the edge, in the Cloudflare Workers vein. It didn't take off, and their first big pivot was "what if you could run your whole app at the edge, not just snippets" — which is what drove them to Firecracker microVMs booted from Docker images, since that let arbitrary existing apps run with hard isolation and fast starts. The anycast/fly-proxy edge platform we discussed was pivot one.
Sprites is pivot two, and your read of the mechanism is right: they looked at what they'd already built and realized the crown jewels weren't the part they'd been selling. The edge/anycast routing — the whole original point — is irrelevant to Sprites. What turned out to be valuable was the boring substrate underneath: the ability to conjure a genuinely isolated Linux VM from a container image in about a second, run it densely and cheaply, and bill by the second. For years that was merely plumbing for the FaaS story; then agentic AI showed up and suddenly "instant disposable-but-persistable Linux boxes at scale" was the product, with the malicious-LLM/untrusted-code concern as the demand driver. Kurt Mauer's blog post about the shift is literally titled "Turn and Face the Strange" — they're quite open that this is a repositioning of existing assets toward where the money moved.
The one place I'd amend "small changes": the checkpoint/restore-to-object-storage machinery (~300ms snapshots of a running VM's full state) was real new engineering, not just a repackaging — it's the piece that converts "cheap ephemeral VM" into "persistent pet you only pay for while it's awake." But architecturally, yes: same Firecracker substrate, same image pipeline, same fleet — different customer, different story, and the second story arguably fits what they built better than the first one did.