@lizard-build/sdk 0.1.35 → 0.1.36

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
@@ -80,40 +80,74 @@ print(f"Live at https://{sandbox.get_host(3000)}")
80
80
  sandbox.kill()
81
81
  ```
82
82
 
83
- ## Pause and Resume
83
+ ## Persisting Work Across Sandboxes
84
84
 
85
- Sandboxes can be paused mid-execution and resumed exactly where they left off — including installed packages, in-memory state, and running processes. Pausing freezes the microVM's vCPUs; memory and processes stay in the host's RAM and are not written to disk, so a paused sandbox does not survive a host failure. This makes Lizard sandboxes well-suited for long-running AI agent workflows where you want to stop and continue across separate invocations.
85
+ Sandboxes are **ephemeral**: killing one, or letting it hit its timeout, discards everything written inside it. State that has to outlive a sandbox goes on a **volume** — a separate disk mounted at `/data` that a later sandbox re-attaches.
86
86
 
87
- Pausing does not stop the sandbox timeout — a paused sandbox is still deleted at its original expiry (default 5 minutes). Pass `timeoutMs: 0` (`timeout_ms=0` in Python) at create time to opt out of expiry.
87
+ A volume is node-local, so it fixes the region too. You don't thread a region through both calls: the sandbox is placed wherever its volume already lives.
88
88
 
89
89
  ```ts
90
- // Boot and set up the environment once (reusing the `lizard` client from above)
91
- const sandbox = await lizard.create('code-interpreter-v1')
92
- await sandbox.process.exec('pip install numpy pandas scikit-learn')
93
- const id = sandbox.sandboxId
94
- await sandbox.pause()
95
-
96
- // Later — resume instantly (no reinstall needed)
97
- const resumed = await lizard.connect(id)
98
- const result = await resumed.process.exec('python -c "import sklearn; print(sklearn.__version__)"')
99
- console.log(result.stdout)
100
- await resumed.kill()
90
+ const vol = await lizard.volumes.getOrCreate('agent-scratch', { sizeGb: 10 })
91
+
92
+ const first = await lizard.create('codex', { volumeName: 'agent-scratch' })
93
+ await first.process.exec('pip install numpy pandas && echo "notes" > /data/notes.txt')
94
+ await first.kill() // sandbox gone, /data survives
95
+
96
+ const second = await lizard.create('codex', { volumeName: 'agent-scratch' })
97
+ console.log(await second.fs.read('/data/notes.txt')) // "notes"
101
98
  ```
102
99
 
103
100
  ```python
104
- # reusing the `lizard` client from above
105
- sandbox = lizard.create("code-interpreter-v1")
106
- sandbox.process.exec_("pip install numpy pandas scikit-learn")
107
- sandbox_id = sandbox.sandbox_id
108
- sandbox.pause()
109
-
110
- # Resume later — environment is exactly as left
111
- resumed = lizard.connect(sandbox_id)
112
- result = resumed.process.exec_("python -c 'import sklearn; print(sklearn.__version__)'")
113
- print(result.stdout)
114
- resumed.kill()
101
+ vol = lizard.volumes.get_or_create("agent-scratch", size_gb=10)
102
+
103
+ first = lizard.create("codex", volume_name="agent-scratch")
104
+ first.process.exec_('echo "notes" > /data/notes.txt')
105
+ first.kill() # sandbox gone, /data survives
106
+
107
+ second = lizard.create("codex", volume_name="agent-scratch")
108
+ print(second.fs.read("/data/notes.txt")) # "notes"
109
+ ```
110
+
111
+ Note that only `/data` survives — installed packages and in-memory state do not. Bake tooling into a template instead of reinstalling it per sandbox.
112
+
113
+ > `pause()` / `resume()` exist on the client but are **not implemented** for the current runtime and always fail with HTTP 501. Use a volume.
114
+
115
+ ## Giving Each of Your Users Their Own Workspace
116
+
117
+ If you are building on top of Lizard and your users each need isolated resources, give each one a workspace and an API key scoped to it. They get isolation from each other; you keep one bill.
118
+
119
+ ```ts
120
+ const lizard = new Lizard({ apiKey: process.env.LIZARD_API_KEY })
121
+
122
+ const ws = await lizard.workspaces.create({ name: `user-${userId}` })
123
+ const prj = await lizard.projects.create({ workspaceId: ws.id, name: 'default' })
124
+ const key = await lizard.apiKeys.create({ name: `user-${userId}`, workspaces: [ws.id] })
125
+
126
+ // key.key is returned exactly once. Store it now.
127
+ await db.users.update(userId, { lizardKey: key.key })
128
+ ```
129
+
130
+ That key reaches nothing outside `ws`, and it cannot mint a broader one — the server rejects that with an exact subset check. So it is safe to hand to the user, and safe to put **inside a sandbox** so agent code can use the CLI as that user:
131
+
132
+ ```ts
133
+ const sandbox = await Sandbox.create('codex', {
134
+ projectId: prj.id,
135
+ lizardToken: key.key, // scoped — bounded if it leaks
136
+ })
137
+ await sandbox.process.exec('lizard volume list') // sees only this workspace
115
138
  ```
116
139
 
140
+ ```python
141
+ ws = lizard.workspaces.create(name=f"user-{user_id}")
142
+ prj = lizard.projects.create(workspace_id=ws.id, name="default")
143
+ key = lizard.api_keys.create(name=f"user-{user_id}", workspaces=[ws.id])
144
+
145
+ sandbox = Sandbox.create("codex", project_id=prj.id, lizard_token=key.key)
146
+ sandbox.process.exec_("lizard volume list")
147
+ ```
148
+
149
+ A key with **no** scope has full access to everything the creating account can reach — pass `workspaces` or `projects` unless you mean that.
150
+
117
151
  ## API
118
152
 
119
153
  ### `new Lizard({ project, apiKey?, apiUrl?, timeoutMs? })`
@@ -137,7 +171,7 @@ const sandbox = await Sandbox.create('code-interpreter-v1', { project: 'my-proje
137
171
 
138
172
  ### `Sandbox.connect(sandboxId, opts?)`
139
173
 
140
- Connect to an existing sandbox by ID. If the sandbox is paused, it is automatically resumed.
174
+ Connect to an existing sandbox by ID. Throws `NotFoundError` if it has been killed or has expired.
141
175
 
142
176
  ### `Sandbox.list(opts?)`
143
177
 
@@ -145,6 +179,38 @@ List all running sandboxes for the authenticated account.
145
179
 
146
180
  ---
147
181
 
182
+ ### Account and provisioning
183
+
184
+ | Namespace | Methods |
185
+ |---|---|
186
+ | `lizard.workspaces` | `list()`, `create({ name })`, `delete(id, { force? })`, `find(nameOrSlugOrId)` |
187
+ | `lizard.apiKeys` | `list()`, `create({ name, workspaces?, projects? })`, `delete(id)` |
188
+ | `lizard.projects` | `list({ workspaceId? })`, `get(id)`, `create({ workspaceId, name })`, `update(id, { name })`, `delete(id)` |
189
+ | `lizard.regions` | `list()` |
190
+ | `lizard.billing` | `balance()`, `transactions({ limit?, cursor?, includeUsage? })`, `summary()`, `live()` |
191
+ | `lizard.whoami()` | The account behind the credential — works for scoped keys too |
192
+ | `lizard.platform` | The raw HTTP client, for endpoints not wrapped yet |
193
+
194
+ `workspaces.delete()` is empty-only by default; the server refuses while any project, sandbox or volume remains. `{ force: true }` deletes the workspace and everything in it, irreversibly.
195
+
196
+ In Python the namespaces are `lizard.workspaces`, `lizard.api_keys`, `lizard.regions`, `lizard.billing`, and arguments are snake_case (`workspace_id`, `include_usage`).
197
+
198
+ ### Volumes
199
+
200
+ | Method | Description |
201
+ |---|---|
202
+ | `lizard.volumes.getOrCreate(name, { sizeGb?, region? })` | The volume with this name, created if absent |
203
+ | `lizard.volumes.create(name, { sizeGb?, region? })` | Create; throws `ConflictError` if the name is taken |
204
+ | `lizard.volumes.get(nameOrId)` | Look one up |
205
+ | `lizard.volumes.list()` | Every volume in the client's project |
206
+ | `lizard.volumes.delete(nameOrId)` | Delete |
207
+
208
+ A volume's **name** is its key inside a project, so an agent can reconstruct it between runs without storing an id. The static `Volume.*` forms take an explicit `projectId` as their first argument.
209
+
210
+ `region` places the volume (see `lizard.regions.list()` for valid ids) and, because a volume is node-local, also fixes the region of any sandbox that mounts it. You normally set it here or nowhere.
211
+
212
+ ---
213
+
148
214
  ### `sandbox.fs`
149
215
 
150
216
  Read and write files inside the microVM filesystem.
@@ -179,7 +245,7 @@ const url = sandbox.getHost(3000)
179
245
 
180
246
  ### `sandbox.pause()` / `sandbox.resume()`
181
247
 
182
- Freeze and unfreeze the microVM's vCPUs. Memory and running processes are kept in the host's RAM, not written to disk. The timeout keeps running while paused.
248
+ **Not implemented** for the current runtime — both always fail with HTTP 501. Use a [volume](#persisting-work-across-sandboxes) to carry work across sandboxes.
183
249
 
184
250
  ### `sandbox.kill()`
185
251