# ol.smolvm next Run local and cloud microVMs from Clojure and Babashka ## ol.smolvm.cloud.workflow # ol.smolvm.cloud.workflow Explicit asynchronous cloud workflows built from single-request operations. Every function returns a java.util.concurrent.CompletableFuture of its documented value. Validation, request and workflow failures complete it exceptionally. Deref wraps failures in ExecutionException. No blocking workflow variant or opaque Machine/Job/Episode handle is introduced. Normal success or exceptional completion occurs after required cleanup attempts and local stream closure. Cancelling the returned future marks it cancelled immediately, stops further ordinary scheduling, and requests cancellation of active work where possible. Required cleanup can continue after cancellation. A cancelled future is not proof of remote rollback, completed cleanup, or termination of commands/jobs. A timed deref does not cancel the workflow. Cleanup failures after cancellation cannot replace the cancelled result. Callers needing observable cleanup outcomes should await normal completion and use explicit remote cleanup plus verification, rather than treating future cancellation as a cleanup acknowledgement. Unknown mutation outcomes require reconciliation. Late child IDs received after cancellation still trigger the documented branch cleanup. HTTP failures use sanitized invoke error data. Resource IDs may accompany errors, but credentials, signed URLs, task data and response bodies do not. No retries occur except checkpoint part PUT retries. Options are open maps. Client configuration is explicit. All implementation stays under impl. ## wait-until-ready ```clojure (wait-until-ready client id) (wait-until-ready client id opts) ``` Polls machine `id` until it is ready. Returns a future of its latest info map. Ready means server `:ready true`. When `:ready` is absent, state `running` or `started` is accepted for older servers. States error, stopped and deleted fail with INVALID_STATE when not ready. Request errors propagate immediately. | | | | --- | --- | | Option | Meaning | | `:timeout-ms` | Positive total polling budget. Defaults to 120000. | | `:interval-ms` | Positive interval after an incomplete probe. Defaults to 1000. | | `:probe-agent?` | Boolean, defaults to false. After two seconds, a running machine without published ports may pass by successfully executing `true`. | Poll immediately. Use a monotonic deadline and bound each request wait by the remaining budget. Timeout completes the future exceptionally with TIMEOUT. A successful agent probe returns the latest info without inventing `:ready true`. This is not application health. No start, resume, deletion or automatic transport retry is performed. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L31-L49) --- ## connect ```clojure (connect client id-or-name) ``` Resolves `id-or-name` to a future of machine info, without starting it or waiting. Fetch the exact ID first. Only HTTP 404 or NOT_FOUND triggers a list lookup for an exact name. No match throws NOT_FOUND. Multiple matches throw AMBIGUOUS_NAME. Other failures propagate. No machine handle is created. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L51-L58) --- ## branch-exec ```clojure (branch-exec client id name command) (branch-exec client id name command opts) ``` Creates child `name` from source `id`, waits, executes `command`, then deletes it. Returns a future of the ordinary cloud exec result. Nonzero exits are results. The source is never deleted. Only a child ID returned by successful creation is eligible for cleanup, never a guessed ID or a pre-existing name. | | | | --- | --- | | Option | Meaning | | `:exec` | Open map of cloud exec options. Defaults to `{}`. | | `:wait` | Open map of [`wait-until-ready`](#wait-until-ready) options. Defaults to `{}`. | | `:cleanup-timeout-ms` | Positive delete-request budget. Defaults to 60000. | Cleanup is attempted after success, failure, timeout or cancellation with its separate budget. Normal completion waits for that attempt. Cleanup failure after execution success completes exceptionally with CLEANUP_FAILED and :machine-id. If execution or waiting also failed, that failure is primary and cleanup failure is suppressed. Cancellation is immediate and cleanup may continue afterward, including after a late creation response supplies an ID. No mutation is retried. An ambiguous creation response requires reconciliation. Source and child must satisfy the server’s branch prerequisites. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L60-L82) --- ## publish ```clojure (publish client id port) ``` Adds guest TCP `port` to machine `id`. Returns a future of updated machine info. Reads the current ports, preserves other guest ports, and replaces the set only when needed. Host ports are allocated by the server. A missing requested port in the reply throws INVALID_REPLY after the update may have succeeded. Serialize all port updates for a machine. This read/replace is not atomic. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L84-L92) --- ## unpublish ```clojure (unpublish client id port) ``` Removes guest TCP `port` from machine `id`. Returns a future of updated machine info. Reads the current ports and replaces the set only when the port is present. Other guest ports are preserved. Serialize all port updates for a machine. This read/replace is not atomic and is not retried. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L94-L101) --- ## job-wait ```clojure (job-wait client id job-id) (job-wait client id job-id opts) ``` Waits for `job-id` on machine `id` to exit. Returns a future of its final info map. Uses successive job-info long polls of at most 30 seconds. State `exited` succeeds even for nonzero exit status. State `lost` throws JOB_LOST. Invalid states or missing terminal exit status throw INVALID_REPLY. | | | | --- | --- | | Option | Meaning | | `:timeout-ms` | Positive total wait budget. Defaults to 120000. | Each request wait is bounded by the remaining monotonic budget. Timeout throws TIMEOUT. Future cancellation and timeout do not send job-cancel. Request errors are not retried. A nonterminal immediate reply is followed by a 100 ms pause. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L103-L118) --- ## job-read-all ```clojure (job-read-all client id job-id) (job-read-all client id job-id opts) ``` Collects one output stream of `job-id` on machine `id` through EOF. Returns a future of a byte array. Reads pages in order, using returned byte offsets. Empty nonterminal pages are long-polled again. Nonadvancing data or regressing offsets throw INVALID_REPLY. An empty nonterminal reply is followed by a 100 ms pause before another long poll. This does not combine stdout and stderr. | | | | --- | --- | | Option | Meaning | | `:stream` | `:stdout` or `:stderr`. Defaults to `:stdout`. | | `:timeout-ms` | Positive total read budget. Defaults to 120000. | | `:max-bytes` | Positive aggregate size limit, at most Integer/MAX_VALUE. Defaults to 16777216. | Uses pages at most 1048576 bytes and long polls at most 30 seconds. Exceeding the aggregate limit throws LIMIT_EXCEEDED without returning partial output. Timeout throws TIMEOUT. Workflow timeout or future cancellation does not cancel the remote job. Streaming larger output is possible with the existing job-output builder. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L120-L139) --- ## file-write ```clojure (file-write client id path data) (file-write client id path data opts) ``` Uploads `data` to absolute guest `path` on machine `id`, then optionally chmods it. Accepts the same text, bytes, File or Path data as cloud/file-write. Returns a future of nil. This is not atomic. A chmod failure leaves the uploaded file in place. | | | | --- | --- | | Option | Meaning | | `:mode` | Integer permission bits 0 through 07777. Omission skips chmod. Zero explicitly removes permissions. | Chmod uses argv, not a shell string. A nonzero chmod exit throws EXEC_FAILED. No upload or chmod retry is performed. Use cloud/file-write for one request. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L141-L154) --- ## checkpoint-upload ```clojure (checkpoint-upload client path) (checkpoint-upload client path opts) ``` Uploads local checkpoint `path` using service-allocated raw PUT parts. `path` is a nonblank string, File or Path naming a nonempty regular file. Returns a future of completed checkpoint metadata. Checks metadata and file readability before remote allocation. Opens file sections during part invocation and closes every section on completion. Keep the file unchanged until normal completion or until cancelled work has released its resources. | | | | --- | --- | | Option | Meaning | | `:parallel` | Concurrent part uploads, 1 through 16. Defaults to 4. | | `:part-attempts` | Attempts per part, 1 through 4. Defaults to 4. | | `:timeout-ms` | Positive total workflow budget. Defaults to 1800000. | | `:progress` | Optional function of aggregate bytes read and total file bytes. Called serially after positive part reads and failed-attempt resets. | Allocation and completion each make one API request. Only part PUTs retry. Match pinned Go: transport failures and every non-2xx part response retry, bounded by part-attempts, the total deadline and cancellation. Default four attempts use delays of 2, 4 and 8 seconds. Each attempt opens a fresh exact file section. Validation/file errors and progress callback failures do not become HTTP retries. No additional attempt starts after cancellation. Track bytes read separately for each part and serialize aggregate callbacks. Each positive sender read updates that part. A failed attempt resets its count to zero and reports the new aggregate before a retry or final failure. Aggregate progress can move backward and can reach total before storage accepts the data. It measures reads, not acknowledged bytes. No synthetic initial or completion callback is promised. Callback failure stops further scheduling and completes exceptionally with PROGRESS_FAILED after active work is cancelled/settled and local resources close. No callback exception message or cause is exposed. Presigned URLs must be HTTPS, with no user info or fragment. Storage requests contain no API bearer, cookies or API headers and never follow redirects. Each PUT has its exact content length. Plan sizes and part counts are checked before transfer. URLs and provider error bodies are not exposed in failures. Completion sends no ETags. No automatic reallocation or completion retry occurs. Timeout stops scheduling, cancels active work where possible and completes exceptionally after active operations settle and local resources close. Explicit future cancellation is immediate. Local cleanup may finish later. Progress callbacks can finish during that cancellation cleanup, but none run after all active work settles. Partial uploads are not automatically deleted. Normal failures identify the checkpoint when its ID is known. Checkpoint deletion exists, but abort/deletion of incomplete uploads is not established. Cancelled futures cannot deliver a newly learned checkpoint ID. Use raw steps when recording the allocation before transfer is required. The remote result of an interrupted mutation can be uncertain. This is raw multipart object upload, not multipart/form-data. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud/workflow.clj#L156-L207) ## ol.smolvm.cloud # ol.smolvm.cloud Describe cloud operations as request maps, then send them with `invoke`. Operation builders are pure. [`invoke`](#invoke) returns a CompletableFuture that resolves to each operation’s documented result. Machine arguments are exact IDs. JSON record keys become kebab-case keywords. Environment and label keys keep their spelling. State values and timestamps are strings. ## client ```clojure (client opts) ``` Returns validated connection configuration from `opts`. | | | | --- | --- | | Option | Meaning | | `:base-url` | Cloud API URL. Defaults to `https://api.smolmachines.com`. | | `:api-key` | Required bearer token. | | `:request-timeout-ms` | Default HTTP request timeout. Defaults to 60000. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L12-L21) --- ## invoke ```clojure (invoke client operation) ``` Starts the HTTP request described by `operation` using `client`. Returns a `java.util.concurrent.CompletableFuture` of the decoded result. Use `@result` to wait, or `(deref result timeout-ms timeout-value)` to bound the wait. The request continues after a wait timeout. HTTP and transport failures complete the future exceptionally with ex-info containing `:code`, plus `:status` and `:request-id` when available. Deref wraps failures in ExecutionException. Its cause carries the error data. Empty successful responses resolve to nil. An operation’s `:timeout` overrides the client’s default HTTP timeout. Use [`job-cancel`](#job-cancel) to terminate a remote job. Additional result kinds, with the same one-request CompletableFuture contract: exec-stream resolves to a Closeable event reader. Call stream-read to consume its response, then close it. Cancellation before delivery closes a late body. checkpoint-upload-part sends one raw PUT to its signed URL, without API bearer, cookies, redirects or automatic retries. Its file section is opened during invocation and closed on completion/failure/cancellation, including failure before the request starts. The future resolves to nil after a successful PUT. Cancellation cannot prove that the remote part was not stored. Absolute storage URLs are accepted only for the checkpoint part operation, not as a general override to authenticated API operations. All new operations keep existing sanitized errors. File paths, signed URLs, lease tokens and progress callback exception messages/causes are not attached to errors. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L23-L53) --- ## list ```clojure (list) (list opts) ``` Describes listing machines. Result: a vector of full info maps. | | | | --- | --- | | Option | Meaning | | `:labels` | String-to-string map. Match every label. Defaults to `{}`. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L55-L62) --- ## info ```clojure (info id) ``` Describes fetching machine `id`. Result: its full info map. Fields include `:id`, `:name`, `:state`, `:ready`, `:source`, `:resources`, `:ports`, `:labels`, and `:created-at` when supplied by the server. Returned environment values are redacted by the service. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L64-L71) --- ## create ```clojure (create config) ``` Describes creating a machine from `config`. Result: the server’s machine-info record, including its state. Resource allocation defaults come from the server. | | | | --- | --- | | Option | Meaning | | `:image` | Required OCI image reference. | | `:name` | Machine name. Defaults to a server-generated name. | | `:cpus` | Positive CPU count. Maps to `resources.cpus`. | | `:memory-mb` | Positive memory allocation. Maps to `resources.memoryMb`. | | `:storage-gb` | Positive disk allocation. Maps to `resources.diskGb`. | | `:env` | String-to-string workload environment. Defaults to the image environment. | | `:workdir` | Workload working directory. Defaults to the image setting. | | `:labels` | String-to-string metadata map. Defaults to `{}`. | | `:ports` | Vector of guest TCP port numbers. Defaults to `[]`. Host ports are allocated by the server. | | `:network?` | True allows outbound networking, false blocks it. Omission uses the server setting. | | `:auto-stop-ms` | Positive idle duration before stopping. Omission uses the server setting. | | `:ttl-ms` | Positive lifetime before deletion. Omission uses the server setting. | Durations round up to whole seconds on the wire. Additional options: | | | | --- | --- | | Option | Meaning | | `:branchable?` | Boolean, maps to `forkable`. Omission uses the service setting. | | `:disk-durability` | `:full` or `:deferred`. Omission uses the service setting. | | `:allowed-cidrs` | Vector of nonblank CIDR strings. Defaults to `[]`. | | `:allowed-hosts` | Vector of nonblank host strings. Defaults to `[]`. | | `:cache-disk` | Nonblank existing cache ID/name. Attaches its latest version at its configured mount path. Omission attaches no cache. | Nonempty allowlists select restricted networking and cannot be combined with `:network? false`. Empty lists use `:network?` or the service setting. The server validates CIDR/host syntax. Create does not start or wait. For a branchable source, separately invoke start with `{:branchable? true}`. Cache lifetime is independent of the machine. Deleting a machine does not request cache deletion. Cache version/slot overrides are not introduced. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L73-L113) --- ## start ```clojure (start id) (start id opts) ``` Describes starting machine `id` from its disks. Result: the server’s response. HTTP timeout defaults to 30 minutes. Adds an options arity. Existing one-argument behavior is unchanged. | | | | --- | --- | | Option | Meaning | | `:branchable?` | Boolean. When supplied, sends `forkable` in the start query. Omission preserves ordinary start. | This does not wait for readiness. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L115-L127) --- ## stop ```clojure (stop id) ``` Describes stopping machine `id` without deleting its storage. Result: the server’s response. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L129-L133) --- ## pause ```clojure (pause id) ``` Describes pausing machine `id` with its execution state preserved. Result: machine info. HTTP timeout defaults to 30 minutes. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L135-L139) --- ## resume ```clojure (resume id) ``` Describes resuming the preserved execution state of machine `id`. Result: machine info. HTTP timeout defaults to 30 minutes. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L141-L145) --- ## delete ```clojure (delete id) (delete id opts) ``` Describes deleting machine `id` and its storage. | | | | --- | --- | | Option | Meaning | | `:include-usage?` | Return the final usage report. Defaults to false. | Result: the usage report when requested, otherwise nil. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L147-L156) --- ## exec ```clojure (exec id command) (exec id command opts) ``` Describes running nonempty argv vector `command` on machine `id`. Result: `:exit-code`, `:stdout`, `:stderr`, `:stdout-bytes`, `:stderr-bytes`, `:stdout-truncated?`, and `:stderr-truncated?`. The result includes server metadata such as `:machine-id`, `:duration-ms`, and `:user`. Nonzero exit codes are results. Missing exit status is an invalid response. Cloud text can be truncated. Byte fields decode base64 when supplied, otherwise encode the returned text as UTF-8. Invalid base64 is an error. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Guest working directory. Defaults to the machine setting. | | `:stdin` | Complete UTF-8 input text. Defaults to empty input. | | `:timeout-ms` | Positive guest timeout, rounded up to whole seconds. Omission uses the server setting. | | `:user` | Guest user name or uid[:gid]. Verify support with [`user-check`](#user-check) before use. Defaults to the image account. | An explicit guest timeout sets the HTTP budget to that rounded timeout plus 30 seconds, with a minimum of 60 seconds. Additional option: | | | | --- | --- | | Option | Meaning | | `:output` | `:text` or `:b64`. Omission requests the server’s default, currently both. | Sent as the output query parameter. Older servers may ignore it. The existing result shape and strict exit/base64 checks are unchanged. Missing text fields become empty strings, even when byte fields contain output. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L158-L190) --- ## user-check ```clojure (user-check id user) ``` Describes a harmless command that checks support for guest `user` on machine `id`. Result: `user` when the server confirms the requested account. Otherwise it throws with code NOT_SUPPORTED or the command failure. Invoke this check before an [`exec`](#exec) that specifies `:user`. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L192-L199) --- ## file-read ```clojure (file-read id path) (file-read id path opts) ``` Describes reading absolute guest `path` on machine `id`. | | | | --- | --- | | Option | Meaning | | `:as` | `:bytes` (default) returns a byte array. `:stream` returns an InputStream for the caller to close. | Close returned streams after use. Stream mode reads the HTTP body incrementally. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L201-L210) --- ## file-write ```clojure (file-write id path data) ``` Describes writing `data` to absolute guest `path` on machine `id`. Accepts a byte array, UTF-8 string, `java.io.File`, or `java.nio.file.Path`. File and Path bodies are streamed by the HTTP client during invocation. Uploads are limited to 100 MiB by the service. Result: nil on success. Use [`exec`](#exec) with chmod to set permissions. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L212-L221) --- ## logs ```clojure (logs id) (logs id opts) ``` Describes fetching workload logs for machine `id`. Result: text. | | | | --- | --- | | Option | Meaning | | `:tail` | Positive number of trailing lines. Defaults to 100. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L223-L230) --- ## ports-set ```clojure (ports-set id ports) ``` Describes replacing machine `id’s published ports with `ports`. `ports` is a vector of guest TCP port numbers. An empty vector removes every published port. Result: machine info with allocated host ports. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L232-L238) --- ## endpoint ```clojure (endpoint client id port) (endpoint client id port path) ``` Returns authenticated endpoint data for guest TCP `port` on machine `id`. Returns `{:method :get :uri string :headers map :ws-uri string}`. Pass this map directly to `babashka.http-client/request`. Use `assoc` to set request options such as `:method`, `:body`, or `:as`. This pure helper uses `client` to construct the URLs and bearer header. The returned headers contain a credential. Optional `path` selects a path beneath the guest service. Defaults to root. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L240-L251) --- ## job-start ```clojure (job-start id command) (job-start id command opts) ``` Describes starting a background job with argv vector `command` on machine `id`. Result: job info with `:id`, `:machine-id`, `:command`, `:state`, and `:created-at`. Job states are running, exited, or lost. Without `:user`, the job runs as the image’s configured account. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Guest working directory. Defaults to the machine setting. | | `:timeout-ms` | Positive runtime limit, rounded up to whole seconds. Omission allows unlimited runtime. | Guest timeout sends SIGTERM, normally producing exit code 143. Additional option: | | | | --- | --- | | Option | Meaning | | `:user` | Nonblank guest account. Omission uses the image account. | Invoke user-check separately before use. Job replies do not prove user support. Existing timeout rounding and other options are unchanged. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L253-L277) --- ## job-list ```clojure (job-list id) ``` Describes listing jobs on machine `id`. Result: a vector of job-info maps. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L279-L282) --- ## job-info ```clojure (job-info id job-id) (job-info id job-id opts) ``` Describes fetching `job-id` on machine `id`. Result: job info, including `:exit-code` and `:finished-at` when available. `:stdout-bytes` and `:stderr-bytes` are byte counts. A lost job’s machine stopped or restarted while the job was running. | | | | --- | --- | | Option | Meaning | | `:wait-seconds` | Long-poll until the job ends, up to 30 seconds. Defaults to 0. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L284-L295) --- ## job-output ```clojure (job-output id job-id) (job-output id job-id opts) ``` Describes fetching a slice of `job-id` output on machine `id`. Result: `{:stream string :offset n :next-offset n :data bytes :eof boolean}`. Data decodes base64 when supplied, otherwise encodes returned text as UTF-8. EOF means the job has finished and this slice reaches the end of the stream. | | | | --- | --- | | Option | Meaning | | `:stream` | `:stdout` (default) or `:stderr`. | | `:offset` | Nonnegative byte offset. Defaults to 0. | | `:wait-seconds` | Long-poll for output or job completion, up to 30 seconds. Defaults to 0. | | `:limit` | Positive byte limit, at most 1048576. Defaults to the server setting, currently 262144. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L297-L311) --- ## job-cancel ```clojure (job-cancel id job-id) ``` Describes sending SIGTERM to `job-id` on machine `id`. Result: job info. A terminated job normally exits with code 143. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L313-L317) --- ## usage ```clojure (usage id) ``` Describes fetching the usage report for machine `id`. Result: `{:machine-id string :from string :to string :usage map :cost map}`. Usage includes `:total-uptime-seconds`, `:cpu-hours`, `:memory-gb-hours`, `:disk-gb-hours`, and `:egress-gb`. Costs are integer micro-dollars, including `:total-micros` and `:amount-due-micros`. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L319-L327) --- ## branch ```clojure (branch id name) (branch id name opts) ``` Describes creating child `name` from branchable source `id`. Result: child machine info. Name is nonblank. No readiness wait or legacy-route fallback is performed. The source can briefly pause during branching. | | | | --- | --- | | Option | Meaning | | `:ports` | TCP maps with required guest 1..65535 and host 0..65535. Defaults to `[]`. Host zero requests allocation. | | `:branchable?` | Boolean, defaults to false. Requests a branchable child. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L329-L339) --- ## branch-batch ```clojure (branch-batch id names) (branch-batch id names opts) ``` Describes one transactional branch batch from source `id`. Names are a nonempty vector of distinct nonblank strings. Result: a vector of child info maps in service order. No waits or legacy-route fallback occur. | | | | --- | --- | | Option | Meaning | | `:ports` | TCP mappings as in branch. Defaults to `[]`. Multiple children require host zero. | Branchable-child, count and name-prefix selectors are not provided. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L341-L352) --- ## checkpoint ```clojure (checkpoint id) ``` Describes capturing machine `id`. Result: durable checkpoint metadata. HTTP timeout defaults to 30 minutes. No download occurs. Download URLs are sensitive. Local destination/store options do not apply. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L354-L359) --- ## checkpoints ```clojure (checkpoints id) ``` Describes listing machine `id’s checkpoints. Result: a vector of metadata maps. Download URLs can be sensitive. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L361-L365) --- ## checkpoint-restore ```clojure (checkpoint-restore checkpoint-id name) (checkpoint-restore checkpoint-id name opts) ``` Describes restoring durable `checkpoint-id` to new machine `name`. Result: machine info. Both arguments are nonblank. No file upload, start or wait. | | | | --- | --- | | Option | Meaning | | `:egress-policy` | Map with required boolean network? and optional nonblank string vectors allowed-cidrs/allowed-hosts. Nonempty lists require true. Supplying either list with network true selects allowlist mode, even if empty. Omission of policy means blocked networking. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L367-L375) --- ## checkpoint-upload-create ```clojure (checkpoint-upload-create size-bytes) ``` Describes allocating an upload of positive `size-bytes`. Result: a map with checkpoint metadata, positive :part-size-bytes and a vector of :upload-urls. URLs authorize raw storage PUTs and must not be logged. This does not stat/read a file, transfer parts or complete the upload. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L377-L383) --- ## checkpoint-upload-part ```clojure (checkpoint-upload-part upload-url file opts) ``` Describes one raw PUT of a file section to presigned HTTPS `upload-url`. `file` is a nonblank path string, File or Path. Construction performs no file I/O. The URL cannot contain user info or a fragment. Result: nil on HTTP 2xx. No API authorization, cookies, redirects or retries. URL query bytes are kept. | | | | --- | --- | | Option | Meaning | | `:offset` | Required nonnegative byte offset. | | `:length` | Required positive byte count. Offset plus length must fit a long. | | `:timeout-ms` | Positive HTTP budget. Omission uses the client request timeout. | | `:progress` | Optional function called with bytes read for this attempt and section length after each positive read. | Invoke opens a fresh file section, checks that it fits a regular file, sends exactly its declared Content-Length and closes its stream on every outcome. Keep the file unchanged until completion. No caller-supplied InputStream is accepted. A new invocation opens a fresh section and starts its count at zero. Progress measures sender reads, not storage acknowledgements. Callbacks can reach length before the HTTP outcome is known. Calls are serialized within one PUT, not across different invocations. Callback failure fails the request with PROGRESS_FAILED. Source file errors use FILE_ERROR. No upload plan, part scheduling, retry/reset policy or aggregate progress is implemented here. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L385-L409) --- ## checkpoint-upload-complete ```clojure (checkpoint-upload-complete checkpoint-id) ``` Describes completing `checkpoint-id` after every part PUT succeeded. Result: checkpoint metadata. HTTP timeout defaults to 30 minutes. Sends no ETag list. Does not transfer parts, retry or delete a failed upload. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L411-L416) --- ## checkpoint-delete ```clojure (checkpoint-delete checkpoint-id) ``` Describes deleting durable `checkpoint-id` and its stored artifact. Result: nil. The service can return HTTP 409 while referenced. Incomplete-upload abort semantics are not established. This endpoint is a service-docs addition to Go. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L418-L423) --- ## share ```clojure (share id) ``` Describes creating/retrieving a sharing link for machine `id`. Result: share metadata. URL/token are credentials. Revoke with unshare. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L425-L429) --- ## unshare ```clojure (unshare id) ``` Describes revoking machine `id’s sharing link. Result: nil. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L431-L434) --- ## port-links ```clojure (port-links id) ``` Describes creating public port links for machine `id`. Result: integer guest-port to URL map. URLs are credentials. This changes access, unlike endpoint. Revoke all links with port-links-revoke. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L436-L441) --- ## port-links-revoke ```clojure (port-links-revoke id) ``` Describes revoking every public port link for machine `id`. Result: nil. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L443-L446) --- ## assign ```clojure (assign id opts) ``` Describes leasing a provisioned child from source machine `id`. Result: machine-id, secret owner-token and optional machine info. No client wait, heartbeat scheduling or cleanup. Repeated lease IDs use service idempotency, not a retry performed by invoke. | | | | --- | --- | | Option | Meaning | | `:lease-id` | Required nonblank idempotency key. | | `:task` | Optional JSON value staged at /run/smol-task.json. | | `:files` | Optional absolute guest path to byte-array/UTF-8-string map, sent as base64. | | `:secrets` | Optional string-to-string map. | | `:ports` | Optional TCP mappings as in branch. | | `:ttl-ms` | Positive lease duration rounded up to seconds. Omission uses service default. | | `:heartbeat-ms` | Positive heartbeat duration rounded up to seconds. Omission uses service default. | Task/result map keys keep their spelling. Do not log task/files/secrets/token. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L448-L466) --- ## lease-heartbeat ```clojure (lease-heartbeat lease-id owner-token) ``` Describes renewing `lease-id` with secret `owner-token`. Result: nil. Arguments are nonblank. No future heartbeat is scheduled. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L468-L472) --- ## lease-complete ```clojure (lease-complete lease-id owner-token opts) ``` Describes completing `lease-id` with secret `owner-token`. Result: nil. Completion can trigger child cleanup. Verify resource state separately. | | | | --- | --- | | Option | Meaning | | `:reason` | Required nonblank completion reason. | | `:score` | Optional finite number. | | `:result` | Optional JSON value, preserving payload map keys. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L474-L484) --- ## lease-info ```clojure (lease-info lease-id) ``` Describes fetching `lease-id`. Result: full lease status map. Payload fields can contain sensitive task data. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L486-L490) --- ## artifact-export ```clojure (artifact-export id) ``` Describes exporting stopped machine `id`. Result: service metadata map. HTTP timeout defaults to 30 minutes. This can create durable data with no established cleanup operation. Confirm retention/cleanup before invocation. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L492-L497) --- ## cache-disk-create ```clojure (cache-disk-create name) (cache-disk-create name opts) ``` Describes creating cache `name`. Result: cache metadata. Name uses lowercase ASCII letters, digits, dots, underscores or hyphens. Requires a supporting plan. Cache storage has a separate lifetime from machines. | | | | --- | --- | | Option | Meaning | | `:size-gb` | Integer 1..500. Omission uses service default 20. | | `:mount-path` | Absolute guest directory. Omission uses /cache. | This is a pinned Node/service-docs addition to Go. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L499-L511) --- ## cache-disk-info ```clojure (cache-disk-info id-or-name) ``` Describes fetching cache `id-or-name`. Result: cache metadata and versions. Useful for version inspection and NOT_FOUND verification after deletion. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L513-L517) --- ## cache-disk-publish ```clojure (cache-disk-publish id) ``` Describes publishing the configured cache of stopped machine `id`. Result: cache and newly published version metadata. Timeout is 30 minutes. Appends a version. Does not configure or create the cache resource. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L519-L524) --- ## cache-disk-delete ```clojure (cache-disk-delete id-or-name) ``` Describes deleting cache `id-or-name` and ALL versions. Result: nil. The service refuses deletion while used by a running machine. This does not target one version. Confirm no other workload needs this shared resource. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L526-L531) --- ## exec-background ```clojure (exec-background id command) (exec-background id command opts) ``` Describes launching detached argv `command` on machine `id`. Result includes positive pid parsed from stdout pid=<n>, stdout/stderr and byte fields plus service metadata. Missing PID is INVALID_REPLY. No exit status is synthesized. A launcher status is not remote child completion. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string overrides, default {}. | | `:workdir` | Nonblank guest directory, default machine setting. | | `:user` | Nonblank guest account. Invoke user-check first. Reply echo is checked. | Uses client HTTP timeout. Supplied stdin, guest timeout, background, stream or output flags fail NOT_SUPPORTED. This is not a cancellable job. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L533-L548) --- ## exec-stream ```clojure (exec-stream id command) (exec-stream id command opts) ``` Describes SSE execution of argv `command` on machine `id`. Result through invoke: a Closeable event reader. Close it after use. Uses pinned /exec/stream, no route fallback, and requires text/event-stream. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string overrides, default {}. | | `:workdir` | Nonblank guest directory, default machine setting. | | `:stdin` | Complete UTF-8 input text, default empty. | | `:timeout-ms` | Positive guest timeout rounded up to seconds, default service setting. | | `:user` | Nonblank account. Invoke user-check first. SSE cannot confirm account. | Header timeout follows exec’s budget, not a timeout on later body reads. Background/output flags are unsupported. No reconnect, replay or event queue. Closing the reader does not promise remote command termination. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L550-L567) --- ## stream-read ```clojure (stream-read stream) ``` Consumes one event from an already-open cloud exec `stream`, blocking as needed. Does not submit another HTTP request. Returns stdout/stderr events with :kind and UTF-8 :data bytes, exit with integer :exit-code, or error with :message. Multiple SSE data lines join with newline. This is text, not binary-exact output. Exit/error is terminal and closes the body before return. Later reads or reads after explicit close return nil. CRLF, fragmented UTF-8 and an unterminated final event are supported. Comments and unrecognized events are ignored. Invalid UTF-8/exit JSON or EOF without a terminal event throws INVALID_REPLY. A frame above 1 MiB raw wire bytes, including ignored lines/line endings, throws LIMIT_EXCEEDED. Enforce bounds while accumulating each line. Read/parse errors close the body. Transport errors are sanitized. Service error-event text can contain sensitive data and is never logged by the SDK. Use one reader thread. Close from another thread releases a blocked read, returning nil if closure caused the failure. Interruption alone is not a guaranteed way to stop body reads. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm/cloud.clj#L569-L587) ## ol.smolvm # ol.smolvm Local microVMs, native execution and Java stream conveniences. Call [`initialize`](#initialize) before native operations. Operations block until the native call returns. Options and nested configuration maps ignore unrecognized keys. Supplied recognized values must have the documented type. Invalid configuration throws ExceptionInfo with code CONFIG_ERROR. Native failures throw ExceptionInfo with the native code. Host paths are nonblank strings without NUL characters and are resolved to absolute paths. Guest paths are absolute strings. Machine names and image references are nonblank strings. Resource sizes are positive integers. CPU counts cannot exceed 255, MiB values cannot exceed 4294967295, and GiB values cannot exceed 9223372036854775807. The engine can impose lower platform limits. A handle returned by create, branch, branch-batch or checkpoint-restore deletes that machine on close. A connect handle does not delete on close. Close children before closing their source. The engine refuses deletion of a source with dependent children. Closing a child never closes its source handle. Explicit delete and repeated close are safe after successful deletion. ## initialize ```clojure (initialize) (initialize opts) ``` Loads the local runtime and returns its version, platform and ABI version. Call before other operations. Repeated calls are safe but cannot select a different runtime. | | | | --- | --- | | Option | Meaning | | `:runtime-dir` | Unpacked runtime directory. Defaults to the platform companion JAR. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L18-L27) --- ## check-host ```clojure (check-host) ``` Returns nil when the host supports local machines, otherwise `{:code code :reason reason}`. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L29-L32) --- ## create ```clojure (create config) ``` Creates and starts a machine and returns a Closeable handle. Closing deletes the machine and its storage, even with `:persistent?` or `:detached?`. Use `with-open` for scoped cleanup. Repeated close, including after [`delete`](#delete), is harmless. A failed start deletes the newly created machine. A name conflict does not delete the existing machine. | | | | --- | --- | | Configuration | Meaning | | `:name` | Required machine name. | | `:image` | OCI image reference. Omit for a bare guest. | | `:cpus` | Positive CPU count. Defaults to the engine setting. | | `:memory-mib` | Positive memory limit in MiB. Defaults to the engine setting. | | `:storage-gib` | Positive storage size in GiB. Defaults to the engine setting. | | `:overlay-gib` | Positive overlay size in GiB. Defaults to the engine setting. | | `:network?` | Unrestricted outbound networking. Defaults to false. | | `:allowed-cidrs` | Vector of CIDR strings for scoped outbound access. Defaults to `[]`. | | `:allowed-hosts` | Vector of hostname rules for scoped outbound access. Defaults to `[]`. The engine validates rules and enforces them through DNS filtering. | | `:network-backend` | `:tsi` or `:virtio-net`. Omit for engine selection. Saved-memory restores cannot switch backends. | | `:disk-durability` | `:full` (default) or `:deferred`. Deferred host writes can be lost if the host crashes. | | `:gpu?` | Enable GPU acceleration. Defaults to false. Requires host support. | | `:gpu-vram-mib` | Positive GPU shared-memory size in MiB. Defaults to the engine setting. | | `:cuda?` | Forward guest CUDA calls to a host NVIDIA GPU. Defaults to false. Requires host support. | | `:env` | String-to-string environment map. Defaults to `{}`. | | `:workdir` | Absolute guest working directory. Defaults to the engine setting. | | `:user` | Nonblank image workload user name or numeric uid[:gid]. Omit for the image default. | | `:mounts` | Vector of mount maps described below. Defaults to `[]`. | | `:ports` | Vector of TCP mappings `{:host port :guest port}`. Both values are integers from 0 through 65535. Host 0 selects a free host port. Defaults to `[]`. | | `:labels` | String-to-string label map. Defaults to `{}`. | | `:persistent?` | Keep the native machine record after the creating process exits. Defaults to true. Does not disable close deletion. | | `:detached?` | Let the native VM outlive its launching process. Defaults to false. Implies native persistence. Does not disable close deletion. | | `:branchable?` | Start as a live-memory branch source. Defaults to false. Cannot be combined with an interceptor. | | `:interceptor-address` | Loopback socket address of an existing egress interceptor. Requires `:interceptor-token`. Omit both for no new binding. | | `:interceptor-token` | Interceptor credential of 64 hexadecimal digits. Requires `:interceptor-address`. Keep it out of logs. | | | | | --- | --- | | Mount key | Meaning | | `:source` | Required host directory string or `s3://` source. The engine checks the source. | | `:target` | Required absolute guest directory. | | `:read-only?` | Boolean, defaults to false. | | `:staged?` | Boolean, defaults to false. Use a guest-local working copy and copy changes back on [`sync`](#sync), graceful stop or delete. Cannot be read-only or use an `s3://` source. | CIDR or hostname allow lists enable scoped networking without `:network? true`. The engine validates combinations and platform support. An interceptor is a separately supplied service, not started by this SDK. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L34-L76) --- ## connect ```clojure (connect name) (connect name opts) ``` Connects to an existing machine, starting it if needed. Returns a borrowed Closeable handle. Closing it does not stop or delete the machine. A frozen branch source can be connected without making it command-ready. | | | | --- | --- | | Option | Meaning | | `:interceptor-address` | Loopback socket address of an existing interceptor. Supply with `:interceptor-token`. | | `:interceptor-token` | Credential of 64 hexadecimal digits. Supply with `:interceptor-address`. Keep it out of logs. | Omit both interceptor keys to use the engine’s existing binding. The engine validates whether the machine can use the supplied interceptor. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L78-L90) --- ## list ```clojure (list) ``` Returns local machine summaries ordered by name. Each summary includes `:name`, `:state`, `:image` and `:labels`. Includes machines created by other SDKs and the CLI. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L92-L97) --- ## state ```clojure (state machine) ``` Returns the machine’s current state as a keyword. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L99-L102) --- ## start ```clojure (start machine) (start machine opts) ``` Starts the machine and waits for guest command execution to become available. Returns `machine`. This does not check application listeners or published ports. | | | | --- | --- | | Option | Meaning | | `:interceptor-address` | Loopback socket address of an existing interceptor. Supply with `:interceptor-token`. | | `:interceptor-token` | Credential of 64 hexadecimal digits. Supply with `:interceptor-address`. Keep it out of logs. | Omit both interceptor keys to use the engine’s existing binding. The engine validates whether the machine can use the supplied interceptor. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L104-L116) --- ## stop ```clojure (stop machine) ``` Stops the machine without deleting its storage. Returns `machine`. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L118-L121) --- ## delete ```clojure (delete machine) ``` Deletes the machine and its storage, stopping it if needed. Returns nil. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L123-L126) --- ## exec ```clojure (exec machine command) (exec machine command opts) ``` Runs a nonempty vector of command strings without inserting a shell. Returns `{:exit-code n :stdout text :stderr text :stdout-bytes bytes :stderr-bytes bytes}`. Text uses UTF-8 replacement decoding. Nonzero exit codes do not throw. Buffers all output in memory. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Guest working directory. Defaults to the machine setting. | | `:user` | Guest user. Requires an image-backed machine. Defaults to the engine setting. | | `:timeout-ms` | Positive command timeout. Omit for no caller-specified timeout. Timeout exits with status 124. | | `:stdin` | Complete UTF-8 text input. Defaults to no input. Not supported with `:cancel` on bare guests. | | `:cancel` | Token from [`cancel-new`](#cancel-new). Defaults to no external cancellation. Cancellation throws with code CANCELLED. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L128-L142) --- ## file-read ```clojure (file-read machine path) ``` Reads an absolute guest `path` into a byte array. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L144-L147) --- ## file-write ```clojure (file-write machine path data) (file-write machine path data opts) ``` Writes a byte array or UTF-8 string to an absolute guest `path`. Returns nil. Creates parent directories and replaces existing content. | | | | --- | --- | | Option | Meaning | | `:mode` | Unix permission bits, such as `0644`. Defaults to the engine setting. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L149-L158) --- ## stream-open ```clojure (stream-open machine command) (stream-open machine command opts) ``` Starts `command` and returns a Closeable native stream handle. Close it even after completion or [`stream-kill`](#stream-kill). Unread output accumulates in an unbounded native queue. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Guest working directory. Defaults to the machine setting. | | `:user` | Guest user. Requires an image-backed machine. Defaults to the engine setting. | | `:timeout-ms` | Positive command timeout. Omit for no caller-specified timeout. | | `:stdin` | Complete UTF-8 text input. Defaults to no input. Not supported on bare guests. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L160-L173) --- ## stream-next ```clojure (stream-next stream) ``` Blocks for one native stream result with no per-read timeout. Returns `{:kind :stdout :data bytes}`, `{:kind :stderr :data bytes}`, `{:kind :exit :exit-code n}`, or `{:kind :error :message text}`. Returns nil after completion, cancellation or close. Use one reader. Another thread may call [`stream-kill`](#stream-kill) or [`stream-close`](#stream-close). Does not accept a [`process`](#process) handle. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L175-L182) --- ## stream-kill ```clojure (stream-kill stream) ``` Requests cancellation of a native stream’s command. Returns nil. Does not release the handle or wait for the command to exit. Repeated calls are safe. Does not accept a [`process`](#process) handle. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L184-L189) --- ## stream-close ```clojure (stream-close stream) ``` Releases a native stream handle and requests cancellation of unfinished execution. Returns nil. Equivalent to `.close` or leaving `with-open`. Repeated calls are safe. Does not accept a [`process`](#process) handle. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L191-L196) --- ## cancel-new ```clojure (cancel-new) ``` Returns a Closeable cancellation token for [`exec`](#exec). [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L198-L201) --- ## cancel ```clojure (cancel token) ``` Signals an [`exec`](#exec) cancellation token. Returns nil. Repeated calls are safe. Does not release the token. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L203-L208) --- ## cancel-close ```clojure (cancel-close token) ``` Releases a cancellation token without cancelling a running command. Returns nil. Equivalent to closing the token. Repeated calls are safe. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L210-L215) --- ## process ```clojure (process machine command) (process machine command opts) ``` Starts `command` and returns a derefable process record. Dereference to wait for `{:exit n :out value :err value}`. Nonzero exits are results. Execution, destination-write and cancellation failures throw when dereferenced. Launch errors throw from this call. `:result` is a `java.util.concurrent.CompletableFuture` of the same result map. Use `(deref (:result p) timeout-ms timeout-value)` for a timed wait. A wait timeout does not cancel the command. Future failures throw `ExecutionException`. Use `ex-cause` for the original failure. Ordinary `@p` throws the original failure directly. Use [`destroy`](#destroy), not `future-cancel`, to cancel the guest command. Consume live output streams while the command runs, or select capture or forwarding. Supplied OutputStreams stay open and unflushed. Native buffering is unbounded. Input streaming is not supported. | | | | --- | --- | | Option | Meaning | | `:out` | `:stream` (default), `:string`, `:bytes`, or an `java.io.OutputStream`. | | `:err` | Same choices as `:out`. Defaults to `:stream`. | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Guest working directory. Defaults to the machine setting. | | `:user` | Guest user. Requires an image-backed machine. Defaults to the engine setting. | | `:timeout-ms` | Positive command timeout. Omit for no caller-specified timeout. | With `:stream`, `(:out p)` and `(:err p)` expose live `java.io.InputStream` objects. Close these streams after use. Use [`destroy`](#destroy) for early cancellation. The process record itself is not Closeable. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L217-L237) --- ## destroy ```clojure (destroy process) ``` Requests cancellation of a high-level process and returns that handle. Safe to call from another thread or repeatedly. Does not wait for the guest command to exit. A cancelled process throws with code CANCELLED when dereferenced. Does not accept a native stream handle. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L239-L244) --- ## start-branchable ```clojure (start-branchable machine) ``` Starts a created or stopped machine as a live-memory branch source. Returns `machine`. Requires state `:created` or `:stopped`, otherwise throws INVALID_STATE. Starting an already running machine does not convert it into a branch source. Use [`branch`](#branch) or [`checkpoint`](#checkpoint) after starting it. Requires native branch support. Does not accept interceptor options or check application listeners. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L246-L253) --- ## pid ```clojure (pid machine) ``` Returns the native VM process ID, or nil when no process ID is available. This is a host PID, not the guest PID returned by [`exec-background`](#exec-background). [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L255-L260) --- ## pause ```clojure (pause machine) ``` Saves machine memory and disk durably, then stops execution. Returns `machine`. Use [`resume`](#resume) to continue saved execution. Ordinary host mounts are allowed, but staged mounts, remote volumes, GPU and CUDA state are not. Other native checkpoint-profile restrictions apply. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L262-L267) --- ## resume ```clojure (resume machine) ``` Resumes saved machine execution. Returns `machine`. Requires resumable saved state. Does not check application listeners. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L269-L274) --- ## resize ```clojure (resize machine opts) ``` Resizes a running machine without rebooting and returns its resource totals. Returns `{:cpus n :memory-mib n :storage-gib n-or-nil :overlay-gib n-or-nil}`. Sizes are totals, not increments. Supply at least one resource key. The engine checks all targets before applying memory, CPUs and disks in that order. A later failure can leave earlier changes applied. | | | | --- | --- | | Option | Meaning | | `:cpus` | Positive target CPU count, at most 255. | | `:memory-mib` | Positive target memory size, at most 4294967295 MiB. | | `:storage-gib` | Positive target storage size, at most 9223372036854775807 GiB. | | `:overlay-gib` | Positive target overlay size, at most 9223372036854775807 GiB. | Omitted resources are unchanged. RAM and disks cannot shrink. CPU changes require Linux x86-64 or macOS ARM64. CPU decreases require Linux x86-64 with native offline-CPU support. RAM growth supports Linux x86-64, Linux ARM64 and macOS ARM64, in 128 MiB increments from boot RAM and within runtime capacity. Disk changes fail when the machine has dependent branches. Host headroom and guest capabilities can impose further limits. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L276-L290) --- ## branch ```clojure (branch machine name) (branch machine name opts) ``` Creates a child from a live-memory branch source and returns a Closeable child handle. `name` is the new child’s name. The child starts from source memory and disk state. A running source resumes on supported Linux and macOS hosts and stays available for more branches. Machines requiring intercepted egress cannot branch. Native machine and platform restrictions apply. Closing the child deletes its machine and storage, not the source. Keep the source handle open until all children are closed. A name conflict must not delete an existing machine. No application-port readiness probe runs. | | | | --- | --- | | Option | Meaning | | `:ports` | Vector of TCP mappings `{:host port :guest port}` with integer ports from 0 through 65535. Host 0 selects a free port. Defaults to `[]`, which remaps source forwards to fresh host ports. | | `:branchable?` | Boolean, defaults to false. Make the child suitable for later branches and checkpoints. This requires an eager memory copy instead of the ordinary copy-on-write memory path. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L292-L304) --- ## branch-batch ```clojure (branch-batch machine names) (branch-batch machine names opts) ``` Creates children from one branch source and returns a vector of Closeable handles in request order. `names` is a nonempty vector of distinct machine names. Each child deletes its machine and storage on close. The vector is not Closeable. Close each child before closing the source. Closing a child does not close the source. The native batch removes children it created if the batch fails. Existing machines must not be deleted on a name conflict. | | | | --- | --- | | Option | Meaning | | `:ports` | TCP mappings applied to every child, with the same shape as [`branch`](#branch). Defaults to `[]`, which remaps source forwards to fresh host ports. A batch with two or more children must omit ports or use `[]`. The pinned engine rejects shared explicit ports, including host 0. | | `:parallel` | Positive integer boot width, at most 2147483647. Defaults to 8. | This operation cannot request branchable children. Use [`branch`](#branch) with `:branchable? true` for that case. No application-port readiness probe runs. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L306-L318) --- ## checkpoint ```clojure (checkpoint machine output) (checkpoint machine output opts) ``` Captures a running branchable machine into a host checkpoint artifact. Returns `{:size-bytes n :reused-bytes n :source-pause-ms n :elapsed-ms n}`. Byte counts are integers. Durations are milliseconds and can be fractional. `output` is a host path that must not exist and must end in .checkpoint or .smolcheckpoint (case-insensitive), including when writing a stored checkpoint directory. Capture temporarily pauses the source. The engine checks platform and artifact constraints. Host mounts, staged mounts, remote volumes, GPU and CUDA state are not supported by this capture API. Other host-bound features can also prevent capture. | | | | --- | --- | | Option | Meaning | | `:store` | Host checkpoint-store directory. Omit to write one portable file. With a store, `output` is a checkpoint directory that shares stored objects. | Checkpoint files and stores are not deleted when the machine handle closes. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L320-L331) --- ## checkpoint-restore ```clojure (checkpoint-restore artifact name) (checkpoint-restore artifact name opts) ``` Restores a host checkpoint artifact into a new machine and starts it. `artifact` is a portable checkpoint file or stored checkpoint directory. `name` is the new machine name. Returns a Closeable handle. Closing deletes the restored machine and its storage, not the artifact or source machine. A failed start or policy update deletes the newly restored machine. A name conflict must not delete the existing machine. | | | | --- | --- | | Option | Meaning | | `:keep-identity?` | Boolean, defaults to false. Reuse checkpoint hostname and machine identity. Do not run multiple copies with the same identity. | | `:egress-policy` | Policy map accepted by [`egress-policy-set`](#egress-policy-set). Applied after installation and before starting. Omit to use checkpoint policy. | | `:interceptor-address` | Loopback address of an existing interceptor for the first start. Requires `:interceptor-token`. | | `:interceptor-token` | Credential of 64 hexadecimal digits. Requires `:interceptor-address`. Keep it out of logs. | Saved-memory restores cannot change network backend. The engine rejects policies incompatible with the saved backend. Restore can fetch referenced images on the host, even with guest networking disabled. Does not check application listeners. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L333-L347) --- ## checkpoint-export ```clojure (checkpoint-export source output) ``` Exports a stored checkpoint directory as one portable file and returns its size in bytes. `source` and `output` are host paths. The output must not exist. No filename suffix is required. Does not delete the source checkpoint or its store. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L349-L354) --- ## checkpoint-prune ```clojure (checkpoint-prune store) ``` Removes unreferenced objects from a host checkpoint store and returns the bytes freed. `store` is a host directory. This deletes stored objects that no checkpoint in the store references. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L356-L361) --- ## egress-policy-set ```clojure (egress-policy-set machine policy) ``` Replaces a created or stopped machine’s outbound network policy. Returns `machine`. Saved-memory machines keep their network backend. A policy requiring a different backend fails. | | | | --- | --- | | Policy key | Meaning | | `:network?` | Required boolean. True permits unrestricted outbound access. False blocks it except for supplied allow lists. | | `:allowed-cidrs` | Vector of CIDR strings. Defaults to `[]`. | | `:allowed-hosts` | Vector of hostname rules. Defaults to `[]`. The engine validates rules and enforces them through DNS filtering. | `{:network? false}` with empty allow lists denies outbound access. No interceptor service is created. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L363-L376) --- ## host-port ```clojure (host-port machine guest-port) ``` Returns the host port forwarding to `guest-port`, or nil if no mapping is available. `guest-port` is an integer from 0 through 65535. This lookup does not check whether an application is accepting connections. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L378-L383) --- ## guest-ports ```clojure (guest-ports machine) ``` Returns a vector of guest ports reported by the native machine. This lists configured forwarding targets, not listening guest sockets. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L385-L390) --- ## published-ports ```clojure (published-ports machine) ``` Returns a vector of published mappings `{:host n :guest n :protocol protocol}`. Protocol is `:tcp` or `:udp`. Reads the persisted machine record even when stopped. Does not probe listeners. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L392-L399) --- ## ports-set ```clojure (ports-set machine ports) ``` Replaces all published port mappings. Returns `machine`. `ports` is a vector of at most 64 maps. An empty vector removes all mappings. Running machines can update TCP mappings through the native ports control socket when available. UDP changes require a stopped machine. A missing or incompatible control socket causes a native failure. | | | | --- | --- | | Mapping key | Meaning | | `:host` | Required integer from 0 through 65535. Zero selects a free host port. | | `:guest` | Required integer from 0 through 65535. The engine validates usable ports. | | `:protocol` | `:tcp` (default) or `:udp`. | Read [`published-ports`](#published-ports) for the effective mappings. No listener readiness probe runs. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L401-L414) --- ## exec-background ```clojure (exec-background machine command) (exec-background machine command opts) ``` Starts a nonempty command vector without a shell and returns its guest PID. Does not wait for exit or collect output. This PID is not a Closeable process handle. The SDK provides no background wait or cancellation handle. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Absolute guest working directory. Defaults to the machine setting. | | `:user` | Nonblank guest user. Requires an image-backed machine. Defaults to the engine setting. | | `:timeout-ms` | Unsupported. Supplying this key throws CONFIG_ERROR. | | `:stdin` | Unsupported. Supplying this key throws CONFIG_ERROR. | | `:cancel` | Unsupported. Supplying this key throws CONFIG_ERROR. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L416-L430) --- ## run ```clojure (run machine image command) (run machine image command opts) ``` Pulls an OCI image if needed and runs a command in a container of that image. `command` is a nonempty vector of strings. Returns the same result map as [`exec`](#exec), with UTF-8 text and byte-array output. Buffers all output. Nonzero exits do not throw. Pulling an uncached image uses the network. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Absolute container working directory. Defaults to the engine setting. | | `:timeout-ms` | Positive timeout, at most 9223372036854775807 milliseconds. Omit for no caller-specified timeout. Timeout exits with status 124. | | `:user` | Unsupported. Supplying this key throws CONFIG_ERROR. Use an image-backed machine and [`exec`](#exec). | | `:stdin` | Unsupported. Supplying this key throws CONFIG_ERROR. | | `:cancel` | Unsupported. Supplying this key throws CONFIG_ERROR. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L432-L446) --- ## image-pull ```clojure (image-pull machine image) ``` Pulls an OCI image into machine storage and returns its metadata. Returns `{:reference text :digest text :size n :architecture text :os text}`. Size is bytes. Pulling can access the network. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L448-L453) --- ## image-list ```clojure (image-list machine) ``` Returns a vector of cached OCI image metadata maps. Each map has the same fields as [`image-pull`](#image-pull). [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L455-L460) --- ## pack ```clojure (pack machine output) ``` Packs a stopped machine into a host artifact and returns the resulting .smolmachine path string. `output` is the destination host path. The engine adds the .smolmachine suffix if needed and writes a runnable file beside the artifact. It uses the CLI bundled with the native runtime. The artifact is not deleted when the handle closes. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L462-L467) --- ## sync ```clojure (sync machine) ``` Copies staged mount changes back to their host directories without stopping the machine. Returns `machine`. This can change host files. It is not a general disk flush operation. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L469-L474) --- ## machine-name ```clojure (machine-name machine) ``` Returns the machine handle’s name string without a native call. Local machine IDs are the same as names. This does not check whether the machine still exists. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L476-L481) --- ## ready? ```clojure (ready? machine) ``` Returns whether the machine is running and its published guest ports pass a TCP readiness probe. Returns false for a non-running machine, a missing host mapping or a failed probe. A running machine with no guest ports returns true. Native state and mapping errors propagate. Each probe connects to 127.0.0.1 with a 250 millisecond connect timeout, then reads at most one byte with a 100 millisecond read timeout. A byte received or a read timeout counts as ready. EOF, reset or connection failure counts as not ready. Each probe socket is closed. This matches the Go local heuristic, not application health. It can consume a greeting byte and create service logs. It cannot establish UDP readiness. No application request is sent. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L483-L492) --- ## wait-until-ready ```clojure (wait-until-ready machine) (wait-until-ready machine opts) ``` Polls [`ready?`](#ready?) and returns `machine` when the probe succeeds. Probes immediately, then sleeps between attempts. Native errors propagate. Non-running states keep polling rather than failing immediately. If another full interval would exceed the deadline, throws TIMEOUT. The deadline does not interrupt an active native call or probe. A successful active probe can finish after the deadline. Thread interruption stops the wait without stopping or deleting the machine. | | | | --- | --- | | Option | Meaning | | `:timeout-ms` | Positive integer milliseconds, at most 9223372036854775807. Defaults to 120000. | | `:interval-ms` | Positive integer milliseconds, at most 9223372036854775807. Defaults to 1000. | This is a machine readiness helper, not a process result waiter. Existing create, connect, start, resume and branch functions do not call it automatically. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L494-L506) --- ## endpoint ```clojure (endpoint machine guest-port) (endpoint machine guest-port path) ``` Returns local HTTP and WebSocket URLs for a published guest port. Returns `{:http-url text :ws-url text :headers {}}` using 127.0.0.1 and the current host mapping. `guest-port` is an integer from 1 through 65535. An absent mapping throws CONFIG_ERROR. This reads the mapping but does not probe or publish a port. `path` is a string, defaults to empty, and must contain URI-valid text. An empty path adds no suffix. Otherwise all leading slashes are replaced by one slash. Queries and fragments are preserved. Supply percent escapes where needed. The host and scheme cannot be replaced by the path. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L508-L515) --- ## http-request ```clojure (http-request machine guest-port path) (http-request machine guest-port path opts) ``` Builds a babashka.http-client request map for a local machine’s guest port without sending it. `guest-port` and `path` follow [`endpoint`](#endpoint). Reads the current native mapping, so construction is not a pure function. The returned map is request data only. Returns `{:method method :uri text :headers headers :body body}`. Send it with `babashka.http-client/request`, adding that client’s response and transport options as needed. Building the map neither reads nor closes the body. | | | | --- | --- | | Option | Meaning | | `:method` | `:get` (default), `:head`, `:post`, `:put`, `:patch`, `:delete` or `:options`. | | `:headers` | String-to-string map. Defaults to `{}`. Names must be HTTP tokens and values must not contain CR or LF. | | `:body` | String, byte array, `java.io.InputStream` or nil. Defaults to nil. Manage supplied stream lifetime through request completion. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L517-L530) --- ## fetch ```clojure (fetch machine guest-port path) (fetch machine guest-port path opts) ``` Sends an HTTP GET to a published local guest port and returns a response map. `guest-port` and `path` follow [`endpoint`](#endpoint). Blocks until response headers are available. Returns at least `{:status integer :headers map :body input-stream}`. Close the response body, including for non-success status codes. HTTP error statuses do not throw. Transport failures and native mapping errors propagate. Follows HTTP and HTTPS redirects, including HTTPS to HTTP, through the HTTP client’s built-in redirect support. Redirects can leave the local machine. Redirect limits and header forwarding follow the Java HTTP client, not Go’s exact redirect policy. No custom redirect loop or application-level retry runs. No readiness wait runs. The default has no request timeout. A timeout limits receipt of response headers, not later reads from the body stream. | | | | --- | --- | | Option | Meaning | | `:headers` | String-to-string request headers, as in [`http-request`](#http-request). Defaults to `{}`. | | `:timeout-ms` | Positive integer milliseconds, at most 9223372036854775807. Omit for no request timeout. | Uses the existing babashka.http-client dependency. Use [`http-request`](#http-request) and that client directly for other methods or response formats. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L532-L546) --- ## publish ```clojure (publish machine opts) ``` Adds or replaces one published mapping and returns its effective map. Returns `{:host integer :guest integer :protocol keyword}`. Replaces mappings with the same host port and protocol, preserving other mappings. If host is zero, briefly binds a loopback socket of the selected protocol to choose a free port, then closes it before publishing. Another process can take that port before publication. Native failures propagate without retry. Reads mappings, applies [`ports-set`](#ports-set), then reads them again. This is not an atomic update. Serialize port changes for the same machine, including changes from other handles or processes. A readback failure can occur after the update took effect. Returns the exact new mapping if present, otherwise another mapping for the same guest and protocol. If neither appears, throws INVALID_STATE rather than reporting unverified success. | | | | --- | --- | | Option | Meaning | | `:guest` | Required guest port integer from 1 through 65535. | | `:host` | Host port integer from 0 through 65535. Defaults to 0, selecting a free port. | | `:protocol` | `:tcp` (default) or `:udp`. | Running TCP changes require the native ports control socket. UDP changes require a stopped machine. No readiness probe runs. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L548-L563) --- ## unpublish ```clojure (unpublish machine opts) ``` Removes matching published mappings and returns `machine`. Matches all supplied selectors and the protocol. Omitting a selector matches every value for it. At least one selector is required. A match-free call makes no update. This does not explicitly close established connections. | | | | --- | --- | | Option | Meaning | | `:guest` | Guest port integer from 1 through 65535. Omit to match every guest port. | | `:host` | Host port integer from 1 through 65535. Omit to match every host port. | | `:protocol` | `:tcp` (default) or `:udp`. | Reads current mappings and replaces the filtered list through [`ports-set`](#ports-set). This is not atomic. Serialize concurrent port changes. Running TCP changes require a compatible control socket. UDP changes require a stopped machine. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L565-L578) --- ## file-read-stream ```clojure (file-read-stream machine path) ``` Reads an absolute guest file path and returns a `java.io.InputStream` over its bytes. Reads and buffers the entire file before returning. This is not a bounded-memory or incremental native read. An empty file returns a stream at EOF. Native read errors occur during this call, not on later stream reads. Close the returned stream after use. [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L580-L585) --- ## branch-exec ```clojure (branch-exec machine name command) (branch-exec machine name command opts) ``` Branches a temporary child, runs one command in it, then deletes the child. `name` is a nonblank unused child name. `command` and `opts` have the same contract and option table as [`exec`](#exec). Returns its result map, including nonzero exit codes without throwing. No additional branch options are accepted. The source must support [`branch`](#branch). Deletes only a child successfully created by this call, on success or failure. A name conflict must not delete an existing machine. Cleanup failure after successful execution throws. If execution and cleanup both fail, preserves the execution failure and attaches the cleanup failure as a suppressed exception. Does not stop or delete the source. A native branch can briefly pause it. No application readiness wait runs. | | | | --- | --- | | Option | Meaning | | `:env` | String-to-string environment overrides. Defaults to `{}`. | | `:workdir` | Guest working directory. Defaults to the machine setting. | | `:user` | Guest user. Requires an image-backed machine. Defaults to the engine setting. | | `:timeout-ms` | Positive command timeout. Omit for no caller-specified timeout. Timeout exits with status 124. | | `:stdin` | Complete UTF-8 text input. Defaults to no input. Not supported with `:cancel` on bare guests. | | `:cancel` | Token from [`cancel-new`](#cancel-new). Defaults to no external cancellation. Cancellation throws CANCELLED. | [source,window=_blank](https://github.com/outskirtslabs/smolvm-clj/blob/main/src/ol/smolvm.clj#L587-L603) ## How to run smol cloud commands # How to run smol cloud commands Complete [installation](installation.adoc). Set `SMOL_CLOUD_TOKEN` to your [smol cloud](https://smolmachines.com/) API token before starting the REPL. Keep the token out of source files and logs. ## Connect and select a machine Create client configuration and request the machine list: ```clojure (require '[ol.smolvm.cloud :as cloud]) (def client (cloud/client {:api-key (System/getenv "SMOL_CLOUD_TOKEN")})) @(cloud/invoke client (cloud/list)) ``` Operation builders return request maps without sending requests. `invoke` sends exactly one asynchronous request and returns a `CompletableFuture`. It does not poll, retry or run cleanup. Use `@` to wait for the decoded result. The SDK does not discover credentials. This example reads the environment variable explicitly and supplies `:api-key`. For multi-request tasks, use the separate [cloud workflow helpers](cloud-workflows.adoc). These also return futures. Replace example IDs and paths before use. Examples can create billable resources. Choose the exact `:id` of a ready machine from the list: ```clojure (def machine-id "replace-with-machine-id") @(cloud/invoke client (cloud/exec machine-id ["echo" "hello"])) ``` Check `:exit-code` in the result. Nonzero command exits are results, not exceptions. Output from smol cloud can be truncated. Check `:stdout-truncated?` and `:stderr-truncated?` before using it as complete output. ## Create a temporary machine If you need a new machine, create it from an OCI image. Send the command with `exec`. The smol cloud service starts a stopped machine automatically for execution: ```clojure (let [machine @(cloud/invoke client (cloud/create {:image "alpine:3.21" :name "command-example"})) id (:id machine)] (try @(cloud/invoke client (cloud/exec id ["echo" "hello"])) (finally @(cloud/invoke client (cloud/delete id))))) ``` **⚠️ WARNING**\ `delete` deletes the smol cloud machine and its storage. The example requests deletion even when execution fails. Requests to smol cloud can fail, including cleanup requests. If cleanup fails, list machines and retry deletion with the exact machine ID. To stop a machine without deleting its disk, use `stop` instead of `delete`. ## Handle request failures Dereferencing a failed request throws `ExecutionException`. Read error data from its cause: ```clojure (try @(cloud/invoke client (cloud/info machine-id)) (catch java.util.concurrent.ExecutionException e (let [cause (.getCause e)] {:message (.getMessage cause) :data (ex-data cause)}))) ``` Error data includes `:code` and, when available, `:status` and `:request-id`. For a limited wait, use timed dereference: ```clojure (def result (cloud/invoke client (cloud/exec machine-id ["echo" "hello"]))) (deref result 1000 ::waiting) ``` A wait timeout does not cancel the request or the guest command. Dereference the same future again to get its result. For remote work that needs explicit cancellation, use `job-start` and `job-cancel`. ## Download a file without buffering the full response Request stream mode and close the returned stream after copying it: ```clojure (require '[clojure.java.io :as io]) (let [operation (cloud/file-read machine-id "/tmp/result" {:as :stream})] (with-open [^java.io.InputStream input @(cloud/invoke client operation) output (io/output-stream "result")] (io/copy input output))) ``` This replaces the local file `result`. The guest file must already exist. ## Consume command output as events Use SSE when output should arrive before the command exits. Close the returned reader even if you stop consuming early: ```clojure (with-open [events @(cloud/invoke client (cloud/exec-stream machine-id ["echo" "hello"]))] (loop [] (when-let [event (cloud/stream-read events)] (case (:kind event) :stdout (print (String. ^bytes (:data event) "UTF-8")) :stderr (binding [*out* *err*] (print (String. ^bytes (:data event) "UTF-8"))) :exit (println "Exit:" (:exit-code event)) :error (println "Remote command reported an error")) (recur)))) ``` `stream-read` blocks on the existing response, not a new request. Exit/error is terminal and closes the body. Later reads return `nil`. One reader thread consumes events. Another thread can close it to release a blocked read. Closing is not a remote command-kill guarantee. Output events contain UTF-8 bytes, not binary-exact process output. Frames are limited to 1 MiB, including ignored lines. Malformed exit data, invalid UTF-8, or EOF without a terminal event fails. HTTP timeout covers response headers, not later body reads. There is no reconnect, replay or alternate-route retry. Service error-event text can contain sensitive data. For buffered binary output, request `(cloud/exec machine-id command {:output :b64})` and read the byte fields. For a detached process, use `exec-background` and inspect its `:pid`. Detached execution does not accept stdin or guest timeout and is not a cancellable job. Use jobs when remote cancellation is required. ## Share access, then revoke it Share links and public port links are credentials. Do not print or log them. `endpoint` only describes access. `port-links` changes access: ```clojure (def public-links @(cloud/invoke client (cloud/port-links machine-id))) ;; After the recipient no longer needs access: @(cloud/invoke client (cloud/port-links-revoke machine-id)) ``` The result maps integer guest ports to URLs. Revocation removes all public port links. For machine sharing, use `share` and revoke with `unshare`. Neither operation schedules automatic revocation. ## Lease a child for a task Choose an application lease ID and keep the returned token private: ```clojure (def source-id "replace-with-provisioned-source-id") (def lease-id "replace-with-application-lease-id") (def assignment @(cloud/invoke client (cloud/assign source-id {:lease-id lease-id :task {"command" "inspect input"} :ttl-ms 60000 :heartbeat-ms 10000}))) @(cloud/invoke client (cloud/lease-heartbeat lease-id (:owner-token assignment))) ;; After the task finishes: @(cloud/invoke client (cloud/lease-complete lease-id (:owner-token assignment) {:reason "done" :result {"ok" true}})) ``` This sends one heartbeat, not a heartbeat schedule. Schedule further heartbeats in your application before the lease expires. `lease-info` inspects status. Task/result JSON maps use string keys. Do not log the assignment, task, files, secrets or token. Completion can trigger child cleanup. Verify resource state separately, including after an uncertain completion response. ## Use a versioned cache independently of a machine Caches require a supporting Pro+ plan. Create a cache, then attach it by ID or name when creating a machine: ```clojure (def cache @(cloud/invoke client (cloud/cache-disk-create "example-cache" {:size-gb 1 :mount-path "/cache"}))) (def cached-machine @(cloud/invoke client (cloud/create {:image "alpine:3.21" :cache-disk (:id cache)}))) ;; Start and use the machine, then stop it before publishing: @(cloud/invoke client (cloud/stop (:id cached-machine))) (def version @(cloud/invoke client (cloud/cache-disk-publish (:id cached-machine)))) @(cloud/invoke client (cloud/cache-disk-info (:id cache))) ``` Attachment selects the latest version at the cache’s configured mount path. Publishing appends a version. It does not create/configure the cache resource. Deleting a machine does not delete its cache: ```clojure @(cloud/invoke client (cloud/delete (:id cached-machine))) ;; Only when no other workload needs this cache: @(cloud/invoke client (cloud/cache-disk-delete (:id cache))) ``` **⚠️ WARNING**\ Cache deletion removes **all versions**, not just the published version. The service refuses deletion while a running machine uses the cache. Verify deletion with `cache-disk-info` and expect a not-found response. `artifact-export` also requires a stopped machine and can create durable data. No artifact cleanup endpoint is established. Agree on retention and cleanup before invoking it. Do not treat machine deletion as artifact deletion. ## Validated live scope On 2026-11-07, revision `94708893c6d0a573b765c03a01f0769b46657ce9` completed live checks on JVM and actual Babashka. Each runtime recorded 48 passed, five partial and four blocked statuses across all 57 public cloud functions. Neither run had a final failed status. This is not full live coverage: * Sharing and public-port issuance/revocation completed, but anonymous access and effective revocation were not verified. An earlier HTTP 302 was not followed. * Artifact export covered nonexistent-ID rejection only. Successful export was excluded by the cleanup policy. * Cache creation returned HTTP 402. All four cache operations were blocked. * Raw and automatic checkpoint uploads each used one part and one attempt, with successful marker restoration. Live multi-part concurrency and injected upload failures were not tested. Independent final checks found all 33 machines and six checkpoints from the expanded attempts and probe absent, with zero machines in the account. Two completed synthetic lease-history records were explicitly approved. No cache or exported artifact was allocated. See [cloud workflows](cloud-workflows.adoc) for branching, readiness, jobs and checkpoint upload, and [smol cloud API reference](api/ol-smolvm-cloud.adoc) for all operations and options. ## How to sequence cloud work and clean up resources # How to sequence cloud work and clean up resources Use the `client` from [cloud commands](cloud-commands.adoc). Raw `ol.smolvm.cloud` builders describe one request. `invoke` sends it once. The separate workflow namespace sequences requests and returns a `CompletableFuture` of the final value: ```clojure (require '[ol.smolvm.cloud :as cloud] '[ol.smolvm.cloud.workflow :as workflow]) ``` Replace example IDs, names and paths before use. These operations can create billable resources. The recipes describe API contracts, not proof that every capability has passed live-service validation. ## Find a machine and wait explicitly `connect` tries an exact ID, then an exact name only after not-found. It does not start or wait for a machine. Start it separately and wait for readiness: ```clojure (def machine @(workflow/connect client "replace-with-machine-id-or-name")) (def machine-id (:id machine)) @(cloud/invoke client (cloud/start machine-id)) @(workflow/wait-until-ready client machine-id {:timeout-ms 120000 :interval-ms 1000}) ``` The wait uses server readiness, with a running-state fallback only when the ready field is absent. It does not test your application’s health. A stopped, deleted or error-state machine fails when not ready. Timeout does not delete it. Agent probing is optional and off by default. For one guest-port change, use `workflow/publish` or `workflow/unpublish`: ```clojure @(workflow/publish client machine-id 8080) @(workflow/unpublish client machine-id 8080) ``` These read and replace the port set and return updated machine info. Serialize port changes across all callers. The update is not atomic and is not retried. This is different from granting public access with `cloud/port-links`. ## Run a command in a temporary branch Prepare a branchable source with `:branchable? true` on both `cloud/create` and `cloud/start`. Use a distinct child name. The helper creates the child, waits, executes, then requests deletion: ```clojure (def source-id "replace-with-branchable-source-id") (def task (workflow/branch-exec client source-id "temporary-trial" ["echo" "hello"] {:exec {:timeout-ms 10000} :wait {:timeout-ms 120000} :cleanup-timeout-ms 60000})) @task ``` Nonzero exit is an ordinary exec result. The source is never deleted. Cleanup targets only a child ID returned by successful creation, never a guessed name. Normal success/failure waits for the cleanup attempt. Cleanup failure after successful execution fails the future. If execution also fails, that error is primary and cleanup failure is suppressed. For children that must survive the call, use `cloud/branch` or `cloud/branch-batch` through `invoke` instead. These do not wait or delete children. Batch names must be distinct. Multiple children with published ports require host port zero for allocation. Track every returned ID for later cleanup. ## Distinguish a wait timeout from cancellation For a still-pending `task`, use a limited wait instead of `@task`: ```clojure (deref task 1000 ::still-running) ;; Use only if you intend to cancel local sequencing: (.cancel task true) ``` Timed dereference does not cancel work. Explicit cancellation marks the future cancelled immediately, stops further ordinary scheduling and requests cancellation of active work where possible. Required cleanup can continue. A late branch response containing a child ID still triggers deletion. A cancelled future is **not** proof of remote rollback, command termination or finished cleanup. It cannot later deliver cleanup errors or newly learned IDs. Await normal completion when you need a cleanup outcome. After uncertain mutations, inspect remote resources and explicitly reconcile them. ## Wait for a job and collect bounded output Use a job when remote cancellation is part of the task: ```clojure (def job @(cloud/invoke client (cloud/job-start machine-id ["echo" "hello"]))) (def job-id (:id job)) @(workflow/job-wait client machine-id job-id {:timeout-ms 120000}) (def stdout @(workflow/job-read-all client machine-id job-id {:stream :stdout :max-bytes 16777216})) ``` `job-wait` returns final info for exited jobs, including nonzero exits. A lost job fails. `job-read-all` returns bytes for one stream through EOF. Its default aggregate limit is 16 MiB, with pages of at most 1 MiB. It does not merge stdout and stderr. Use raw `job-output` pages for larger output. Empty nonterminal replies pause before another long poll. Timeout/cancellation of either helper does not cancel the remote job. Invoke `cloud/job-cancel` explicitly when needed. Before using `:user`, invoke `cloud/user-check`. That verifies exec user support, not that jobs honor the account. To upload a file and apply permissions, use the two-request helper: ```clojure @(workflow/file-write client machine-id "/tmp/input" "sample" {:mode 0600}) ``` Without `:mode`, chmod is skipped. Zero explicitly removes permissions. The sequence is not atomic. Failed chmod leaves the uploaded file in place. Use raw `cloud/file-write` when no chmod is required. ## Capture and restore durable checkpoints Capture returns checkpoint metadata, not a downloaded file: ```clojure (def checkpoint @(cloud/invoke client (cloud/checkpoint machine-id))) (def checkpoint-id (:id checkpoint)) (def restored @(cloud/invoke client (cloud/checkpoint-restore checkpoint-id "restored-trial"))) @(cloud/invoke client (cloud/start (:id restored))) @(workflow/wait-until-ready client (:id restored)) ``` Restore without an egress policy uses blocked networking, not the checkpoint’s previous policy. Supply `:egress-policy` explicitly if access is required. `cloud/checkpoints` lists captures for a machine. Treat download URLs as secrets. After deleting machines that reference the checkpoint, request `cloud/checkpoint-delete`. Referenced checkpoints can produce HTTP 409. ## Upload a local checkpoint automatically Keep the nonempty checkpoint file unchanged during transfer. The helper checks file access before allocation, uploads exact file sections, then completes: ```clojure (def progress (atom {:read-bytes 0 :total-bytes 0})) (def upload (workflow/checkpoint-upload client "replace-with-checkpoint-file" {:progress (fn [read-bytes total-bytes] (reset! progress {:read-bytes read-bytes :total-bytes total-bytes}))})) (def uploaded-checkpoint @upload) ``` Defaults match Go’s part policy: four concurrent parts, four attempts per part, and retry delays of 2, 4 and 8 seconds. Transport failures and every non-2xx part response can retry, within the total deadline and attempt limit. Allocation and completion are not retried. File preparation, validation and callback errors do not become HTTP retries. The default total budget is 30 minutes. Progress counts sender reads, not storage acknowledgements. Callbacks are serialized across parts. A failed attempt resets that part’s counter to zero, so aggregate progress can move backward. Reaching total does not mean storage accepted the data. Wait for normal future completion. Callback failure stops further scheduling. Keep callbacks short. The helper closes local part streams. Cancellation is immediate, but active work/resource cleanup can finish later. No partial upload is automatically deleted. Normal failures include a known checkpoint ID. Incomplete-upload abort/deletion semantics are not established. ## Control checkpoint upload one request at a time Use raw steps when the application must record an allocation ID before transfer or supply application scheduling. Determine file size before allocation: ```clojure (def checkpoint-path (.toPath (java.io.File. "replace-with-checkpoint-file"))) (def size-bytes (java.nio.file.Files/size checkpoint-path)) (def plan @(cloud/invoke client (cloud/checkpoint-upload-create size-bytes))) (def allocated-id (get-in plan [:checkpoint :id])) ``` Save `allocated-id` securely before sending parts. Do not log signed URLs. Validate plan geometry against file size. This fragment sends only the first part, not a complete upload: ```clojure @(cloud/invoke client (cloud/checkpoint-upload-part (first (:upload-urls plan)) checkpoint-path {:offset 0 :length (min size-bytes (:part-size-bytes plan))})) ``` The builder performs no file I/O. Invocation opens a fresh bounded section and sends one raw PUT with exact Content-Length. It closes that section afterward. Storage requests receive no API bearer or cookies and follow no redirects. Do not forward authenticated API headers to storage URLs. These are raw parts, not multipart/form-data. Raw `invoke` does not retry. Send every exact section to its corresponding URL and wait for every success before completion: ```clojure @(cloud/invoke client (cloud/checkpoint-upload-complete allocated-id)) ``` Completion sends no ETags. If any part fails, do not complete until your application has resolved the failure. Use the automatic helper unless you need this control. ## Data specs # Data specs `ol.smolvm.spec` registers `clojure.spec.alpha` specs for local and cloud configuration, options and results. Loading it does not initialize a native runtime or contact a service. ```clojure (require '[clojure.spec.alpha :as s] '[ol.smolvm.spec :as spec]) (s/valid? ::spec/local-config {:name "worker" :memory-mib 256}) ;; => true (s/valid? ::spec/exec-options {:timeout-ms 0}) ;; => false ``` ## Selected shapes | | | | | --- | --- | --- | | Data | Local specs | Cloud specs | | Configuration | `::spec/local-config` | `::spec/cloud-client-options`, `::spec/cloud-config` | | Commands | `::spec/exec-options`, `::spec/process-options` | `::spec/cloud-exec-options`, `::spec/cloud-job-options` | | Execution results | `::spec/exec-result`, `::spec/process-result` | `::spec/cloud-exec-result`, `::spec/cloud-job-output` | | Machine records | `::spec/machine-summary` | `::spec/cloud-machine-info` | | Saved state | `::spec/checkpoint-options`, `::spec/restore-options`, `::spec/checkpoint-result` | `::spec/cloud-restore-options`, `::spec/cloud-checkpoint`, `::spec/cloud-upload-plan` | | Resources and ports | `::spec/resize-options`, `::spec/resize-result`, `::spec/published-ports` | `::spec/cloud-ports` | | Individual port changes | `::spec/publish-options`, `::spec/unpublish-options` | — | | Readiness waits | `::spec/wait-ready-options` | — | | HTTP helper options | `::spec/local-http-request-options`, `::spec/local-fetch-options` | — | | Endpoints | `::spec/local-endpoint` | `::spec/cloud-endpoint` | | Branch requests | — | `::spec/cloud-branch-options`, `::spec/cloud-branch-batch-options` | | Checkpoint parts | — | `::spec/cloud-part-options` | | Leases | — | `::spec/cloud-assign-options`, `::spec/cloud-assignment`, `::spec/cloud-lease-complete-options` | | Caches | — | `::spec/cloud-cache-options`, `::spec/cloud-cache` | | SSE | — | `::spec/cloud-stream-options`, `::spec/cloud-stream-event` | Maps accept unrelated keys, including registered qualified keys, and preserve them when conformed. SDK input validation uses the shared field specs without changing its error maps. It does not validate every returned result. These specs do not check opaque handles, machine existence, filesystem access or native feature support. Cloud record specs allow additional server fields and do not describe every nested resource or usage field. For example, check helper options without connecting to a machine: ```clojure (s/valid? ::spec/publish-options {:guest 8080}) ;; => true (s/valid? ::spec/unpublish-options {}) ;; => false (s/valid? ::spec/wait-ready-options {:timeout-ms 10000 :interval-ms 250}) ;; => true (s/valid? ::spec/local-http-request-options {:method :post :headers {"Content-Type" "application/json"} :body "{\"command\":\"refresh\"}"}) ;; => true ``` `branch-exec` uses `::spec/exec-options` and returns `::spec/exec-result`. The endpoint spec describes URL data, not proof that a service is listening. For practical examples, see [Check a service and read its HTTP response](local-workloads.adoc#check-service-readiness). ## Cloud request values Part ranges require a nonnegative offset and positive length. Cache size is 1 through 500 GB. Lease task/result maps use string keys and JSON values: ```clojure (s/valid? ::spec/cloud-part-options {:offset 0 :length 1048576}) ;; => true (s/valid? ::spec/cloud-part-options {:offset 0 :length 0}) ;; => false (s/valid? ::spec/cloud-cache-options {:size-gb 1 :mount-path "/cache"}) ;; => true (s/valid? ::spec/cloud-assign-options {:lease-id "example-lease" :task {"command" "inspect"}}) ;; => true ``` Valid part options do not prove that a file exists or that a signed URL permits upload. Cache specs do not prove plan entitlement or deletion safety. Workflow futures resolve to data, not machine handles. Result specs describe that data, not the future’s state or completion of remote cleanup. See [cloud workflows](cloud-workflows.adoc) for resource handling. Spec explanations can contain the supplied values. Do not log explanations that contain API keys or interceptor credentials. ## ol.smolvm # ol.smolvm > Clojure bindings for local microVMs and [smol cloud](https://smolmachines.com/) machines. ![doc](https://img.shields.io/badge/doc-outskirtslabs-orange.svg) ![status: experimental](https://img.shields.io/badge/status-experimental-orange.svg) `ol.smolvm` lets JVM Clojure programs and Babashka scripts run guest commands, transfer files, stream output, and manage smol machines. Use `ol.smolvm` for local microVMs or `ol.smolvm.cloud` for the [smol cloud](https://smolmachines.com/) HTTP API. Local execution needs a native runtime and hardware virtualization. [smol cloud](https://smolmachines.com/) operations use an API token and need no local hypervisor. Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**. This SDK is unreleased. Use a source checkout. ## Quick Start Run `uname -a` in a temporary Alpine machine with outbound networking: ```clojure (require '[ol.smolvm :as smol]) ;; => nil (smol/initialize) ;; => {:version "1.32.4", :platform "linux-x64", :abi-version 1} (smol/check-host) ;; => nil (with-open [machine (smol/create {:name "readme-example" :network? true :image "alpine"})] (print (:stdout (smol/exec machine ["uname" "-a"])))) ;; prints: Linux readme-example 6.18.55 ... x86_64 Linux ;; => nil ``` Closing this handle deletes the machine and its storage, including when execution throws. ## Installation For local execution on Linux x86-64, use Nix with flakes enabled and a host with access to `/dev/kvm`. Clone the SDK and enter its development shell: ```shell git clone https://github.com/outskirtslabs/smolvm-clj.git cd smolvm-clj nix develop ``` Build the pinned runtime companion JAR and start a Clojure REPL: ```shell bb native:jars :platforms '["linux-x64"]' clojure -J-Xss4m -J--enable-native-access=ALL-UNNAMED \ -J-Dbabashka.json.provider=org.clojure/data.json \ -Sdeps '{:paths ["src" "resources" "target/smol-native-linux-x64-1.32.4.jar"]}' -M ``` See [installation](https://docs.outskirtslabs.com/ol.smolvm/next/installation) for Babashka, other supported hosts, and setup for [smol cloud](https://smolmachines.com/). ## Documentation * [Docs](https://docs.outskirtslabs.com/ol.smolvm/next/) * [API Reference](https://docs.outskirtslabs.com/ol.smolvm/next/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/smolvm-clj/issues) ## Licensing The Clojure code uses [EUPL-1.2](https://spdx.org/licenses/EUPL-1.2.html). Native runtime bundles include separate upstream licenses and notices. ## How to install the SDK # How to install the SDK The SDK supports local microVMs and [smol cloud](https://smolmachines.com/). Both use the `com.outskirtslabs/smolvm-clj` library from Clojars. Local execution also needs native binaries for your host. Access to smol cloud uses HTTP and needs no native binaries. Add dependencies to `deps.edn` for Clojure or `bb.edn` for Babashka. Replace `SDK_VERSION` in the examples with the SDK version you want from [Clojars](https://clojars.org/com.outskirtslabs/smolvm-clj). ## Use local machines Local machines run on your computer through the smol native runtime. We do not build that runtime. We redistribute upstream release binaries in platform-specific companion JARs, with their upstream licences and notices. You do not need to compile Rust code or install the smol CLI. Add the main library and the companion for your host. For Linux x86-64: ```clojure {:deps {com.outskirtslabs/smolvm-clj {:mvn/version "SDK_VERSION"} com.outskirtslabs/smolvm-clj-native-linux-x64 {:mvn/version "1.32.4"}}} ``` The companion version follows the upstream runtime, not the Clojure library. This SDK uses runtime 1.32.4. Choose the companion that matches your host: | Host | Coordinate | | --- | --- | | Linux x86-64 | `com.outskirtslabs/smolvm-clj-native-linux-x64` | | Linux ARM64 | `com.outskirtslabs/smolvm-clj-native-linux-arm64` | | macOS Apple Silicon | `com.outskirtslabs/smolvm-clj-native-darwin-arm64` | When you call `initialize`, the SDK finds the companion on the classpath, verifies their checksums and extracts them to a private temporary directory. Normal process exit removes these files. Forced termination can leave the directory on disk. The SDK does not download binaries at runtime. The companion supplies the native library, VM executable, guest root filesystem and supporting files. ```clojure (require '[ol.smolvm :as smol]) (smol/initialize) (smol/check-host) ;; => nil ``` A `nil` host check means the host can run local machines. Otherwise, the result includes a `:code` and `:reason` describing the problem. The native binaries do not replace the host’s virtualization support. Linux needs glibc 2.34 or later, hardware virtualization and access to `/dev/kvm`. macOS uses system virtualization. Local execution is not supported on Windows or Intel macOS. ### Supply a runtime directory instead Use `:runtime-dir` if you manage the native files separately, for example when packaging an application or preparing an offline installation. Keep the main SDK dependency, but omit the companion JAR. Unpack the upstream runtime 1.32.4 bundle for your host and pass its directory to `initialize`: ```clojure (smol/initialize {:runtime-dir "/absolute/path/to/runtime"}) ``` The path must point to the complete unpacked bundle, not just its shared library (`libsmol_go.so` on Linux or `libsmol_go.dylib` on macOS). Include the guest root filesystem and supporting files. Keep `smol-vmm` executable and the bundle’s file layout intact. Use this call instead of the no-argument `initialize` above. Once a process has initialized a runtime, restart it before selecting a different directory. ## Use smol cloud For smol cloud, install only the main library: ```clojure {:deps {com.outskirtslabs/smolvm-clj {:mvn/version "SDK_VERSION"}}} ``` The `ol.smolvm.cloud` namespace sends HTTP requests to the service. It does not load the local runtime, so you need neither a companion JAR nor hardware virtualization. Do not call `ol.smolvm/initialize` for cloud use. Set `SMOL_API_KEY` to your API token before starting the process, then create a client: ```clojure (require '[ol.smolvm.cloud :as cloud]) (def client (cloud/client {:api-key (System/getenv "SMOL_API_KEY")})) ``` Keep the token out of source files and logs. Continue with [How to run smol cloud commands](cloud-commands.adoc). ## Configure Clojure or Babashka On the JVM, select the JSON provider with `-Dbabashka.json.provider=org.clojure/data.json`. Local execution also requires Java 25 or later and `--enable-native-access=ALL-UNNAMED`. Set `-Xss4m` for a four-MiB thread stack. The pinned native checkpoint code exceeds a one-MiB stack and can crash the JVM. Pass these as `-J` options with the Clojure CLI, for example `-J-Xss4m`. These are JVM options, not SDK options. For Babashka, use version 1.13.225 or later. Local execution needs an FFI-enabled build. On Linux, use a dynamic build. Babashka uses its built-in Cheshire JSON implementation and needs none of the JVM options above. The same companion JAR or runtime-directory choices apply to local execution. ## How to run local commands # How to run local commands Complete [installation](installation.adoc) and initialize the local runtime before running these examples. ## Run a command in a temporary machine Choose an unused machine name. Pass commands as vectors of strings: ```clojure (require '[ol.smolvm :as smol]) (with-open [machine (smol/create {:name "command-example" :cpus 1 :memory-mib 256})] (smol/exec machine ["echo" "hello"])) ``` The result includes `:exit-code`, `:stdout`, `:stderr`, and byte arrays for both output streams. Check `:exit-code` for success. A nonzero exit code does not throw. **⚠️ WARNING**\ Closing a handle from `create` deletes the machine and its storage. The example deletes both when `with-open` ends, including after an exception. Copy any required files out before leaving the block. Commands do not run through a shell. For pipes or redirection, explicitly run a shell available in the guest: ```clojure (with-open [machine (smol/create {:name "shell-example"})] (smol/exec machine ["sh" "-c" "printf hello > /tmp/message; cat /tmp/message"] {:timeout-ms 5000})) ``` `exec` buffers all output in memory. For large output, use [streaming output](#stream-command-output). ## Transfer a file Use absolute guest paths. `file-write` creates parent directories and replaces existing content: ```clojure (with-open [machine (smol/create {:name "file-example"})] (smol/file-write machine "/workspace/message" "hello") (String. ^bytes (smol/file-read machine "/workspace/message") "UTF-8")) ``` The returned string is `"hello"`. For binary data, pass a byte array and use the returned byte array directly. If a consumer expects an input stream, use `file-read-stream`: ```clojure (with-open [machine (smol/create {:name "file-stream-example"})] (smol/file-write machine "/workspace/message" "hello") (with-open [input (smol/file-read-stream machine "/workspace/message")] (slurp input :encoding "UTF-8"))) ``` This also returns `"hello"`. The entire file is buffered before the stream is returned. It is not a bounded-memory transfer. Read errors occur during `file-read-stream`, not during later stream reads. Close the returned stream. ## Use an existing machine List machines, then connect by name: ```clojure (smol/list) (with-open [machine (smol/connect "existing-machine")] {:name (smol/machine-name machine) :result (smol/exec machine ["echo" "hello"])}) ``` Replace `"existing-machine"` with a name from the list. `connect` starts the machine if needed. Closing this handle does not stop or delete the machine. `machine-name` reads the handle without a native call. Local IDs equal machine names. This accessor does not check whether the machine still exists. To stop a machine but keep its disk, call `stop` before closing the handle. To delete the machine and its storage, call `delete`. ## Stream command output Use `process` to consume command output through Java streams. It returns a process record with live output streams and a `CompletableFuture` in `:result`. Waiting on that future needs no extra thread. ### Copy stdout as the command runs Capture stderr separately so it cannot fill an unread Java pipe: ```clojure (require '[clojure.java.io :as io]) (with-open [machine (smol/create {:name "stream-example"})] (let [process (smol/process machine ["echo" "hello"] {:err :string :timeout-ms 5000})] (try (with-open [^java.io.InputStream output (:out process)] (io/copy output System/out)) (deref (:result process) 10000 ::timeout) (finally (smol/destroy process))))) ``` The command prints `hello`. The result future supports timed dereference on both Clojure and Babashka. Failures throw `ExecutionException`. Use `ex-cause` to inspect the original failure. Timed dereference waits up to ten seconds for `{:exit n :out value :err value}`, then returns `::timeout` if no result is available. A wait timeout does not cancel the command. The `finally` block requests cancellation if the process is still running. Use `smol/destroy`, not `future-cancel`, to stop the guest command. This wait limit does not interrupt the preceding `io/copy`. The separate `:timeout-ms` option limits guest execution to five seconds. It does not limit time spent writing to an output stream. Close the returned input streams after use. The process record is not Closeable, so do not put it in `with-open`. If both outputs use the default `:stream` mode, drain them concurrently. Do not wait for the process before draining its live output streams. ### Forward both outputs If you do not need to read the output in Clojure, pass output streams: ```clojure (with-open [machine (smol/create {:name "forward-example"})] (let [process (smol/process machine ["echo" "hello"] {:out System/out :err System/err :timeout-ms 5000})] (try (deref (:result process) 10000 ::timeout) (finally (smol/destroy process) (.flush System/out) (.flush System/err))))) ``` The library does not close or flush supplied output streams. For short output, choose `:string` or `:bytes` instead. ### Consume native events Use `stream-open` when you need separate stdout, stderr and exit events. Use one reader and close the handle even after an exit event: ```clojure (with-open [machine (smol/create {:name "event-example"}) stream (smol/stream-open machine ["echo" "hello"] {:timeout-ms 5000})] (loop [] (when-let [event (smol/stream-next stream)] (case (:kind event) :stdout (.write System/out ^bytes (:data event)) :stderr (.write System/err ^bytes (:data event)) :exit (println "Exit:" (:exit-code event)) :error (throw (ex-info (:message event) event))) (recur))) (.flush System/out) (.flush System/err)) ``` Output events carry bytes. A UTF-8 character can cross event chunks, so use a streaming decoder if you need text. Use `stream-kill` to cancel a native stream command. It does not close the handle. Do not pass process records to native stream functions. Native output queues are unbounded. Consume output promptly. Streaming stdin is not supported. For staged mounts, networking and images, see [How to configure local workloads](local-workloads.adoc). For branches, checkpoints and packing, see [How to reuse and save machine state](saved-machines.adoc). See [Local API reference](api/ol-smolvm.adoc) for image configuration, mounts, ports, stream and process options, and cancellation. For smol cloud file streams, see [How to run smol cloud commands](cloud-commands.adoc). ## How to configure local workloads # How to configure local workloads Complete [installation](installation.adoc) and initialize the runtime. Use unused machine names. The examples below are independent. ```clojure (require '[ol.smolvm :as smol]) ``` ## Copy staged work back to the host Create `/tmp/smol-work` on the host before running this example. A staged mount gives the guest a local working copy. Call `sync` to copy changes back without stopping the machine: ```clojure (with-open [machine (smol/create {:name "staged-work" :mounts [{:source "/tmp/smol-work" :target "/workspace" :staged? true}]})] (smol/file-write machine "/workspace/result.txt" "finished") (smol/sync machine)) ``` Read `/tmp/smol-work/result.txt` on the host to check the result. Graceful stop and deletion also copy staged changes back. Staged mounts cannot be read-only or use `s3://` sources. Do not use them with pause or checkpoint capture. **⚠️ WARNING**\ Synchronization can change host files. Use a dedicated work directory, not an irreplaceable source directory. `sync` is not a general disk flush. ## Publish a guest service Request a free host TCP port, then read the assigned mapping: ```clojure (with-open [machine (smol/create {:name "service-example" :ports [{:host 0 :guest 8080}]})] {:host-port (smol/host-port machine 8080) :mappings (smol/published-ports machine)}) ``` Start your service inside the same scope before using that port. The example only allocates forwarding. A mapping does not prove that a guest service is listening. `guest-ports` lists forwarding targets, not listeners. To replace mappings, call `ports-set` with the complete desired vector. TCP mappings can change while running when the native ports control socket supports the operation. For UDP changes, stop the machine first: ```clojure (with-open [machine (smol/create {:name "udp-example"})] (smol/stop machine) (smol/ports-set machine [{:host 0 :guest 5353 :protocol :udp}]) (smol/start machine) (smol/published-ports machine)) ``` Pass `[]` to remove all mappings. Read `published-ports` after an update for effective host ports. `start` waits for guest command execution, not application readiness. ### Add or remove one mapping Use `publish` instead of replacing the full vector yourself. This example connects to a machine whose native control socket supports live TCP changes: ```clojure (with-open [machine (smol/connect "existing-machine")] (let [mapping (smol/publish machine {:guest 8080})] (try (println "Published:" mapping) (finally (smol/unpublish machine mapping))))) ``` Omitting `:host` selects a free host port. The result includes `:host`, `:guest` and `:protocol`. The example removes that mapping before closing its handle. `unpublish` matches all supplied selectors and defaults to TCP. For UDP, stop the machine before either operation. **⚠️ WARNING**\ Serialize port changes across all handles and processes for a machine. These helpers read and replace the mappings, so concurrent updates can be lost. Free-port selection releases its socket before publication, allowing another process to take the port. A readback error can follow a successful update. Inspect `published-ports` before deciding whether to retry. ## Check a service and read its HTTP response First start your service and publish its guest port. The following example assumes an existing machine named `http-service`, an HTTP service on guest port 8080, and a published mapping for that port: ```clojure (with-open [machine (smol/connect "http-service")] (println "Probe:" (smol/ready? machine)) (smol/wait-until-ready machine {:timeout-ms 10000 :interval-ms 250}) (let [urls (smol/endpoint machine 8080 "/health") response (smol/fetch machine 8080 "/health" {:timeout-ms 5000})] (with-open [^java.io.InputStream body (:body response)] {:endpoint urls :status (:status response) :text (slurp body :encoding "UTF-8")}))) ``` Check both `:status` and the service’s response content. HTTP error statuses are returned, not thrown. Always close the response body, including for errors. The request timeout limits receipt of headers, not subsequent body reads. Without `:timeout-ms`, `fetch` has no request timeout. The readiness probe is a TCP heuristic, not an application health check or UDP check. For each published guest port it connects within 250 ms, then tries to read one byte within 100 ms. A byte or read timeout counts as ready. EOF, reset or connection failure does not. This can consume a greeting byte and produce service logs. A running machine with no guest ports passes. Native state or mapping errors propagate. Waits are explicit. Create, connect, start, resume and branch do not call `wait-until-ready` automatically. Its defaults are 120 seconds and a one-second interval. A wait timeout or thread interruption does not stop or delete the machine. The deadline does not interrupt an active call or probe. This helper does not wait for a process result. `endpoint` returns `:http-url`, `:ws-url` and empty `:headers` for the current loopback mapping. It does not send a request, probe or publish a port. Use URI-valid paths and percent escapes. Missing mappings throw `CONFIG_ERROR`. `fetch` follows HTTP/HTTPS redirects, including HTTPS to HTTP. Redirects can leave the local machine. Redirect limits and header forwarding follow Java’s HTTP client, not Go’s exact policy. ### Build a request for another HTTP method Use `http-request` to get request data for `babashka.http-client`: ```clojure (with-open [machine (smol/connect "http-service")] (smol/http-request machine 8080 "/jobs" {:method :post :headers {"Content-Type" "application/json"} :body "{\"command\":\"refresh\"}"})) ``` This returns a map with `:method`, `:uri`, `:headers` and `:body`, without sending HTTP. It reads the current native port mapping. Omit `:method` for GET. Pass the map to `babashka.http-client/request` with the response and transport options you need. Keep any supplied input-stream body open through request completion. Keep the target machine running until the request finishes. For input and result shapes, see [Data specs](data-specs.adoc). ## Restrict outbound access Stop an existing machine before replacing its policy. Closing a connected handle leaves that machine intact: ```clojure (with-open [machine (smol/connect "existing-machine")] (smol/stop machine) (smol/egress-policy-set machine {:network? false :allowed-cidrs ["192.0.2.0/24"]}) (smol/start machine)) ``` Replace the example CIDR with the network your workload needs. Use `{:network? false}` with no allow lists to deny outbound access, or `:network? true` for unrestricted access. The engine validates policy rules. Saved-memory machines cannot switch network backends. An egress interceptor is a separate service, not supplied by this library. ## Run image-based work Pull an image into machine storage, inspect the cache, then run a short command: ```clojure (with-open [machine (smol/create {:name "image-work" :network? true})] (smol/image-pull machine "alpine:3.21") (smol/image-list machine) (smol/run machine "alpine:3.21" ["echo" "hello"])) ``` Image pulling needs registry access. `run` also pulls if the image is not cached. Check `:exit-code` and `:stdout` in its result. It buffers all output and rejects `:stdin`, `:user` and `:cancel`. For a particular guest user, create an image-backed machine and use `exec` instead. ## Start work without waiting for its exit Use `exec-background` when you only need a guest PID. Connect to an existing machine so closing the handle does not end the work: ```clojure (with-open [machine (smol/connect "existing-machine")] (smol/exec-background machine ["sleep" "60"])) ``` The machine continues running after this scope. Stop or delete it explicitly when its work is complete. If you use a `create` handle instead, keep that handle open until the work finishes. The guest PID is not the host VM PID returned by `pid`. There is no background result, output capture or cancellation handle. For exit status and cancellation, use [process or exec](local-commands.adoc). See [Local API reference](api/ol-smolvm.adoc) for complete configuration and option tables. ## How to reuse and save machine state # How to reuse and save machine state Initialize the runtime as described in [installation](installation.adoc). Use unused machine names and artifact paths. These examples need native branch and checkpoint support on the host. A supported host can still reject a machine’s resource or device configuration. ```clojure (require '[ol.smolvm :as smol]) ``` ## Run work from a prepared machine Create a branchable source, prepare its files, then branch for each task. Keep the source open until its children have closed: ```clojure (with-open [source (smol/create {:name "prepared-source" :branchable? true})] (smol/file-write source "/tmp/input" "prepared data") (with-open [child (smol/branch source "task-child")] (smol/exec child ["cat" "/tmp/input"]))) ``` Check the returned `:stdout` and `:exit-code`. Closing the child deletes its machine and storage, not the source. Closing the outer scope deletes the source. The engine refuses to delete a source with dependent children. For an existing machine, stop it before calling `start-branchable`. That function does not convert an already running machine. For several children, `branch-batch` returns a vector, not a Closeable handle: ```clojure (with-open [source (smol/create {:name "batch-source" :branchable? true})] (let [children (smol/branch-batch source ["task-a" "task-b"])] (try (mapv #(smol/exec % ["echo" "ready for work"]) children) (finally (doseq [^java.io.Closeable child children] (.close child)))))) ``` Omit explicit ports for a multi-child batch. Empty ports remap any source forwards to fresh host ports rather than removing them. Neither branch call checks application listeners. Use `host-port` to find a mapping, then apply your application’s readiness check separately. ## Run one command in a disposable branch Use `branch-exec` when the child needs only one command. Supply an unused child name and ordinary `exec` options: ```clojure (with-open [source (smol/create {:name "single-task-source" :branchable? true})] (smol/file-write source "/tmp/input" "prepared data") (smol/branch-exec source "single-task-child" ["cat" "/tmp/input"] {:timeout-ms 5000})) ``` Check `:stdout` and `:exit-code` as with `exec`. A nonzero exit does not throw. The helper deletes its child after execution, including after an execution error. It does not delete a pre-existing machine if the name conflicts. Cleanup failure after success throws. If execution also failed, its exception carries the cleanup failure as a suppressed exception. The helper does not delete the source, though branching can briefly pause it. It does not wait for application listeners or accept extra branch options. ## Save a checkpoint and restore a separate machine Use a branchable source without host mounts, staged mounts, remote volumes, GPU or CUDA state. Other host-bound features can also prevent capture. Choose an output path that does not exist and ends in `.checkpoint` or the legacy `.smolcheckpoint` suffix (case-insensitive). This applies to portable files and stored checkpoint directories. ```clojure (with-open [source (smol/create {:name "checkpoint-source" :branchable? true})] (smol/file-write source "/tmp/input" "saved data") (smol/checkpoint source "/tmp/saved-machine.checkpoint")) (with-open [restored (smol/checkpoint-restore "/tmp/saved-machine.checkpoint" "restored-task")] (smol/exec restored ["cat" "/tmp/input"])) ``` The restored command should return `"saved data"` in `:stdout`. Closing either machine does not delete the checkpoint. Delete the artifact explicitly when it is no longer needed. A failed restore start or policy update cleans up the newly installed machine, not the artifact. Restore can fetch referenced images on the host even when guest networking is disabled. Saved-memory restores cannot switch network backends. For repeated captures, pass `{:store "/absolute/checkpoint-store"}` to `checkpoint` and use a new directory such as `/tmp/capture-2.checkpoint` as its output. Use `checkpoint-export` to make a portable file from that directory. The export destination must not exist. A `.checkpoint` suffix is useful for recognition but is not required for export. `checkpoint-prune` deletes unreferenced store objects, not active checkpoints. Keep checkpoint directories and their store together until export. ## Suspend work and continue later Use `pause` to save execution durably and `resume` to continue it: ```clojure (with-open [machine (smol/create {:name "pause-example" :branchable? true})] (smol/file-write machine "/tmp/input" "continue here") (smol/pause machine) (smol/resume machine) (smol/exec machine ["cat" "/tmp/input"])) ``` Pause permits ordinary host mounts but rejects staged mounts, remote volumes, GPU and CUDA state. To leave a paused machine for another session, connect to an existing machine instead of creating a deleting handle in `with-open`. Closing a `create` handle deletes the machine even if it is paused. ## Increase capacity during a task Pass target totals, not increments. For example, grow a machine from 256 MiB to 384 MiB: ```clojure (with-open [machine (smol/create {:name "resize-example" :memory-mib 256})] (smol/resize machine {:memory-mib 384})) ``` Check the returned resource totals. The guest must support live growth and the host must have enough memory. RAM growth uses 128 MiB increments from boot RAM, within runtime capacity. RAM and disks cannot shrink. Disk changes fail while dependent branches exist. CPU changes have narrower platform support. **⚠️ WARNING**\ Resize is not transactional. A later failure can leave an earlier resource change applied. Inspect the machine before retrying. ## Package a stopped machine Stop the machine, then pack its disk state: ```clojure (with-open [machine (smol/create {:name "pack-example"})] (smol/file-write machine "/tmp/input" "packaged data") (smol/stop machine) (smol/pack machine "/tmp/prepared-machine")) ``` This writes both `/tmp/prepared-machine`, a runnable file, and `/tmp/prepared-machine.smolmachine`, the artifact. The return value is the artifact path. Packing uses the helper included in the native runtime. Neither file is deleted when the machine closes. Remove both explicitly when finished. Use checkpoints, not pack, to save running memory state. See [Local API reference](api/ol-smolvm.adoc) for resource limits, platform restrictions and complete option tables.