Environments.
What a new box inherits, kept as versions on your account. Repos to clone, a setup script, env vars, secret files, and which of your credentials a box may receive.
What a box receives
An environment is what a new box starts as. Pick one at create time (the dashboard form, or environment on POST /api/v1/boxes), or let the default apply. The box then receives, in order, at first boot:
| Item | When | Where it lands |
|---|---|---|
| Env vars | first boot + live | /etc/doppel/box-env.sh, sourced by every login shell |
| Repos | first boot | ~/<dir> (default: the repo name), cloned as your box user, blocking repos first |
| Setup script | first boot | runs once as your box user in your home, after the repos; log in /var/log/doppel-setup.log |
| Secret files | first boot + live | ~/<path>, owned by your box user, mode 0600 unless you set another |
Env vars and secret files are also kept live: the box agent applies changes within seconds, without a reboot. Repos and the setup script run at first boot only.
The environment's vars and per-box vars (below) are written to /etc/doppel/box-env.sh, which login shells source. A shell you already have open keeps its old values; open a new one to see a change.
Secret files never ride the launch user-data (which the instance can read from its metadata service): the box agent fetches them over its authenticated channel, the same way brokered secrets travel, and writes them atomically, owned by your box user, never through a symlinked folder. The dashboard and CLI show a secret file's path and size only; its contents are write-only once saved.
Boxes created before environments existed keep whatever their first boot wrote: the agent's live channel leaves them alone (it still waits on the server between polls like every other box, so nothing spins). Give such a box per-box vars and the agent keeps its boot-time file in place, sourced first, and writes the live file beside it.
The default environment
Nothing is a default until you mark one: tick "Make it the default for new boxes" when creating an environment, use "Make default" on its card, or run prized environments default <name>. Once marked, a box created from the dashboard form or POST /api/v1/boxes without naming an environment takes it; pick none (the dashboard) or send environment: null (the API) for a box with no environment at all. Boxes Prized creates for you (the box your plan launches, a snapshot restore, a fork) never inherit an environment. Deleting the default leaves you with no default.
Versions and upgrade
Every save that changes what a box inherits (script, vars, repos, files, or the credential switches) mints a new immutable version. A running box keeps the version it was created from until you upgrade it; renaming an environment or changing its description does not mint a version.
environment ml v1 ──── v2 ──── v3 (latest)
│ │
box kiwi (created v1) ●────────┼──── upgrade ──▶ v3
box mango (created v2) ●──── stays on v2 until upgradedprized environments upgrade <name> (or the Upgrade button on the environment or on a box's page) pins boxes to the latest version:
- Env vars and secret files re-apply on the box within seconds. A file the new version no longer carries is removed from the box; a var it dropped disappears from
box-env.sh. - Repos and the setup script are first-boot work and do not re-run.
- If the new version is safe for third parties, the upgraded box becomes restricted, permanently: secrets already mounted on it stop resolving on the box's next poll, its phone-access password is revoked, and device keys leave on the next heartbeat.
- A box whose own per-box vars would not fit the launch budget together with the new version is skipped (named in
skipped) and keeps its version; trim its vars, then upgrade again.
Each environment keeps up to 50 versions; older ones that no live box pins are pruned. Deleting an environment leaves its boxes exactly as they are, on the version they carry.
Safe for third parties
A box built for someone else (a contractor, a customer demo, an agent you do not fully trust) should hold none of your credentials. Switch on Safe for third parties on the environment, or tick Restricted when creating a single box. Four channels close:
| Channel | What is refused |
|---|---|
| Secret mounts | POST /api/v1/boxes/:id/secrets answers 409 box_restricted; no connector or secret can be mounted |
| Device keys | The box receives no member device SSH keys; desktop apps that rely on them cannot log in |
| Phone access | POST /api/v1/boxes/:id/mobile-ssh answers 409 box_restricted; no phone password is minted |
| CLI credential handoff | prized agents handoff and the agent, secrets and data steps of prized setup refuse with CHECK_FAILED (exit 5) |
The environment's env vars still apply (they are plain configuration, shown in the open), but a safe environment cannot carry secret files: the save is refused (safe_environment_has_secret_files) until the files are removed, so "none of your credentials" holds for everything Prized could deliver.
Restricted is one-way for a box: it is set at create (restricted: true, or an environment that is safe for third parties), by an upgrade onto a safe version, or inherited by a restore of a restricted box, and it never unsets. The three channels can also be closed one at a time on an environment (secret mounts, device keys, phone access) without making the box restricted; those boxes stay unrestricted and the closed channel simply refuses.
Repos
A repo is owner/name (GitHub) or an https:// clone URL. Each repo has an optional branch, a directory under your home (default: the repo name), an optional setup script that runs inside the clone after it lands, and a blocking flag: blocking repos clone before the others and before the environment's setup script.
Clones are plain https, so public repos work everywhere and no credential ever rides a URL into .git/config. For a private repo, clone on the box once gh is signed in there (prized agents handoff gh or prized setup), or ship a deploy key as a secret file and clone in the environment's setup script.
A repo directory that already exists (a restore, a second boot) is left alone and its clone skipped.
Vars and secret files
Env vars are non-secret by definition: names are shell identifiers, values single-line, and the dashboard and CLI show them. Put anything sensitive in a secret (the broker never exposes the value) or in a secret file.
A secret file is a body of at most 64 KB at a path relative to your home on the box (.aws/credentials, .npmrc, .config/gh/hosts.yml). It is encrypted on the control plane with the secrets key, decrypted only for the box it belongs to, and written 0600 (or the mode you set). Up to 16 files per environment.
Per-box vars
A box can carry its own vars: env at create (POST /api/v1/boxes with {"env": {"NAME": "value"}}), or the Vars editor on the box page (PATCH /api/v1/boxes/:id with {"env": {...}}), which replaces the set. A per-box var wins over an environment var of the same name; everything else from the environment stays. Changes reach the box within seconds and are visible to new login shells.
Per-box vars share the launch budget with the environment: the environment's script, vars and repos plus the box's own vars must fit EC2's 16 KB gzipped user-data as rendered for a snapshot restore (the larger of the two launch shapes, so a box that fits also relaunches from its snapshots), and a create or PATCH that would not is refused with environment_too_large rather than accepted and failed at launch.
Limits
| Limit | Value |
|---|---|
| Env vars per environment | 64, values up to 4096 characters |
| Per-box vars | 100 names, 64 KB in all, and inside the launch budget together with the environment |
| Repos per environment | 16 |
| Secret files per environment | 16, each up to 64 KB |
| Setup script | 16 KB; the whole launch, as rendered for a snapshot restore (the larger launch shape), must fit EC2's 16 KB gzipped user-data budget, or the save is refused (environment_too_large) |
| Versions kept per environment | 50 (older unpinned ones are pruned) |
CLI
prized environments (alias prized envs) manages environments from the terminal. Every command takes --json.
| Command | What it does |
|---|---|
prized environments ls | One row per environment: default, version, boxes, repos, vars, files, third-party switch. JSON: {"environments": [...]} |
prized environments info <name> | Channels, repos, vars, secret files (path, mode, size), versions with box counts. JSON: {"environment": {...}} |
prized environments new <name> [--description] [--setup-script FILE] [--default] | Create; --default marks it the default for new boxes |
prized environments default <name> | Make it the default for new boxes |
prized environments rm <name> | Delete (asks; -y skips); boxes keep their version |
prized environments set <name> --safe-for-third-parties true|false --secret-mounts true|false --device-keys true|false --phone-access true|false [--description] [--setup-script FILE] | Flip the switches or replace the description or script; each change mints a version |
prized environments set-var <name> K=V / rm-var <name> K | Set or remove one env var |
prized environments set-file <name> <path> --from FILE|- [--mode 0600] | Set a secret file from a local file or stdin; rm-file <name> <path> removes it |
prized environments add-repo <name> owner/repo [--branch] [--dir] [--setup-script FILE] [--blocking] | Add or replace a repo; rm-repo <name> owner/repo removes it |
prized environments upgrade <name> [--box B]... | Pin every live box (or just --box ones) to the latest version. JSON: {"environment", "version", "upgraded": [ids], "skipped": [ids]}; skipped = not this environment's live boxes, or their vars would not fit the new version |
Environment JSON (info, and each row of ls):
{"id": "env_…", "name": "ml", "default": true, "safe_for_third_parties": false, "secret_mounts": true, "device_keys": true, "phone_access": true, "setup_script": "…", "vars": [{"name": "A", "value": "1"}], "secret_files": [{"path": ".npmrc", "mode": "0600", "size": 42}], "repos": [{"repo": "owner/name", "branch": "main", "dir": null, "setup_script": null, "blocking": true}], "latest_version": 3, "versions": [{"version": 3, "created": "…", "boxes": 2}], "boxes": 2}Exit codes follow the CLI table: an unknown environment or a restricted-box refusal is CHECK_FAILED (5), a bad value is USAGE (2), a taken name is CONFLICT (6).
API
All routes take your session or a dcp_ CLI bearer and are scoped to your workspace. Bodies are JSON.
| Route | Body / answer |
|---|---|
GET /api/v1/environments | {"environments": [row]}; a row carries isDefault, the four switches, latestVersionNo, repos, secretFiles as {path, mode, size}, versions as {versionNo, createdAt, boxCount} |
POST /api/v1/environments | {name, description?, baseTier?, setupScript?, envVars?, repos?, secretFiles?: [{path, content (base64), mode?}], safeForThirdParties?, passSecretMounts?, passDeviceKeys?, passPhoneAccess?, isDefault?} → 201 {environment} |
PATCH /api/v1/environments/:id | Any of the above; a config field mints a version → 200 {environment} |
DELETE /api/v1/environments/:id | 204; boxes keep their version |
POST /api/v1/environments/:id/default | Make it the default → {environment} |
POST /api/v1/environments/:id/toggles | {safeForThirdParties?, passSecretMounts?, passDeviceKeys?, passPhoneAccess?} → {environment} |
POST /api/v1/environments/:id/vars | {name, value} (add or replace) → {environment}; DELETE /:id/vars/:name removes |
PUT /api/v1/environments/:id/files | {path, content (base64), mode?} (add or replace) → {environment}; DELETE /:id/files?path= removes |
POST /api/v1/environments/:id/repos | {repo, branch?, dir?, setupScript?, blocking?} (add or replace) → {environment}; DELETE /:id/repos?repo= removes |
POST /api/v1/environments/:id/upgrade | {boxIds?} → {upgraded: [ids], skipped: [ids], latestVersionNo} |
POST /api/v1/boxes | Gains environment (id or name; null = none; absent = your marked default, if any), env: {K: V}, restricted |
PATCH /api/v1/boxes/:id | {env: {K: V}} replaces the per-box vars |
GET /api/v1/boxes/:id | Gains environment: {id, name, versionNo, latestVersionNo}, envVars, restricted |
Errors: 400 invalid_repo, invalid_file, invalid_env_var, invalid_env, too_many_repos, too_many_files, environment_too_large (also from POST /api/v1/boxes and PATCH /api/v1/boxes/:id when the per-box vars would not fit the launch), safe_environment_has_secret_files; 404 not_found for an environment, var, file or repo that is not there; 409 name_taken; 409 box_restricted from the mount and phone-access routes.
Something unclear or out of date?
Snapshots
Point-in-time images of a box's disk. Take one by hand or on a schedule, restore it into a new box, fork a running box in one step, and save named templates new boxes start from.
Credentials
Prized does not store your dev credentials. setup hands your agents' sign-ins from your machine straight to the box over the SSH tunnel; the control plane never sees them, and they stay on the box.