smolmachines 1.22.2 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,6 +87,33 @@ try {
87
87
  }
88
88
  ```
89
89
 
90
+ For a local VM whose published service starts through `exec()`, set
91
+ `waitForPorts: false` on creation. The SDK still waits for the guest agent,
92
+ so `exec()` is safe; call `waitUntilReady()` once the service is listening.
93
+ The default keeps waiting for all published ports. This option is local-only.
94
+
95
+ ```ts
96
+ const m = await Machine.create(
97
+ { image: 'node:24-alpine', ports: [{ host: 18080, guest: 8080 }], waitForPorts: false },
98
+ { target: 'local' },
99
+ );
100
+ try {
101
+ await m.exec(['sh', '-c',
102
+ `nohup node -e 'require("http").createServer((_, res) => res.end("ok"))
103
+ .listen(8080, "0.0.0.0")' >/tmp/server.log 2>&1 &`,
104
+ ]);
105
+ await m.waitUntilReady();
106
+ console.log(await (await m.fetch(8080)).text());
107
+ } finally {
108
+ await m.delete();
109
+ }
110
+ ```
111
+
112
+ When reconnecting to clean up a local machine whose published service has
113
+ stopped, `Machine.connect(name, { target: 'local', waitForPorts: false })`
114
+ attaches after the agent is ready, so `delete()` can run without a listener.
115
+ The default continues waiting for all published ports.
116
+
90
117
  To reach a service **inside** the VM, use the authenticated connect bridge —
91
118
  **no Cloudflare/localhost.run tunnel, no public exposure, no egress allow-list.**
92
119
  Have the worker LISTEN on a published port and connect *inbound*:
@@ -249,6 +276,51 @@ const session = await Machine.restoreCheckpoint('prepared.smolcheckpoint', 'sess
249
276
  allow lists on the host. A restored machine keeps its checkpoint's backend, so
250
277
  an allow list on a TSI checkpoint is refused rather than left unenforced.
251
278
 
279
+ ### API keys the workload uses without seeing
280
+
281
+ Bind a credential to the hosts it is for. The guest variable holds a
282
+ placeholder, and the real value is substituted only in the headers of HTTPS
283
+ requests to those hosts:
284
+
285
+ ```ts
286
+ const machine = await Machine.create({
287
+ image: 'alpine:3.20',
288
+ credentials: [
289
+ {
290
+ name: 'notion',
291
+ envVar: 'NOTION_API_KEY',
292
+ hosts: ['api.notion.com', 'files.notion.com'],
293
+ value: process.env.NOTION_API_KEY,
294
+ },
295
+ ],
296
+ });
297
+ await machine.exec(['sh', '-c', 'curl -H "Authorization: Bearer $NOTION_API_KEY" https://api.notion.com/v1/users/me']);
298
+ ```
299
+
300
+ - Hosts are exact names; list each subdomain.
301
+ - Locally the value stays in this process's memory. Without `value`, this
302
+ process's own `envVar` is read at each start, which is how a machine reopened
303
+ by a later process gets it. `methods: ['GET', 'HEAD']` limits a read-only token.
304
+ - On the cloud, a credential with a `value` is stored for your account under
305
+ `name`; one without refers to a credential already stored there.
306
+
307
+ ## Cache disks (cloud)
308
+
309
+ A cache disk is a disk image many machines start from, each through its own
310
+ copy-on-write layer: dependencies, build caches, datasets. Publish a stopped
311
+ machine's cache as the next version, and later machines start from it.
312
+
313
+ ```ts
314
+ import { CacheDisk, Machine } from "smolmachines";
315
+
316
+ await CacheDisk.create({ name: "deps" }); // v0: empty
317
+ const m = await Machine.create({ image: "node:22", cacheDisk: { cache: "deps" } });
318
+ await m.exec(["sh", "-c", "cd /cache && npm install"]);
319
+ await m.stop();
320
+ await m.publishCacheDisk(); // v1
321
+ // Every machine created with cacheDisk: { cache: "deps" } now starts from v1.
322
+ ```
323
+
252
324
  ## Building from source
253
325
 
254
326
  This package's native core lives alongside it (Rust, `src/*.rs`) and links the
package/dist/binding.d.ts CHANGED
@@ -36,6 +36,47 @@ export interface MachineConfig {
36
36
  persistent?: boolean
37
37
  /** If true, every start uses cloneable, memfd-backed guest RAM. */
38
38
  forkable?: boolean
39
+ /**
40
+ * If true, the VM outlives this process: its boot subprocess does not
41
+ * watch the parent, so it keeps running when this process exits or
42
+ * crashes, and a later process reattaches with `connect`. Remembered by
43
+ * the machine, so later starts are detached too.
44
+ */
45
+ detached?: boolean
46
+ /**
47
+ * Caller metadata stored with the machine and returned by `list`; the
48
+ * engine never interprets it.
49
+ */
50
+ labels?: Record<string, string>
51
+ /**
52
+ * Credentials the workload uses without seeing them, as the CLI's
53
+ * `--credential`.
54
+ */
55
+ credentials?: Array<CredentialConfig>
56
+ }
57
+ /** One machine from this host's shared machine database. */
58
+ export interface MachineSummary {
59
+ /** Machine name. */
60
+ name: string
61
+ /**
62
+ * Lifecycle state as `state()` reports it: `running`, `stopped`,
63
+ * `frozen`, `paused`, `failed`, ...
64
+ */
65
+ state: string
66
+ /** OCI image the machine boots, when it is an image machine. */
67
+ image?: string
68
+ /** Caller metadata given at create. */
69
+ labels: Record<string, string>
70
+ /** Host PID of the VM process while it is running. */
71
+ pid?: number
72
+ /** Whether the record outlives the process that created it. */
73
+ persistent: boolean
74
+ /** Whether the VM outlives the process that starts it. */
75
+ detached: boolean
76
+ /** Whether it starts as a live branch source. */
77
+ branchable: boolean
78
+ /** Creation time, seconds since the Unix epoch. */
79
+ createdAt: number
39
80
  }
40
81
  /** Whether this host can run local machines, from the engine's own checks. */
41
82
  export interface HostAvailability {
@@ -89,6 +130,26 @@ export interface MachineResources {
89
130
  /** Overlay disk size in GiB, or unset while it has the default size. */
90
131
  overlayGib?: number
91
132
  }
133
+ /**
134
+ * A credential the workload uses without seeing it: the guest variable
135
+ * `env_var` holds a placeholder, and the host substitutes the real value in
136
+ * request headers of HTTPS requests to `hosts` only.
137
+ */
138
+ export interface CredentialConfig {
139
+ /** Binding name. */
140
+ name: string
141
+ /** Guest environment variable that holds the placeholder. */
142
+ envVar: string
143
+ /** Exact host names the value may be sent to. */
144
+ hosts: Array<string>
145
+ /** HTTP methods the value may be used with; all supported ones when unset. */
146
+ methods?: Array<string>
147
+ /**
148
+ * The value, held in this process's memory only. When unset, this
149
+ * process's own variable named `env_var` is read at each start.
150
+ */
151
+ value?: string
152
+ }
92
153
  /** VM resource allocation. */
93
154
  export interface VmResourcesConfig {
94
155
  /** Number of vCPUs (default: src/data/resources.rs default). */
@@ -123,6 +184,22 @@ export interface VmResourcesConfig {
123
184
  * CUDA toolkit needed in the image). Local target only (default: false).
124
185
  */
125
186
  cuda?: boolean
187
+ /**
188
+ * Network backend: `"tsi"` (outbound-only, the default for plain network
189
+ * access) or `"virtio-net"`. A machine that will be checkpointed and have
190
+ * its egress policy changed later needs virtio-net, since the policy is
191
+ * enforced by virtio-net's host-side stack and a restore keeps the backend.
192
+ */
193
+ networkBackend?: string
194
+ }
195
+ /** A replacement outbound network policy for a stopped machine. */
196
+ export interface EgressPolicyConfig {
197
+ /** Outbound network access at all. */
198
+ network: boolean
199
+ /** Addresses the machine may reach; empty means no address restriction. */
200
+ cidrs?: Array<string>
201
+ /** Host names (`api.github.com`, or `*.github.com` for subdomains only). */
202
+ hosts?: Array<string>
126
203
  }
127
204
  /** Options for executing a command. */
128
205
  export interface ExecOptions {
@@ -218,8 +295,25 @@ export declare class NapiMachine {
218
295
  * process — backs the SDK's local `Machine.connect()`.
219
296
  */
220
297
  static connect(name: string, interceptorAddress?: string | undefined | null, interceptorToken?: string | undefined | null): NapiMachine
221
- /** Create a stopped machine from a portable live checkpoint on disk. */
298
+ /**
299
+ * Every machine in this host's shared machine database, in name order.
300
+ * The database is shared with the CLI and every other embedder on the
301
+ * host, so machines other processes made are listed too — `labels` tell
302
+ * a process's own apart after it restarts.
303
+ */
304
+ static list(): Promise<Array<MachineSummary>>
305
+ /**
306
+ * Create a stopped machine from a portable live checkpoint on disk.
307
+ * `keep_identity` restores it as the same machine going back in time, so
308
+ * its first start skips the identity reset a clone needs.
309
+ */
222
310
  static restoreCheckpoint(name: string, artifact: string, keepIdentity?: boolean | undefined | null): NapiMachine
311
+ /**
312
+ * Replace this stopped machine's outbound network policy, e.g. to give a
313
+ * machine just restored from a shared checkpoint its own allow list
314
+ * before it first boots.
315
+ */
316
+ setEgressPolicy(policy: EgressPolicyConfig): void
223
317
  /** Export a stored checkpoint directory as one portable checkpoint file. */
224
318
  static exportCheckpoint(source: string, output: string): number
225
319
  /** Remove objects that no retained checkpoint in a local store references. */
@@ -265,12 +359,12 @@ export declare class NapiMachine {
265
359
  * live RAM + disks (same host). `ports` are `{ host, guest }` inbound
266
360
  * forwards for the clone. Returns a handle to the running clone.
267
361
  */
268
- fork(name: string, ports?: Array<PortMappingConfig> | undefined | null, checkpointable?: boolean | undefined | null): Promise<NapiMachine>
362
+ fork(name: string, ports?: Array<PortMappingConfig> | undefined | null, checkpointable?: boolean | undefined | null, freezeSource?: boolean | undefined | null): Promise<NapiMachine>
269
363
  /**
270
364
  * Fork many clones from one retained snapshot and boot them in bounded
271
365
  * parallel waves. Transactional: an error removes every clone in this call.
272
366
  */
273
- forkBatch(names: Array<string>, ports?: Array<PortMappingConfig> | undefined | null, parallel?: number | undefined | null): Promise<Array<NapiMachine>>
367
+ forkBatch(names: Array<string>, ports?: Array<PortMappingConfig> | undefined | null, parallel?: number | undefined | null, freezeSource?: boolean | undefined | null): Promise<Array<NapiMachine>>
274
368
  /** Execute a command directly in the VM (not in a container). */
275
369
  exec(command: Array<string>, options?: ExecOptions | undefined | null): Promise<ExecResult>
276
370
  /**