The Lightspeed manual
Implement an environment provider
An environment provider supplies managed compute through Lightspeed’s public environment protocol. It translates requests such as creating, waking, or closing an environment into the infrastructure it controls, then makes that environment’s filesystem and process service reachable.
The provider depends on the protocol boundary. It does not need Lightspeed’s database, API implementation, engine, or Temporal runtime. The included Incus provider demonstrates that separation. If you only need to connect an existing machine, outbound daemon registration already provides that path without implementing a provisioning controller.
Separate control from execution
The controller manages targets: machines or other units of compute owned by the provider. The data endpoint performs filesystem, process, and job operations inside a selected target. A target that exists in infrastructure is not necessarily ready to serve that data protocol.
flowchart LR Runtime[Lightspeed runtime] -->|controller requests| Provider[Provider service] Provider -->|provision and power| Infrastructure[Compute infrastructure] Runtime -->|target data route| Provider Provider -->|data protocol| Daemon[Daemon in target] Infrastructure --> Target[Target machine] Target --- Daemon
The current hosted controller implementation supports WebSocket transport.
Public transport enums also name other transports, but that does not mean
the runtime implements them for registered controllers. Use a controller URL
ending in /control, optionally under a path prefix. The runtime derives a
sibling data route with this form:
/routes/{universe}/{binding}/{environment}/{incarnation}/{target}Your provider must serve that route and reach the appropriate target’s data endpoint. This is distinct from the public outbound registration routes used by bring-your-own daemons.
The current connection configuration does not supply an application bearer token or arbitrary authentication headers to provider controllers. The Incus implementation relies on its protected deployment network and transport boundary. Plan that boundary explicitly; a provider URL field alone is not an authorization mechanism for a publicly exposed controller.
Start with the protocol types
The environment protocol crate defines transport-independent controller and data messages. The current protocol version is 2. Messages use their defined camelCase fields and standard base64 for byte payloads. The initialization handshake establishes protocol compatibility and advertised capabilities; implementation/build information helps diagnostics but does not authenticate a peer.
The typed client and Incus provider are useful implementation references. Keep their internal infrastructure choices separate from the public wire contract. In particular, the runtime’s internal environment resolver is policy inside Lightspeed, not an interface a third-party provider needs to implement.
Implement controller operations
Advertise only the capabilities your controller can fulfill:
| Operation | Provider responsibility |
|---|---|
controller/initialize |
Negotiate the protocol and describe controller capabilities. |
controller/listTemplates |
Return approved templates available to the supplied universe/binding. |
controller/createTarget |
Create or reconcile a target from stable request and environment identity. |
controller/listTargets, controller/getTarget |
Report observed targets and state within that binding. |
controller/adoptTarget |
Explicitly transfer an existing target into managed ownership, when supported. |
controller/setTargetPower |
Converge toward an advertised power state and report actual observation. |
controller/closeTarget |
Remove the owned target; repeated closure should observe it as closed. |
controller/ensureIngress, controller/removeIngress |
Apply or remove provider-approved application exposure, when supported. |
A create request carries identity and a template selection. For example:
{ "binding": { "bindingId": "primary", "universeId": "00000000-0000-0000-0000-000000000001" }, "environmentId": "environment-1", "incarnationId": "incarnation-1", "requestId": "request-1", "templateId": "lightspeed-dev-v1"}These are controller method parameters, not a public Lightspeed API request. They come from the protocol’s create-target fixture. The request does not carry an arbitrary image, private address, or cloud-init document. The provider’s template policy determines what the caller may provision.
Make ownership survive retries
An ambiguous infrastructure response is normal: a create request can succeed while its response is lost. Record enough ownership information to find that same target on retry. Include universe, binding, environment, incarnation, request, and template identity, and reject conflicting reuse.
Every mutation and data route must verify the target belongs to the supplied binding and incarnation. Knowing a native target ID is insufficient. An old incarnation must not gain access to the replacement environment or destroy it during delayed cleanup.
Incus uses deterministic names and user.lightspeed.* ownership metadata on
native resources. This allows the provider process to restart and reconcile
without a private database. Another provider can use different persistence,
but must preserve the same ownership and retry properties.
Adoption transfers ownership deliberately. In the Incus implementation, it replaces the source VM’s networking/profiles with the managed binding policy, and a later close destroys the adopted VM. Document your provider’s adoption effects as part of its contract rather than presenting adoption as a harmless label change.
Report readiness and power honestly
Target status can distinguish creating, starting, ready, paused, suspended, stopped, closing, closed, failed, and unknown. Report infrastructure observation and protocol readiness, not merely the desired state requested by a caller. Advertise only the supported steady power states.
The Incus provider probes the guest data handshake before reporting a running VM as ready. Until that handshake succeeds, the target remains starting. Apply the same distinction when your infrastructure reports a machine running before its daemon, filesystem, or network is usable.
Ingress follows provider policy. The controller request identifies an owned
target; it does not let callers choose arbitrary ports or upstream addresses.
A template advertises publicIngress, and the provider decides the approved
port and endpoint. Return the ready public endpoint or disabled state and
remove access when ingress or the target is closed. See
Networking and ingress for the
user-facing behavior this must support.
Supply the data endpoint
Reusing lightspeed-envd inside a target avoids implementing the entire data
surface again. The daemon already provides filesystem confinement, process
groups, output cursors, PTYs, background jobs, credential transfer, and idle
observation. Your provider still owns routing and target lifecycle.
A custom data server must accept initialize, return a compatible version,
connection ID, capabilities, and implementation information, then accept the
initialized notification. A default working directory is optional in the
response. Its real capabilities must agree with the
controller’s target summary. Declaring support for an operation creates an
expectation that the operation behaves as the protocol specifies.
Pay attention to the state that survives an individual socket:
| Area | Behavior to preserve |
|---|---|
| Filesystem | Explicit paths/root confinement, bounded reads/searches, write semantics, and typed errors. |
| Processes | Caller-owned process IDs, input/termination, retained output, and cursor behavior across connections. |
| Jobs | Namespace/request/job identity, dependency and queue policy, idempotent submission, and retained results. |
| Credentials | Per-call secret handling without logging or persisting injected values in the job specification. |
| Idle observation | Monotonic time since real work, together with running-work and leftover-process information. |
For processes, an explicit afterSeq rereads retained output without advancing
the daemon-owned cursor; omitting it uses that cursor. waitMs controls how
long an observation waits, while timeoutMs is an execution deadline. Treating
both as process-kill settings would break callers that poll long-running work.
The shipped daemon retains process/job state beyond a socket but does not
implement connection resumption through resumeConnectionId; initialization
returns a fresh connection ID. Do not infer durable process survival across a
daemon restart from socket reconnection behavior. Processes and jobs
explains that separate lifecycle.
secretEnv contains plaintext secret values while crossing the data protocol.
Protect the transport and keep those values out of logs, provider metadata,
and workflow history. Redacted debug formatting is not encryption. The
credential guide describes the binding and
injection policy surrounding this transport.
env/idle reports a monotonic idle duration. Running processes and jobs count
as work; handshake and idle probes do not. Leftover process groups after their
root exits are reported separately and do not automatically block idle power
policy. That distinction lets policy choose an appropriate freeze or stop
behavior without treating observation itself as activity.
Return the protocol’s string-coded errors, such as notFound, forbidden,
conflict, unsupported, or capabilityUnavailable. These are distinct from
the public Lightspeed API’s numeric JSON-RPC error mapping.
Test the provider boundary
Start with serialization fixtures and the typed client’s expectations. The protocol and client unit suites can run without provisioning infrastructure:
cargo test -p environment-protocolcargo test -p environment-clientThe existing assert_environment_data_conformance helper in
environment tooling
exercises the handshake and generic filesystem operations, mapped errors, and
forbidden-path confinement. Run it from a test harness against a disposable
data endpoint and directory; it creates and removes test files. Keep that
harness separate from the provider’s production protocol dependencies.
This is a useful compatibility check, not a complete controller/process/job test suite. Also exercise ambiguous create retries, conflicting request identity, cross-binding access, stale incarnations, daemon startup lag, disconnect/reconnect, duplicate process/job submissions, advertised power transitions, and ingress teardown. Infrastructure-backed tests need a deliberately configured disposable account or project.
Register and verify it in Lightspeed
Register the provider through operator/environment-providers/put, create an
enabled universe binding, and inspect the returned templates. The
Incus setup guide shows the current registration
and binding procedure; use your provider’s controller URL and templates.
Create an environment, wait for protocol readiness, read/write a disposable file, and run a harmless command. Then exercise each advertised power state, wake it, and close it. Confirm the infrastructure target and any ingress are actually removed. Verify that retrying the same create does not allocate a second machine and that stale identity cannot access a replacement.
| Symptom | Likely boundary |
|---|---|
| Provider registration succeeds but calls fail | Supported WebSocket transport, /control URL shape, and private reachability. |
| VM runs but environment never becomes ready | Guest daemon startup and data handshake, including the derived route. |
| Retrying create allocates another target | Missing persisted ownership/request reconciliation. |
| A supported tool returns an unsupported-operation error | Advertised capabilities disagree with the data implementation. |
| A closed target still has public access | Ingress cleanup and infrastructure deletion need reconciliation. |