Claude Code transcript - page 1/2

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/

Thinking

I'll evaluate this in two parts: understand the provider abstraction in this codebase, and research the fly.io Machines API. Let me start.

ToolSearch
{
  "query": "select:WebFetch,WebSearch",
  "max_results": 2
}
[
  {
    "type": "tool_reference",
    "tool_name": "WebFetch"
  },
  {
    "type": "tool_reference",
    "tool_name": "WebSearch"
  }
]
Read
{
  "file_path": "/home/david/projects/stirlingbridge/machine/machine/provider.py"
}
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	
Read
{
  "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	
Thinking
WebFetch
{
  "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."
}
Read
{
  "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.
Thinking
Thinking
WebFetch
{
  "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?"
}
WebFetch
{
  "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.
Thinking

Feasibility assessment: fly.io provider

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.

What maps cleanly

  • API mechanics: a simple REST API (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.

What breaks

  • cloud-init / 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.
  • SSH keys — fly.io has no SSH key resource. Access is via 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.
  • Public IPs — allocated per app, not per machine, and they're anycast addresses (shared IPv4 by default; dedicated IPv4 costs extra and is still app-scoped). Machines themselves only have private 6PN IPv6 addresses. VM.ip_address — which the DNS workflow presumably feeds — has no clean per-VM answer.
  • DNS — fly.io offers no DNS hosting or record-management API (only .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.
  • App container concept — every machine must live inside a "Fly App" (which is also the unit of IP allocation and internal DNS), so the provider would need an app-management strategy (one app per VM vs. one shared app) that has no analogue in the current abstraction.

Verdict

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?

Thinking

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?

Thinking

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Thinking

Let me check what Sprites actually is before I characterize it — it's a newer product.

WebSearch
{
  "query": "fly.io Sprites AI agents product sprites.dev"
}
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.
Thinking

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:

  • Machines/apps model: stateless cattle, image is the source of truth, anycast + fly-proxy routes inbound user traffic, machines are disposable and interchangeable.
  • Sprites model: each sandbox is a pet with a name. It keeps its filesystem state — an agent can 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.

Thinking

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.