@lizard-build/cli 4.0.44 → 4.0.46
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/AGENTS.md +3 -0
- package/dist/commands/add.js +2 -7
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/agents.js +21 -2
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/domain.js +32 -2
- package/dist/commands/domain.js.map +1 -1
- package/dist/commands/init.js +15 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/metrics.js +11 -2
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/s3.d.ts +15 -0
- package/dist/commands/s3.js +59 -0
- package/dist/commands/s3.js.map +1 -1
- package/dist/commands/sandbox.js +13 -5
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/skill.js +89 -16
- package/dist/commands/skill.js.map +1 -1
- package/dist/commands/up.js +23 -6
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/upgrade.js +17 -3
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/volume.js +38 -4
- package/dist/commands/volume.js.map +1 -1
- package/dist/commands/workspace.js +4 -7
- package/dist/commands/workspace.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/api.js +21 -6
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +10 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/default-project.js +7 -2
- package/dist/lib/default-project.js.map +1 -1
- package/dist/lib/skills-data.generated.d.ts +2 -0
- package/dist/lib/skills-data.generated.js +6 -1
- package/dist/lib/skills-data.generated.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -1
- package/scripts/gen-skills.mjs +24 -5
- package/skill-data/core/SKILL.md +122 -377
- package/skill-data/core/references/deploys.md +201 -0
- package/skill-data/core/references/sandboxes.md +169 -0
- package/skill-data/core/references/sdk.md +227 -0
- package/src/commands/add.ts +2 -6
- package/src/commands/agents.ts +20 -4
- package/src/commands/domain.ts +36 -2
- package/src/commands/init.ts +18 -0
- package/src/commands/metrics.ts +11 -2
- package/src/commands/s3.ts +65 -0
- package/src/commands/sandbox.ts +15 -5
- package/src/commands/skill.ts +95 -21
- package/src/commands/up.ts +20 -6
- package/src/commands/upgrade.ts +14 -3
- package/src/commands/volume.ts +35 -4
- package/src/commands/workspace.ts +4 -9
- package/src/index.ts +4 -1
- package/src/lib/api.ts +21 -6
- package/src/lib/config.ts +11 -1
- package/src/lib/default-project.ts +5 -2
- package/src/lib/updater.ts +1 -1
- package/test/unit/default-project.test.ts +8 -2
- package/test/unit/s3-presign.test.ts +32 -0
- package/test/unit/sandbox-transport.test.ts +5 -1
- package/test/unit/sandbox.test.ts +1 -0
- package/test/unit/skills.test.ts +31 -12
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Deploys in depth
|
|
2
|
+
|
|
3
|
+
The details behind the deploy, database and secrets sections of the core guide: how builds are chosen, git and upload deploys, service fields, worker mode, secret precedence and references, addon variables, and multi-step recipes.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Before you deploy](#before-you-deploy)
|
|
8
|
+
- [Platform builder](#platform-builder)
|
|
9
|
+
- [Deploying](#deploying)
|
|
10
|
+
- [Worker mode](#worker-mode)
|
|
11
|
+
- [Secrets](#secrets)
|
|
12
|
+
- [Managed addons](#managed-addons)
|
|
13
|
+
- [Composition patterns](#composition-patterns)
|
|
14
|
+
- [Logs, metrics and lizard run](#logs-metrics-and-lizard-run)
|
|
15
|
+
|
|
16
|
+
## Before you deploy
|
|
17
|
+
|
|
18
|
+
This skill documents platform behavior (build pipeline, env precedence, what knobs the API exposes). It does not describe the user's repo.
|
|
19
|
+
|
|
20
|
+
Before writing commands for a specific project:
|
|
21
|
+
|
|
22
|
+
1. Read the user's `package.json`, `Dockerfile`, `requirements.txt`, framework config — confirm what already exists before adding flags.
|
|
23
|
+
2. Don't assume scripts/conventions that aren't visible. On the lizardpack auto-detect path, `Procfile` (`web:` line, Python/Ruby) and `package.json scripts.start` (Node) ARE picked up as the start command; on the synthesized-Dockerfile path (`buildCommand`/`startCommand` set) neither is read.
|
|
24
|
+
3. Ports: `lizard up` fixes a new service's port from `--port`, else the folder's Dockerfile `EXPOSE`, else 3000, and the app must listen there (`$PORT`). lizardpack's Python commands bind 8000 and PHP 80: pass `--port 8000` / `--port 80`, or a start command that reads `$PORT`. Git services take the port from the image's `EXPOSE` unless `containerPort` or a `PORT` variable is set. Nothing reads the port from logs.
|
|
25
|
+
4. When in doubt, ask the user or run `lizard <cmd> --help --json`.
|
|
26
|
+
|
|
27
|
+
## Platform builder
|
|
28
|
+
|
|
29
|
+
Builds run on the platform's build nodes (no local Docker needed). When a build fails, read logs with `lizard logs --build`.
|
|
30
|
+
|
|
31
|
+
### Build decision order
|
|
32
|
+
|
|
33
|
+
1. Repo Dockerfile (verbatim) — if `dockerfilePath` (relative to `rootDirectory`) is set, the platform builds that Dockerfile unchanged and ignores `buildCommand`/`startCommand` for the image. A set `startCommand` still replaces the image's ENTRYPOINT and CMD at run time (`sh -c`, so the image needs `sh`).
|
|
34
|
+
2. Synthesized Dockerfile — if `buildCommand` and/or `startCommand` are set (on the service or via `lizard up`), the platform generates a Go, Python or Node Dockerfile, chosen by the command's first word (anything else gets Node). It replaces the repo Dockerfile and lizardpack, so on other stacks fix the start through a Dockerfile, not `startCommand`.
|
|
35
|
+
3. Root `Dockerfile` — used as is for uploads. For git, used unless it has no install or build `RUN` (`npm|yarn|pnpm|bun|pip|bundle|cargo|go build|mvn|gradle`) and copies a build folder (`COPY dist ...`, `build`, `out`, `.next`, `public`): then lizardpack replaces it and the build log says so.
|
|
36
|
+
4. lizardpack auto-detect — Hugo, Go, Node, Python, Rust, Ruby, PHP, Java, static, first match wins. When nothing matches, the image runs `bash start.sh`, which builds but fails to start.
|
|
37
|
+
|
|
38
|
+
### What triggers a rebuild
|
|
39
|
+
|
|
40
|
+
- `git push` to the tracked branch → auto-rebuild, when the Lizard GitHub App covers the repo (`lizard git status --json`).
|
|
41
|
+
- `lizard redeploy` / `lizard up` → explicit rebuild.
|
|
42
|
+
- Setting a `VITE_*` or `NEXT_PUBLIC_*` variable → the platform redeploys the running services that receive it, so the build bakes it in. Do NOT chain a `lizard redeploy`. See [Applying an env change](#applying-an-env-change) for which builds receive them.
|
|
43
|
+
- `service set` for build-affecting fields (`repoUrl`, `branch`, `sourceType`, `buildCommand`, `dockerfilePath`, `rootDirectory`) → auto-rebuilds a service whose status is `running`. Do NOT chain a `lizard redeploy` after it — that queues a second build. A service that never came up, crashed or stopped doesn't rebuild: run `lizard redeploy`.
|
|
44
|
+
- `service set` for runtime-only fields (`startCommand`, `preDeployCommand`, `containerPort`, `watchPatterns`) → no auto-rebuild. Follow with `lizard redeploy` to apply.
|
|
45
|
+
- All other env vars / secrets → applied without a rebuild; the service restarts to pick them up.
|
|
46
|
+
|
|
47
|
+
## Deploying
|
|
48
|
+
|
|
49
|
+
First question for a new service: upload vs git repo. **Always check for a git remote before reaching for `up`** — the CLI never auto-detects one, so you must do it yourself:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
git remote get-url origin
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **Exit 0 with a GitHub URL → use the git-source path below**, not `up`. Parse `owner/repo` from the URL and run `lizard add -r owner/repo -n <svc>`. This is strongly preferred: pushes redeploy (with the Lizard GitHub App), and there's no re-upload on every change.
|
|
56
|
+
- **No remote / non-GitHub / not a git repo → use tarball upload (`lizard up`).** This is the fallback for quick local iteration or no-remote situations, not the default.
|
|
57
|
+
|
|
58
|
+
`lizard up` always works regardless of git state, which makes it the path of least resistance — resist it when a GitHub remote exists. Only fall back to upload when the remote check actually fails. (For a private repo whose remote exists but isn't yet accessible to Lizard, run `lizard git connect` to install the GitHub App, then use the git-source path — don't silently downgrade to a tarball.)
|
|
59
|
+
|
|
60
|
+
### Git-source deploy (preferred when there's a remote)
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
# One-shot for a new service from GitHub (tracks main):
|
|
64
|
+
lizard add -r owner/repo -n <svc> --json
|
|
65
|
+
|
|
66
|
+
# Another branch:
|
|
67
|
+
lizard add -r owner/repo -n <svc> --no-deploy --json
|
|
68
|
+
lizard service set <svc> --set branch=<branch> --json
|
|
69
|
+
lizard redeploy --service <svc> --wait --json
|
|
70
|
+
|
|
71
|
+
# Existing service: switch source to git (rebuilds by itself when the service is running;
|
|
72
|
+
# otherwise follow with `lizard redeploy --service <svc>`):
|
|
73
|
+
lizard service set <svc> \
|
|
74
|
+
--set sourceType=github \
|
|
75
|
+
--set repoUrl=https://github.com/owner/repo \
|
|
76
|
+
--set branch=main \
|
|
77
|
+
--json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`add -r` with a name that already exists overwrites that service (branch back to `main`, commands cleared) and redeploys it: pick a new `-n`. A private repo Lizard can't read answers 404 `Repository not found`: `lizard git connect --json` gives an `installUrl` for the user. When `repoUrl` is set, pushes to the branch redeploy through the Lizard GitHub App. With a `rootDirectory` or watch patterns, only pushes touching either one redeploy; `autoDeploy=false` turns pushes off.
|
|
81
|
+
|
|
82
|
+
Useful `service set` fields (the full list is in the plain `lizard service set --help`; the JSON schema leaves it out):
|
|
83
|
+
|
|
84
|
+
- `sourceType` = `github | upload`
|
|
85
|
+
- `repoUrl`, `branch`, `rootDirectory`
|
|
86
|
+
- `dockerfilePath` — use a specific repo Dockerfile, bypasses lizardpack auto-detect
|
|
87
|
+
- `buildCommand`
|
|
88
|
+
- `startCommand`, `preDeployCommand` — `preDeployCommand` runs before `startCommand` at the start of every container (each replica, each restart), and only when `startCommand` is also set (otherwise it is skipped). If it fails, the container never starts. Make it safe to run many times and at the same time.
|
|
89
|
+
- `watchPatterns` — string array, comma-separated or JSON
|
|
90
|
+
- `containerPort` — TCP port the app listens on (a new `lizard up` service gets 3000 unless `--port` or an `EXPOSE` says otherwise). Set to `0` for [worker mode](#worker-mode).
|
|
91
|
+
- `name` — rename a service (lowercase a-z, digits, hyphens; 1–40 chars). Goes through `config:apply`; the legacy `PATCH /api/apps/:id` returns 410. Refs to the old name (`${{old.KEY}}`) stop resolving: update them.
|
|
92
|
+
|
|
93
|
+
Field names are flat and match the wire schema 1:1 (and `service show` output). No `build.*` / `deploy.*` / `source.*` grouping exists in the API, DB, or node-agent.
|
|
94
|
+
|
|
95
|
+
`service set` uses optimistic concurrency via `configRevision`. On 409, re-read with `lizard service show`, reconcile, retry; `--force` overrides.
|
|
96
|
+
|
|
97
|
+
### Tarball upload (no git remote, or quick local iteration)
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
lizard up --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- Uploads cwd as a tarball (at most 150 MB compressed) and forces `sourceType=upload`. In a git repo it sends tracked files plus untracked files that `.gitignore` doesn't exclude; outside git it skips `node_modules`, `.git`, `dist`, `build`, `.next`, virtualenvs and caches, so a prebuilt `dist/` is not sent. There is no `.lizardignore`, and `.dockerignore` is not read.
|
|
104
|
+
- Streams build logs; ends with `{ event: "deployed", status: "running", url }`. A failed build ends with `{ event: "error", message }` and exit 1; no `deployed` event means the service isn't running yet (see the core guide).
|
|
105
|
+
- With no linked service, `up` deploys into the service named like `package.json` `name` (else the folder), and switches it to an upload even if it was git-backed. `--service <name>` deploys to that service, creating it when none has the name; a folder linked to an addon (after `lizard add postgres`) is treated as unlinked.
|
|
106
|
+
- Flags: `--project`, `--service`, `--region`, `--build-command`, `--start-command`, `--pre-deploy-command`, `--port`, `--detach`, `--ci`.
|
|
107
|
+
- If cwd isn't linked and no `--project` is given, it links the folder to the default project (see the core guide) and deploys there as a new service. `--project <name>` uses that project, or creates it if none matches. It never creates a project from the folder name on its own.
|
|
108
|
+
- `lizard up` always switches the service to `sourceType=upload`. Do not use it to update a git-backed service — use `lizard redeploy` or push to the remote.
|
|
109
|
+
|
|
110
|
+
## Worker mode
|
|
111
|
+
|
|
112
|
+
For services that don't expose an HTTP listener (background workers, reconcilers, queue consumers, cron-style polling loops), set `containerPort=0`. The platform then:
|
|
113
|
+
|
|
114
|
+
- Skips `PORT` env injection (the worker doesn't bind anywhere).
|
|
115
|
+
- Runs no readiness check, and each deploy stops the old copy before starting the new one (two copies never overlap).
|
|
116
|
+
- Skips `EXPOSE` in the synthesized Dockerfile.
|
|
117
|
+
- A generated `.onlizard.com` domain and route may still exist; they answer with an error.
|
|
118
|
+
|
|
119
|
+
Set it one of three ways:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
lizard up --port 0 # new upload-source worker
|
|
123
|
+
lizard port 0 [--service <svc>] # flip existing service to worker mode, then `lizard redeploy`
|
|
124
|
+
lizard service set <svc> --set containerPort=0 # same, via the config:apply path
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`lizard port` with no argument prints the current port (or `worker mode` when 0). Worker mode is a hard switch — re-deploys are needed for the port change to take effect.
|
|
128
|
+
|
|
129
|
+
Don't use worker mode for a regular HTTP service that just happens to be slow to start — worker mode hides "the listener never came up" bugs because there's nothing to check.
|
|
130
|
+
|
|
131
|
+
## Secrets
|
|
132
|
+
|
|
133
|
+
Two scopes exist. No workspace-level globals.
|
|
134
|
+
|
|
135
|
+
- Project ("global"): `lizard secrets set KEY=v [K2=v2 …] --global` → project scope (wire: `secrets.shared`)
|
|
136
|
+
- Service (default): `lizard secrets set KEY=v [K2=v2 …] [--service <svc>]` → service scope (wire: `secrets.services[<svc>]`)
|
|
137
|
+
|
|
138
|
+
`set` is variadic. Companion subcommands: `lizard secrets list|delete K1 K2|import` (import reads dotenv from stdin). When the linked service in cwd is set, plain `lizard secrets set KEY=v` writes to that service. Pass `--global` to escape to project scope.
|
|
139
|
+
|
|
140
|
+
### Precedence (last writer wins)
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
addon-issued env < project secrets < project env < app env < app secrets < platform vars
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
App secrets override project secrets. Platform vars come last and cannot be shadowed: `PORT` (absent in worker mode), `LIZARD_PROJECT_ID`, `LIZARD_SERVICE_NAME` (an internal `app-…` handle, not the dashboard name), `LIZARD_PUBLIC_DOMAIN` (absent until the service has a domain; a comma list once a custom domain is attached) and `LIZARD_PRIVATE_DOMAIN`. For service-to-service URLs use `${{<svc>.LIZARD_PRIVATE_DOMAIN}}`, not the service name. A `PORT` you set yourself doesn't override the injected one, but it becomes `containerPort` at the next deploy when the port wasn't set explicitly. Addon-issued env is every running Postgres and Redis addon's public `DATABASE_URL` / `REDIS_URL`, injected into every service at its next deploy or secret change; with two addons of a type, either may win.
|
|
147
|
+
|
|
148
|
+
### Secret scoping
|
|
149
|
+
|
|
150
|
+
Default to service-scope. `--global` puts the value into `process.env` of every service in the project — including ones that don't need it.
|
|
151
|
+
|
|
152
|
+
Rules:
|
|
153
|
+
|
|
154
|
+
- Check first: `lizard secrets list` (+ `--global`) before set/update — avoid creating a duplicate or shadowing an existing key (service scope wins over global; see [Precedence](#precedence-last-writer-wins)).
|
|
155
|
+
- Default — service-scope: `lizard secrets set KEY=v --service <svc>` per consumer. For addon DSNs, prefer a private reference when the consumer can reach it: `lizard secrets set DATABASE_URL='${{postgres.DATABASE_PRIVATE_URL}}' --service <svc>` (no separate `env` command — refs are interpolated wherever they appear). Refs are read again on every deploy, redeploy and secret change, not on `lizard restart`; there is no automatic credential rotation. A ref to another service sees only its stored values and platform vars, not the `DATABASE_URL` it got automatically.
|
|
156
|
+
- `secrets import` keeps quotes and `export ` prefixes as part of the value: strip them first.
|
|
157
|
+
- `--global` only for non-secrets and provably-public values: `LOG_LEVEL`, `NODE_ENV`, feature flags, frontend `SENTRY_DSN`. If unsure whether a value is a secret, treat it as one. A compromised service reads its own env; broader scope = more credentials exposed for no reason.
|
|
158
|
+
|
|
159
|
+
### Applying an env change
|
|
160
|
+
|
|
161
|
+
- Runtime vars/secrets apply without rebuilding the image. The application process restarts to read its new environment; do not promise uninterrupted requests.
|
|
162
|
+
- `VITE_*` / `NEXT_PUBLIC_*` (build-time baked) → setting one redeploys the running services that receive it; a plain restart won't pick them up. Only some builds bake them in: lizardpack or a reused repo Dockerfile on a GitHub service, or a Node-style build generated from `buildCommand`/`startCommand`. A `dockerfilePath` build, or a `lizard up` upload without those overrides, gets no Lizard variables at build time, and no build gets build args except `COMMIT_SHA`. For public values there, commit them in `.env.production`, set a build command, or write them into the Dockerfile.
|
|
163
|
+
- Verify a non-secret value or the application's behavior. Do not print the full environment: it can expose credentials.
|
|
164
|
+
|
|
165
|
+
## Managed addons
|
|
166
|
+
|
|
167
|
+
Provision with `lizard add <type>`. Each addon exposes a fixed env-var set; reference by name from a consumer service via `${{<addon-name>.KEY}}`. The first addon of a given type gets the bare type as its name (so use `postgres` in the reference for the first Managed Postgres instance); subsequent ones get `{type}-{adjective}-{noun}` like `postgres-autumn-bear`. There's no type-alias fallback — a ref must use the addon's actual name. Refs match the addon's current name, so renaming it breaks existing refs: update them after a rename. Every running Postgres and Redis addon's `DATABASE_URL` / `REDIS_URL` also reaches every service in the project at the lowest precedence; a value set on the consumer overrides it.
|
|
168
|
+
|
|
169
|
+
- `postgres` — two connection URLs: prefer `DATABASE_PRIVATE_URL` when available and reachable within the same project and region (no ingress/egress charges); use `DATABASE_URL` for public access, such as local clients or external CI (network charges may apply). Private URLs resolve only in the addon's region, and `lizard add` defaults to `us-east-1`. Also exposes `PGHOST`, `PGPORT` (the public endpoint), `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `POSTGRES_USER`, `POSTGRES_DB`, `POSTGRES_PASSWORD`.
|
|
170
|
+
- `redis` — `REDIS_URL`; services in the same project should use `REDIS_PRIVATE_URL`. No eviction policy and no append-only file are configured: if it runs out of memory, the instance restarts and recent writes can be lost. Keep data you cannot lose in Postgres.
|
|
171
|
+
- `s3` — `S3_ENDPOINT`, `S3_DEFAULT_BUCKET`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`. Auto-creates a private bucket named `default`; each S3 addon stores up to 16 GiB. Make a bucket public from the dashboard's S3 browser; objects in a public bucket are served without auth two ways: the gateway URL the dashboard shows, `https://s3-<region>.onlizard.com/<addonId>/<bucket>/<key>`, or the platform proxy `<dashboard-host>/api/s3/<addonId>/public/<bucket>/<key>` (the host `lizard open` launches; long-lived immutable cache headers). For AWS SDK use, set `forcePathStyle: true`. ACL flips aren't on the CLI yet, so point users at the dashboard. To push a local file into a bucket directly (no AWS SDK needed), use `lizard s3 upload <file> [--addon <name>] [--bucket <name>] [--key <key>] [--content-type <type>]` — auto-resolves the project's only S3 addon and bucket `default` if omitted; prints `{key, etag, size, url}` (`url` is `null` unless the bucket is public). `lizard s3 list|ls [--bucket <name>] [--prefix <p>]` lists objects in a bucket. `lizard s3 url <key> [--expires <seconds>]` prints a presigned link (default 1 hour, at most 7 days) that downloads a private object without credentials. The CLI has no ACL command: make a bucket public in the dashboard, or with an S3 `PutBucketAcl` (`public-read`) call using the addon's credentials.
|
|
172
|
+
|
|
173
|
+
## Composition patterns
|
|
174
|
+
|
|
175
|
+
Multi-step requests follow natural chains. Return one unified response, don't farm out steps:
|
|
176
|
+
|
|
177
|
+
- First deploy from git — pick action via the setup decision flow in the core guide → `lizard add -r owner/repo` → stream build → surface URL.
|
|
178
|
+
- First deploy from local code — Setup decision flow → `lizard up` → surface URL.
|
|
179
|
+
- Add a managed database to an existing service — `lizard add postgres` → bind the consumer's `DATABASE_URL` to the private reference as described above (the service restarts with it; no redeploy needed).
|
|
180
|
+
- Keep files across deploys — `lizard volume create data --service <svc> --mount-path /data` (one volume per service; the service restarts with it) → point the app's file paths there.
|
|
181
|
+
- Add object storage to a service — `lizard add s3` → reference `${{s3.S3_ENDPOINT}}`, `${{s3.S3_DEFAULT_BUCKET}}`, `${{s3.S3_ACCESS_KEY_ID}}`, `${{s3.S3_SECRET_ACCESS_KEY}}`, `${{s3.S3_REGION}}` from the consumer service. The `default` bucket is private; to serve files without auth, have the user make it public in the dashboard's S3 browser, then objects are served at `<dashboard-host>/api/s3/<addonId>/public/default/<key>`. See [Managed addons](#managed-addons).
|
|
182
|
+
- Wire a fresh git source on an existing service — `service set --set sourceType=github --set repoUrl=… --set branch=…` (rebuilds a running service by itself; otherwise `redeploy`).
|
|
183
|
+
- Fix a failed build — `logs --build --service <svc>` → diagnose → fix project (user's repo) OR adjust `buildCommand` / `startCommand` via `service set` → `redeploy --service <svc> --wait` → `logs` to verify.
|
|
184
|
+
- Fix a crash-looping service — `logs --restart latest --service <svc>` → fix the cause (secret, port, code) → `redeploy --service <svc> --wait`: a service whose first deploy failed gets its URL only from a successful deploy.
|
|
185
|
+
- Add a custom domain — `domain <host> --service <svc>` (the hostname is a positional, there is no `add` subcommand) → give the user `dnsRecords` → `domain verify <host> --service <svc>` once DNS propagates (it checks only the TXT; nothing verifies on its own). `domain list --service <svc>` shows pending domains. If the host is already attached to another of the user's services, the attach 409s with a reclaim hint — re-run with `--force` to move it here.
|
|
186
|
+
|
|
187
|
+
## Logs, metrics and lizard run
|
|
188
|
+
|
|
189
|
+
`lizard metrics --all` ignores the directory's linked service for this command.
|
|
190
|
+
It uses the linked project unless `--project <name>` selects another project.
|
|
191
|
+
Without `--all`, metrics defaults to the linked service, or the project overview
|
|
192
|
+
when no service is linked. Do not combine `--all` with `--service`.
|
|
193
|
+
The project overview includes `Egress` (outbound bytes per second) and `Volumes`
|
|
194
|
+
(used / total bytes). These columns use the latest sample from the past hour;
|
|
195
|
+
`Sampled` refers to CPU and memory. Missing measurements show `—`.
|
|
196
|
+
Watch mode refreshes CPU and memory every 3 seconds and I/O history every 30 seconds.
|
|
197
|
+
JSON output includes the history in each service's `series` and `timestamps` fields.
|
|
198
|
+
|
|
199
|
+
`lizard run --service <app>` injects stored values only: `${{…}}` refs arrive as literal text, and platform vars (`PORT`, `LIZARD_*`) are not set. `--service <addon>` injects the addon's real connection values, so use it to run a local command against a database. To check a service's wiring, use `lizard logs --build --service <svc>` (it warns about every reference that came back empty), `lizard secrets list --service <svc> --json` (names) and `lizard secrets list --ref --service <svc> --json` (the references that resolve; `available: false` for an addon that isn't running); shell access into services is off.
|
|
200
|
+
|
|
201
|
+
For exact flags, `lizard <cmd> --help --json`. Other commands not shown above: `lizard git` (GitHub integration), `lizard config` (project configuration), `lizard workspace` (workspace info) — discover each with `lizard <cmd> --help --json`.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Sandboxes in depth
|
|
2
|
+
|
|
3
|
+
The details behind the Sandboxes section of the core guide. Read the part the task needs.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Templates](#templates)
|
|
8
|
+
- [Running commands](#running-commands)
|
|
9
|
+
- [Files](#files)
|
|
10
|
+
- [Ports](#ports)
|
|
11
|
+
- [Lifetime](#lifetime)
|
|
12
|
+
- [Size and pricing](#size-and-pricing)
|
|
13
|
+
- [Interactive shell](#interactive-shell)
|
|
14
|
+
- [Coding agents](#coding-agents)
|
|
15
|
+
- [Desktop (computer use)](#desktop-computer-use)
|
|
16
|
+
- [Volumes](#volumes)
|
|
17
|
+
- [Snapshots and pause/resume](#snapshots-and-pauseresume)
|
|
18
|
+
|
|
19
|
+
## Templates
|
|
20
|
+
|
|
21
|
+
| `-t` | What is inside |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `base` (default) | Python 3.11 on Alpine Linux, nothing else |
|
|
24
|
+
| `interpreter` | Python 3.11 with numpy, pandas, matplotlib, scipy, requests, httpx and IPython; Node 22 and `lizard` |
|
|
25
|
+
| `codex`, `claude`, `opencode`, `pi`, `prime` | One coding agent each (Codex, Claude Code, OpenCode, pi, Prime Agent) on Node 22, with git, python3, ripgrep, curl, jq and `lizard`: see [Coding agents](#coding-agents) |
|
|
26
|
+
| `desktop` | A graphical desktop with Chromium, plus Codex and `lizard`: see [Desktop](#desktop-computer-use) |
|
|
27
|
+
|
|
28
|
+
An unknown name fails and lists the region's `availableTemplates`. `--with-token` on `sandbox create` signs in the sandbox's own `lizard` with a new key limited to the sandbox's project, deleted with the sandbox (only useful on templates that ship `lizard`).
|
|
29
|
+
|
|
30
|
+
## Running commands
|
|
31
|
+
|
|
32
|
+
`lizard sandbox exec <id> -- <cmd> [args...]` runs the command in the sandbox, streams stdout and stderr, and exits with the command's exit code. Each argument stays one word: `exec <id> 'cd /workspace && npm test'` fails with `not found`, so put a shell line in `-- sh -c '…'`. Without a terminal it prints the raw output without the `$ <cmd>` header. Each call is a new process that starts in `/root`, so `cd` and exported variables don't carry over: chain steps in one call, e.g. `lizard sandbox exec <id> -- sh -c 'cd /workspace && npm test'`, and keep results in files.
|
|
33
|
+
|
|
34
|
+
A call gives up after 150 seconds (exit `4`, `EXEC_TIMEOUT`; the command may still be running) and keeps at most 512 KB of output. Run longer work in the background and poll a log:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
lizard sandbox exec <id> -- sh -c 'cd /workspace && nohup npm run build > build.log 2>&1 &'
|
|
38
|
+
lizard sandbox exec <id> -- tail -n 50 /workspace/build.log
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Ports 25, 465, 587 and 2525 are blocked for outbound traffic: sandboxes cannot send mail directly.
|
|
42
|
+
|
|
43
|
+
## Files
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
lizard sandbox files ls <id> [path] # default /
|
|
47
|
+
lizard sandbox files cat <id> <path>
|
|
48
|
+
lizard sandbox files put <id> <local> <remote>
|
|
49
|
+
lizard sandbox files get <id> <remote> <local>
|
|
50
|
+
lizard sandbox files rm <id> <path>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`ssh` starts in `/workspace` and volumes mount there; `exec` starts in `/root`, so use absolute paths. `files put` and `files get` keep binary files intact. `files get` and `files cat` stop at 15 MiB (HTTP 413); `files put` takes about 110 MB.
|
|
54
|
+
|
|
55
|
+
## Ports
|
|
56
|
+
|
|
57
|
+
`lizard sandbox expose <id> <port> --json` publishes a port inside the sandbox on an HTTPS URL, `https://<id>-<port>.sandbox.<region>.onlizard.com/?lizard_token=…`; `lizard sandbox unexpose <id> <port>` removes it. One token opens every port of that sandbox, so treat the URL like a password. The server can listen on `127.0.0.1` or `0.0.0.0`.
|
|
58
|
+
|
|
59
|
+
- A browser opening the URL trades the token for a cookie and is redirected, so the link just works.
|
|
60
|
+
- `curl` gets an empty `302`, and `curl -L` without a cookie jar gets `401`. From a script send the header instead: `curl -H "X-Lizard-Access-Token: <accessToken>" https://<hostname>/` (both come in the `--json` output), or keep cookies: `curl -sL -c jar -b jar '<url>'`.
|
|
61
|
+
- A `POST` with `?lizard_token=` passes straight through, so webhooks work.
|
|
62
|
+
- Port `49983` is the platform's and can't be exposed.
|
|
63
|
+
|
|
64
|
+
## Lifetime
|
|
65
|
+
|
|
66
|
+
`lizard sandbox create` and `lizard sandbox restore` send a five-minute lifetime (`300000` milliseconds) by default. Pass `--timeout 0` to create a sandbox without a deadline (the VM itself still stops after 596 hours), or pass an integer up to `2147483647` milliseconds. This is a lifetime, not an idle timer: commands do not reset it. `lizard sandbox timeout <id> <ms>` sets a new lifetime of at least `1000` milliseconds. Pausing a sandbox freezes its remaining lifetime; resuming continues from it (see [private snapshots and pause/resume](#snapshots-and-pauseresume)). The CLI has no sandbox fork command. Use volumes for persistent files.
|
|
67
|
+
|
|
68
|
+
## Size and pricing
|
|
69
|
+
|
|
70
|
+
`lizard sandbox create -s/--size small|medium|large` picks the machine; the default is `medium`. There are exactly three sizes:
|
|
71
|
+
|
|
72
|
+
| Size | vCPU | RAM | Disk | Price |
|
|
73
|
+
|---|---|---|---|---|
|
|
74
|
+
| `small` | 2 | 4 GiB | 5 GB | $0.0162/h |
|
|
75
|
+
| `medium` (default) | 4 | 8 GiB | 20 GB | $0.0324/h |
|
|
76
|
+
| `large` | 8 | 16 GiB | 50 GB | $0.0648/h |
|
|
77
|
+
|
|
78
|
+
Prices are per hour, billed per second while the sandbox runs, flat by size: measured CPU/RAM is not charged, and disk and egress are included. A paused sandbox costs nothing. Attached volumes bill separately. `--size` cannot be combined with `--snapshot`: a snapshot restores at the size it was captured at (see [private snapshots](#snapshots-and-pauseresume)).
|
|
79
|
+
|
|
80
|
+
Sandboxes run as Firecracker microVMs, each with its own kernel. A sandbox that attaches an existing pod-backed volume, or restores an older pod snapshot, runs as a Kubernetes pod instead; a Firecracker start that fails without a volume also falls back to a pod. The create JSON's `runtime` says which.
|
|
81
|
+
|
|
82
|
+
An account runs at most 5 sandboxes at once on a trial and 100 after its first paid invoice (paused ones don't count), with limits on starts per minute, hour and day. Enterprise accounts have a cap per workspace instead (50 by default). Over a limit the API answers 429 `SANDBOX_LIMIT_REACHED` or `SANDBOX_RATE_LIMITED` (with `retryAfterS`).
|
|
83
|
+
|
|
84
|
+
## Interactive shell
|
|
85
|
+
|
|
86
|
+
`lizard sandbox ssh <id>` (alias `shell`) opens an interactive shell in any running sandbox,
|
|
87
|
+
in `/workspace` as root. It uses your Lizard login, so there are no SSH keys to set up; a
|
|
88
|
+
scoped API key only reaches sandboxes in its scope. It needs a real terminal — from a script
|
|
89
|
+
or an agent, use `lizard sandbox exec <id> -- <cmd>` instead, which returns output and an
|
|
90
|
+
exit code.
|
|
91
|
+
|
|
92
|
+
## Coding agents
|
|
93
|
+
|
|
94
|
+
Run a coding agent in a sandbox made from its template (`-t codex`, `-t claude`, `-t opencode`, `-t pi`, `-t prime`) on the user's own credentials. Give the sandbox a real lifetime (`--timeout 3600000`): at five minutes it is deleted with the agent mid-task. Another template with Node installs the agent at the first prompt, which is slow; one without Node can't run agents.
|
|
95
|
+
|
|
96
|
+
- `lizard agents` — your account's credentials and which agents can run. Connect once (here or in the
|
|
97
|
+
dashboard: Account settings › Agents); every sandbox and every agent that accepts it uses it.
|
|
98
|
+
- `lizard agents login chatgpt` — ChatGPT subscription via device code.
|
|
99
|
+
`lizard agents add claude` (token from `claude setup-token`), `add anthropic|openai|openrouter|llmgateway|deepseek` for API keys.
|
|
100
|
+
- `lizard agents use <agent> --credential … --model … --effort …` — per-agent defaults.
|
|
101
|
+
- `lizard sandbox prompt <id> --agent codex|claude|pi|opencode|prime "task" --json` — one JSON event per
|
|
102
|
+
line: tool calls, then a `response` event whose `text` is the answer, then `finished` or `failed`.
|
|
103
|
+
`-c <conversation>` continues one; `--cwd` (default `/workspace`), `-m <model>`, `--effort
|
|
104
|
+
minimal|low|medium|high|xhigh`. `--no-wait` returns at once, and the CLI has no command to read the
|
|
105
|
+
answer later, so prefer waiting.
|
|
106
|
+
- `lizard sandbox conversations <id>`, `lizard sandbox interrupt <id> [-c <conversation>]`.
|
|
107
|
+
|
|
108
|
+
Codex and Claude Code use one credential at a time; pi, OpenCode and Prime use every connected one
|
|
109
|
+
they accept. A ChatGPT login reaches the sandbox only as a short-lived access token. API keys and the
|
|
110
|
+
Claude token go in as they are (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`), so any code in the
|
|
111
|
+
sandbox can read them. `lizard agents` needs a full sign-in: a key limited to projects gets 403
|
|
112
|
+
`ACCOUNT_SCOPE_REQUIRED`. A failed agent login is not always the user's credential: report the
|
|
113
|
+
error as it is.
|
|
114
|
+
|
|
115
|
+
Use `sandbox exec` to read a log file written by your process.
|
|
116
|
+
|
|
117
|
+
## Desktop (computer use)
|
|
118
|
+
|
|
119
|
+
The `desktop` template is a sandbox with a graphical desktop (Openbox + Chromium) streamed to a browser over WebRTC (H.264), falling back to noVNC where WebRTC cannot connect. The viewer's side panel has fullscreen with keyboard lock, send-keys, type-clipboard and a switch to VNC.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
lizard sandbox create -t desktop # only this template has a desktop
|
|
123
|
+
lizard sandbox desktop <id> --open # start it (idempotent) and open it in the browser
|
|
124
|
+
lizard sandbox desktop <id> --resolution 1920x1080 # 640-3840 x 480-2160
|
|
125
|
+
lizard sandbox desktop <id> --view-only # watch-only link
|
|
126
|
+
lizard sandbox desktop <id> --status # URLs/state without starting
|
|
127
|
+
lizard sandbox desktop <id> --stop # stop and unpublish
|
|
128
|
+
lizard sandbox vnc <id> # native VNC app: tunnels localhost:5900 to the desktop
|
|
129
|
+
lizard sandbox vnc <id> --open # macOS: opens Screen Sharing; --view-only, --port N
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The printed URLs embed secrets (the desktop's token and its password): the main URL grants control of the sandbox, so treat it like a password and don't paste it into logs, issues or chat; share `--view-only` for watching. On any other template the command fails with `DESKTOP_NOT_SUPPORTED`.
|
|
133
|
+
|
|
134
|
+
Agents drive the desktop from inside the sandbox once it is started (`DISPLAY=:1` is preset in the image):
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
lizard sandbox exec <id> -- xdotool mousemove 400 300 click 1
|
|
138
|
+
lizard sandbox exec <id> -- xdotool type 'hello'
|
|
139
|
+
lizard sandbox exec <id> -- lizard-desktop screenshot | grep -E '^[A-Za-z0-9+/=]+$' | base64 -d > screen.png
|
|
140
|
+
lizard sandbox exec <id> -- lizard-desktop screenshot --path # writes the PNG to a file in the sandbox, prints its path
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`lizard-desktop screenshot` prints the PNG as base64 wrapped at 76 columns; the `grep` keeps only those lines (in a terminal it drops the `$ <cmd>` header `sandbox exec` prints). Use `--path` to save the PNG, then download that path with `lizard sandbox files get <id> <remote-path> screen.png`. `files put` and `files get` preserve binary files.
|
|
144
|
+
|
|
145
|
+
## Volumes
|
|
146
|
+
|
|
147
|
+
Use `lizard volume create <name> --size <gb> --project <project>` for persistent sandbox storage. Names are unique within a project; use lowercase letters, digits and dashes (up to 64 characters). Pass the name to `lizard sandbox create --volume <name>`. New volumes default to 5 GB and allow 1–50 GB. The CLI reads the server's current size limits before creating a volume; a server config override may change the maximum. Existing larger volumes keep their size. Storage charges cover used bytes, not the whole allocation.
|
|
148
|
+
|
|
149
|
+
Persistent volumes for sandboxes. A volume's name is its key within the project; every command takes a name or ID.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
lizard volume create <name> [--size <gb>] [--region <code>] # default 5 GB; a sandbox with --volume starts in its region
|
|
153
|
+
lizard volume list --json # name, sizeGb, status, attachedTo
|
|
154
|
+
lizard volume resize <name> --size <gb> # grow in place
|
|
155
|
+
lizard volume rm <name> [-y] # refused while attached; delete the sandbox first
|
|
156
|
+
lizard sandbox create --volume <name> # attach at create time
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A volume attaches to one sandbox at a time (409 `Volume is already attached to another sandbox`); a paused sandbox keeps its volume. A workspace holds 200 GB of volumes by default (`volume_quota_exceeded`). For a deployed service's volume, see `lizard volume create <name> --service <svc>` in the core guide.
|
|
160
|
+
|
|
161
|
+
`volume resize` is online: an attached sandbox keeps running, and its `/workspace` shows the new size right away. A volume grown while detached gets the new size when a sandbox next mounts it. Firecracker volumes (the default) only grow; asking for less fails with `volume_shrink_unsupported`. A Kubernetes volume can also shrink if at least 10% of the new size stays free (`volume_too_full_to_shrink` otherwise). The size limit (`volume_too_large`) and the workspace's volume quota apply as at create. `The volume is not ready` (HTTP 409) right after create means retry in a few seconds.
|
|
162
|
+
|
|
163
|
+
## Snapshots and pause/resume
|
|
164
|
+
|
|
165
|
+
- `lizard sandbox snapshot <sandbox-id> --name my-app` saves files and running memory. You and the members of its project can list, restore and delete it; only you change its scope. By default it restores into any of your projects and workspaces; `--scope workspace` keeps it to the sandbox's workspace and `--scope project` to its project, and `sandbox snapshot-scope <id> <user|workspace|project>` changes that later. A Firecracker snapshot is ready as soon as the capture ends. A snapshot of a pod sandbox keeps **five** independent copies warm; `--warm 1..10` changes that count. It waits until ready; `--no-wait` returns the queued capture.
|
|
166
|
+
- `lizard sandbox restore <snapshot-id> [--project <project>] [--timeout <ms>]` starts a new sandbox from a warm copy (five-minute lifetime by default), in the snapshot's project unless `--project` names another. `--volume <volume>` mounts a volume at `/workspace` in the restored sandbox (Firecracker snapshots of a sandbox that had no volume attached; the volume must be in the snapshot's region). `sandbox create --snapshot <id> --project <project> [--volume <volume>]` also works. Restores stay in the snapshot's region. A Firecracker snapshot restores at its source's size and bills flat at that size; a pod snapshot runs on the machine it was captured on and bills by measured usage. `--size` with `--snapshot` is an error.
|
|
167
|
+
- `lizard sandbox snapshots --project <project>` lists a project's snapshots; `--mine` lists every snapshot you took. `sandbox snapshot-warm <id> <count>` changes capacity. `sandbox snapshot-rm <id>` deletes the snapshot and free copies, preserving already running sandboxes.
|
|
168
|
+
- `lizard sandbox pause <id>` snapshots and stops a Firecracker VM (a pod sandbox is checkpointed with CRIU), stops billing, and freezes its remaining lifetime. `sandbox resume <id>` restores the same sandbox ID, files, processes, memory, published ports, and remaining lifetime. Paused sandboxes keep no warm copies; resume includes a cold restore. Both support `--no-wait`.
|
|
169
|
+
- Disconnect clients and terminals and stop workspace writes before capturing or pausing. On a pod sandbox, active TCP connections and unsupported CRIU process state cause a clear failure. To reuse one snapshot across sandboxes that each keep their own data, snapshot a sandbox with no volume attached and pass `--volume` on each restore; a snapshot taken with a volume attached cannot take a different one.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Building with Lizard (Lizard SDK)
|
|
2
|
+
|
|
3
|
+
Use Lizard SDK when the user's own code creates and drives sandboxes or manages Lizard resources: an agent framework, a code-execution feature, a backend that gives each of its users a machine. For one-off work from a terminal, use the CLI instead.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Install and configure](#install-and-configure)
|
|
8
|
+
- [Quickstart](#quickstart)
|
|
9
|
+
- [Commands and output](#commands-and-output)
|
|
10
|
+
- [Files](#files)
|
|
11
|
+
- [Stateful code (CodeSandbox)](#stateful-code-codesandbox)
|
|
12
|
+
- [Ports](#ports)
|
|
13
|
+
- [Keep files (volumes)](#keep-files-volumes)
|
|
14
|
+
- [Lifecycle, pause, snapshots and fork](#lifecycle-pause-snapshots-and-fork)
|
|
15
|
+
- [Desktop (computer use)](#desktop-computer-use)
|
|
16
|
+
- [Sandboxes for your own users](#sandboxes-for-your-own-users)
|
|
17
|
+
- [Platform APIs](#platform-apis)
|
|
18
|
+
- [Billing from code](#billing-from-code)
|
|
19
|
+
- [Errors](#errors)
|
|
20
|
+
|
|
21
|
+
## Install and configure
|
|
22
|
+
|
|
23
|
+
| SDK | Install | Requires |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| TypeScript | `npm install @lizard-build/sdk` | Node.js 18.16+ |
|
|
26
|
+
| Python | `pip install lizard-sdk` | Python 3.10+ |
|
|
27
|
+
|
|
28
|
+
This guide matches SDK 0.1.59 or later.
|
|
29
|
+
|
|
30
|
+
The SDK reads `LIZARD_API_KEY` and, optionally, `LIZARD_API_URL` (default `https://lizard.build`); `apiKey` / `api_key` and `apiUrl` / `api_url` override them. It does not load `.env` files. Create a key with `lizard keys create <name> --project <project> --json`: a key limited to one project is safest when the code runs somewhere else. The key is shown once; pass it to the program as `LIZARD_API_KEY`. The `lizard` CLI's own sign-in is not read by the SDK. Static methods (`Sandbox.connect`, `Sandbox.list`, `Sandbox.restore`, `Sandbox.deleteSnapshot`, `CodeSandbox.create`) read `LIZARD_API_KEY` too, not the key given to `new Lizard`: on a server holding several keys pass `apiKey` / `api_key` to them, or use `lizard.connect(id)`.
|
|
31
|
+
|
|
32
|
+
Sandboxes and volumes need a project: `new Lizard({ project })` / `Lizard(project=...)` takes its ID, slug or unique name; use the exact ID when names are ambiguous. Platform APIs work without one.
|
|
33
|
+
|
|
34
|
+
Options for `create()` (TypeScript / Python):
|
|
35
|
+
|
|
36
|
+
| Option | Purpose |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `size` | `small` (2 vCPU / 4 GB, $0.0162/h), `medium` (4 vCPU / 8 GB, $0.0324/h, default), `large` (8 vCPU / 16 GB, $0.0648/h) |
|
|
39
|
+
| `timeoutMs` / `timeout_ms` | Lifetime; default 300,000 ms (5 minutes); `0` means no deadline (the VM still stops after 596 hours) |
|
|
40
|
+
| `envs` | Environment variables for every command in the sandbox (up to 256) |
|
|
41
|
+
| `metadata` | Your own string labels, returned by `getInfo()` / `get_info()` (up to 64) |
|
|
42
|
+
| `region` | Region ID; omit to use the platform default |
|
|
43
|
+
| `volumeName` / `volume_name`, `volumeId` / `volume_id` | Attach a persistent volume at `/workspace` |
|
|
44
|
+
| `snapshotId` / `snapshot_id` | Start from a saved snapshot |
|
|
45
|
+
| `lizardToken` / `lizard_token` | One of your own keys: the sandbox's `lizard` gets a new key limited to its project, deleted with it. Someone else's key is skipped without an error |
|
|
46
|
+
|
|
47
|
+
Templates: `base` (default), `interpreter` (Python with data libraries, plus Node 22), `codex`, `claude`, `opencode`, `pi`, `prime` (one coding agent each), `desktop`. The SDK and CLI give a sandbox 5 minutes by default; the raw API and the dashboard give none. Commands do not reset the deadline. Pricing is flat by size, billed per second while the sandbox runs; a sandbox restored from a snapshot runs at the source's size and is billed at that size; volumes bill separately.
|
|
48
|
+
|
|
49
|
+
An account runs at most 5 sandboxes at once on a trial and 100 after its first paid invoice, with limits on starts per minute, hour and day: over a limit, a `LizardError` with status 429 and `code` `SANDBOX_LIMIT_REACHED` or `SANDBOX_RATE_LIMITED`. More than 32 commands at once in one sandbox gives `EXEC_LIMIT_REACHED`.
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Lizard } from '@lizard-build/sdk'
|
|
55
|
+
|
|
56
|
+
const lizard = new Lizard({ project: 'my-project' })
|
|
57
|
+
const sandbox = await lizard.create('base', { timeoutMs: 300_000 })
|
|
58
|
+
try {
|
|
59
|
+
await sandbox.fs.write('/tmp/hello.txt', 'Hello from Lizard!')
|
|
60
|
+
const result = await sandbox.process.exec('cat /tmp/hello.txt')
|
|
61
|
+
if (result.exitCode !== 0) throw new Error(result.stderr)
|
|
62
|
+
console.log(result.stdout)
|
|
63
|
+
} finally {
|
|
64
|
+
await sandbox.kill()
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from lizard import Lizard
|
|
70
|
+
|
|
71
|
+
lizard = Lizard(project="my-project")
|
|
72
|
+
with lizard.create("base", timeout_ms=300_000) as sandbox:
|
|
73
|
+
sandbox.fs.write("/tmp/hello.txt", "Hello from Lizard!")
|
|
74
|
+
result = sandbox.process.exec_("cat /tmp/hello.txt")
|
|
75
|
+
if result.exit_code != 0:
|
|
76
|
+
raise RuntimeError(result.stderr)
|
|
77
|
+
print(result.stdout)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Always kill the sandbox when done: `try/finally` in TypeScript, the `with` block in Python.
|
|
81
|
+
|
|
82
|
+
## Commands and output
|
|
83
|
+
|
|
84
|
+
`process.exec(cmd)` / `process.exec_(cmd)` returns stdout, stderr and the exit code. A nonzero exit code is a result to check; request failures throw or raise.
|
|
85
|
+
|
|
86
|
+
- `onStdout` / `onStderr` (`on_stdout` / `on_stderr`) stream output while the call still waits for the end.
|
|
87
|
+
- `timeoutMs` / `timeout_ms` bounds one command: 1–600 seconds, **60 seconds by default**. Past it the SDK gives up with a `DOMException` named `TimeoutError` (TypeScript) or `httpx.ReadTimeout` (Python), not a `LizardError`. Pass it for anything slower, such as `pip install`; run longer jobs with `nohup … &` and poll a log.
|
|
88
|
+
- Output keeps 512 KiB per stream, then `truncated` is set.
|
|
89
|
+
- `envs`, `workdir` and `user` (e.g. `user: 'user'`, the template's unprivileged account) apply to that command only.
|
|
90
|
+
- `onPid` / `on_pid` gives the pid before any output; `process.list()` lists commands and `process.kill(pid, 'SIGKILL')` signals the whole process tree.
|
|
91
|
+
- An argv array runs the binary without a shell, so text from a model or a user cannot run commands.
|
|
92
|
+
|
|
93
|
+
## Files
|
|
94
|
+
|
|
95
|
+
`fs.write`, `fs.read`, `fs.readBytes` / `fs.read_bytes`, `fs.list`, `fs.stat`, `fs.makeDir` / `fs.make_dir`, `fs.move`, `fs.remove`. Use absolute paths. Writes take text or bytes; bytes are stored exactly, so read binary files back with `readBytes()` / `read_bytes()`. A missing path raises `NotFoundError`. `list()` and `stat()` entries carry `type`, `size`, `mode`, `permissions`, `owner`, `group` and `modTime` (`mod_time` in Python). Reads stop at 15 MB (HTTP 413). File watching is not available yet.
|
|
96
|
+
|
|
97
|
+
## Stateful code (CodeSandbox)
|
|
98
|
+
|
|
99
|
+
`CodeSandbox` boots the `interpreter` template and runs Python in a persistent Jupyter kernel, so variables, imports and functions survive between calls. Plain `process.exec` starts a new process each time.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { CodeSandbox } from '@lizard-build/sdk'
|
|
103
|
+
|
|
104
|
+
const sandbox = await CodeSandbox.create({ projectId })
|
|
105
|
+
try {
|
|
106
|
+
await sandbox.runCode('import pandas as pd\ndf = pd.DataFrame({"x": [1, 2, 3]})')
|
|
107
|
+
const run = await sandbox.runCode('int(df.x.sum())')
|
|
108
|
+
const out = run.results[0]
|
|
109
|
+
if (out?.type === 'result') console.log(out.data) // "6"
|
|
110
|
+
} finally {
|
|
111
|
+
await sandbox.kill()
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from lizard import CodeSandbox
|
|
117
|
+
|
|
118
|
+
with CodeSandbox.create(project_id=project_id) as sandbox:
|
|
119
|
+
sandbox.run_code('import pandas as pd\ndf = pd.DataFrame({"x": [1, 2, 3]})')
|
|
120
|
+
print(sandbox.run_code("int(df.x.sum())").results[0].data) # "6"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
An `Execution` has `stdout`, `stderr`, `results` (the last expression as text, charts and images as base64 PNG, HTML), `error` (`name`, `message`, `traceback`) and `executionCount`. A code error is a result, not an exception. `createContext()` / `create_context()` starts a separate kernel with its own state; `restartContext` clears one. `timeoutMs` (default 60 s) bounds one call: the kernel is interrupted, keeps its state, and `error.name` is `TimeoutError`. `bash` and `javascript` run as fresh processes with no state; `javascript` uses the template's Node 22. With numpy 2, a bare `df.x.sum()` displays as `np.int64(6)`: convert to `int` for plain output.
|
|
124
|
+
|
|
125
|
+
## Ports
|
|
126
|
+
|
|
127
|
+
Start the server on `0.0.0.0` inside the sandbox, then publish the port:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const { hostname, url, accessToken } = await sandbox.exposePort(3000)
|
|
131
|
+
const res = await fetch(`https://${hostname}/api`, { headers: { 'X-Lizard-Access-Token': accessToken } })
|
|
132
|
+
await sandbox.unexpose(3000)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
exposed = sandbox.expose_port(3000)
|
|
137
|
+
res = httpx.get(f"https://{exposed.hostname}/api", headers={"X-Lizard-Access-Token": exposed.access_token})
|
|
138
|
+
sandbox.unexpose(3000)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The port is private: every request needs the token in the `X-Lizard-Access-Token` header, or once as `?lizard_token=<token>` in a browser, which then keeps a cookie. `url` already carries the token, so treat it like a password. `getHost(port)` / `get_host(port)` returns only the hostname, without a scheme.
|
|
142
|
+
|
|
143
|
+
## Keep files (volumes)
|
|
144
|
+
|
|
145
|
+
Only files on an attached volume outlive the sandbox; volumes mount at `/workspace`. A volume attaches to one sandbox at a time, and the sandbox runs in the volume's region, so omit `region` when attaching one.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
await lizard.volumes.getOrCreate('agent-files', { sizeGb: 10 })
|
|
149
|
+
const sandbox = await lizard.create('base', { volumeName: 'agent-files' })
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
lizard.volumes.get_or_create("agent-files", size_gb=10)
|
|
154
|
+
sandbox = lizard.create("base", volume_name="agent-files")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Names are unique within a project: `getOrCreate` returns the existing volume, `create` raises `ConflictError`. New volumes are 1–50 GB (default 5). `volumes.list()`, `volumes.get(nameOrId)` and `volumes.delete(nameOrId)` (deletes the files too). Grow one with `lizard.volumes.resize('agent-files', 20)` / `lizard.volumes.resize("agent-files", 20)`; volumes only grow.
|
|
158
|
+
|
|
159
|
+
## Lifecycle, pause, snapshots and fork
|
|
160
|
+
|
|
161
|
+
| Task | TypeScript | Python |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| Reconnect by ID | `Sandbox.connect(id)` | `Sandbox.connect(id)` |
|
|
164
|
+
| Status | `sandbox.getInfo()` | `sandbox.get_info()` |
|
|
165
|
+
| New lifetime from now (1000–2147483647 ms) | `sandbox.setTimeout(ms)` | `sandbox.set_timeout(ms)` |
|
|
166
|
+
| List a project's sandboxes | `Sandbox.list({ projectId })` | `Sandbox.list(project_id=...)` |
|
|
167
|
+
| Kill | `sandbox.kill()` | `sandbox.kill()` |
|
|
168
|
+
| Pause and resume (memory and processes kept, billing stops) | `pause()`, `resume()`, `waitForStatus(s)` | `pause()`, `resume()`, `wait_for_status(s)` |
|
|
169
|
+
| Snapshot (about 2 s, the source keeps running) | `snapshot(name)` | `snapshot(name)` |
|
|
170
|
+
| Start a sandbox from a snapshot (about 0.4 s) | `Sandbox.restore(id)` | `Sandbox.restore(id)` |
|
|
171
|
+
| Live copies of a running sandbox (each counts toward the running limit) | `fork({ count })` | `fork(count=...)` |
|
|
172
|
+
|
|
173
|
+
A snapshot belongs to the user who took it and restores into any project they can use, unless its scope is `workspace` or `project` (`snapshot(name, { scope })`, `Sandbox.setSnapshotScope(id, scope)`). To start many sandboxes that each keep their own files from one snapshot, take it without a volume and pass `volumeId` to each `restore`. A sandbox with a volume cannot be forked. `fork()` returns one entry per copy, with `sandbox` or `error` (for example the account's sandbox limit). Delete snapshots with `Sandbox.deleteSnapshot(id)`; sandboxes already restored keep running.
|
|
174
|
+
|
|
175
|
+
## Desktop (computer use)
|
|
176
|
+
|
|
177
|
+
The `desktop` template has a graphical desktop with Chromium on `DISPLAY=:1`. `sandbox.desktop.start({ width, height })` starts it and returns `url` (watch and control) and `viewOnlyUrl` (watch only). Both are credentials: never log them, and share `viewOnlyUrl` when someone only needs to look. Then drive it with `screenshot()` (PNG bytes), `click(x, y)`, `moveMouse`, `drag`, `type(text)`, `press('ctrl+l')`, `scroll(direction)` and `openUrl(url)`. Input never goes through a shell. On other templates these calls fail with `DESKTOP_NOT_SUPPORTED`.
|
|
178
|
+
|
|
179
|
+
## Sandboxes for your own users
|
|
180
|
+
|
|
181
|
+
Each sandbox is its own microVM, so running every customer's code in a sandbox of its own already keeps customers apart: one project, one key limited to it, a sandbox per customer or per task tagged with `metadata` (`{ customerId }`), killed when done. That is the default.
|
|
182
|
+
|
|
183
|
+
Add a workspace and a key per customer only when customers get Lizard access of their own (their code calls Lizard, or you hand them a key). Creating workspaces needs an unscoped key; a scoped key gets 403. Run this on your trusted server:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const lizard = new Lizard({})
|
|
187
|
+
const workspace = await lizard.workspaces.create({ name: 'user-123' })
|
|
188
|
+
const project = await lizard.projects.create({ workspaceId: workspace.id, name: 'default' })
|
|
189
|
+
const key = await lizard.apiKeys.create({ name: 'user-123', workspaces: [workspace.id] })
|
|
190
|
+
// Store key.key and project.id now: the API returns the key only once.
|
|
191
|
+
const forUser = new Lizard({ apiKey: key.key, project: project.id })
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
lizard = Lizard()
|
|
196
|
+
workspace = lizard.workspaces.create(name="user-123")
|
|
197
|
+
project = lizard.projects.create(workspace_id=workspace.id, name="default")
|
|
198
|
+
key = lizard.api_keys.create(name="user-123", workspaces=[workspace.id])
|
|
199
|
+
for_user = Lizard(api_key=key.key, project=project.id)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Keep `project.id`: every user's project is named `default`, so the name is ambiguous for your own key. Every user workspace bills to your account and counts toward your sandbox limits. A key without scopes reaches everything the account can; `workspaces` or `projects` limit it, and a scoped key cannot create a broader one. Scoped keys cannot see account billing (HTTP 403 `ACCOUNT_SCOPE_REQUIRED`).
|
|
203
|
+
|
|
204
|
+
`workspaces.delete()` needs an empty workspace unless you pass `force`, and `force` removes apps, addons and projects but leaves sandboxes running and volumes and snapshots in place: kill the workspace's sandboxes and delete its volumes and snapshots first.
|
|
205
|
+
|
|
206
|
+
## Platform APIs
|
|
207
|
+
|
|
208
|
+
The same client manages the rest of Lizard: `projects`, `services` (deploy, inspect, scale, run commands), `addons` (managed databases), `secrets`, `domains`, `storage` (S3 objects), `github`, `metrics`, `workspaces`, `apiKeys` / `api_keys`, `regions`, `billing` and `volumes`. `whoami()` returns the identity behind the key. `platform` is the raw HTTP client for endpoints without a method. A config change that saved but failed to deploy raises `ConfigApplyError`: read its `result` before retrying.
|
|
209
|
+
|
|
210
|
+
## Billing from code
|
|
211
|
+
|
|
212
|
+
`billing.subscription()` returns the plan, trial and this month's usage; `billing.startCheckout()` / `start_checkout()` returns a Stripe Checkout URL for the trial or Pro, which the user finishes in a browser. `startProNow`, `cancel` and `resume` change what the account pays: call them only when the account owner asks.
|
|
213
|
+
|
|
214
|
+
## Errors
|
|
215
|
+
|
|
216
|
+
| Error | Meaning |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `PaymentRequiredError` | HTTP 402: the account must start Pro, start it now, or pay an invoice. Show `message` as is and open `url`; retry after the user pays |
|
|
219
|
+
| `AuthenticationError` | HTTP 401 or 403: bad key, no access, or a refusal such as a volume quota, a snapshot's scope or `ACCOUNT_SCOPE_REQUIRED`: check `code` |
|
|
220
|
+
| `NotFoundError` | HTTP 404 |
|
|
221
|
+
| `ConflictError` | HTTP 409, such as a volume name in use |
|
|
222
|
+
| `TimeoutError` | HTTP 408 or 504 |
|
|
223
|
+
| `LizardError` | Other API failures, including unsupported operations |
|
|
224
|
+
|
|
225
|
+
Each error has the HTTP `status` (`status_code` in Python) and, when the API sends one, a `code`. `kill()` returns `false` for a sandbox that is already gone. Check command exit codes and code errors separately from API errors.
|
|
226
|
+
|
|
227
|
+
Full reference: [sandboxes](https://github.com/lizard-build/lizard-sdk/blob/main/docs/sandboxes.md), [platform](https://github.com/lizard-build/lizard-sdk/blob/main/docs/platform.md), [TypeScript](https://github.com/lizard-build/lizard-sdk/blob/main/packages/js/README.md), [Python](https://github.com/lizard-build/lizard-sdk/blob/main/packages/python/README.md).
|