Workspaces API
Lists the API routes that create, change, run, copy, and reach a workspace.
Last updated
On this page
- The workspace document
- Managed workspaces
- POST /v1/workspaces
- GET /v1/workspaces
- GET /v1/workspaces/{id}
- PATCH /v1/workspaces/{id}
- POST /v1/workspaces/{id}/packages/update
- POST /v1/workspaces/{id}/start
- POST /v1/workspaces/{id}/stop
- POST /v1/workspaces/{id}/restart
- DELETE /v1/workspaces/{id}
- POST /v1/workspaces/{id}/clone
- POST /v1/workspaces/restore
- POST /v1/workspaces/{id}/snapshots
- PUT /v1/workspaces/{id}/ttl
- PUT /v1/workspaces/{id}/access
- GET /v1/workspaces/{id}/tools
- POST /v1/workspaces/{id}/file-link
- GET /v1/workspaces/{id}/files
- GET /v1/workspaces/{id}/live/state
- GET /v1/workspaces/{id}/live/stream
- GET /v1/workspaces/{id}/live/tree
- GET /v1/workspaces/{id}/live/file
- GET /v1/workspaces/{id}/live/diff
- GET /v1/workspaces/{id}/live/procs/{pid}/log
- POST /v1/workspaces/{id}/trees
- DELETE /v1/workspaces/{id}/trees/{name}
- POST /v1/workspaces/{id}/ssh-session
- PUT /v1/workspaces/{id}/env
- GET /v1/workspaces/registries
- POST /v1/workspaces/registry-credential
The workspace document
Every route that returns a workspace returns this object. A field marked "omitted" is absent when it has no value.
{ "id": "ws-0a1b2c3d4e5f6a7b", "owner": "karthik", "team": "", "name": "backend", "region": "centralindia", "state": "ready", "image": "ghcr.io/kloudlite/kloudlite-workspace", "placement": "node-2", "volume": "ws-0a1b2c3d4e5f6a7b", "ssh": { "gateway": "wss://ws-centralindia.khost.dev/tunnel/ws-0a1b2c3d4e5f6a7b", "host_key": "ssh-ed25519 AAAA…" }, "packages": ["nodejs@22", "ripgrep"], "base_packages": ["git", "openssh"], "packages_status": { "ready": true, "reason": "Built", "message": "" }, "locks": [{ "entry": "nodejs@22", "version": "22.11.0", "rev": "0f1e2d3c…", "source": "nixhub" }], "desk": "desk-9c8b7a6d5e4f3a2b", "environment": "env-1f2e3d4c5b6a7988", "ttl_secs": 86400, "expires_at": "2026-10-19T09:30:00Z", "trees": [{ "name": "fix-auth", "path": "/home/kl/.agents/fix-auth/workspace", "ready": true }]}| Field | Type | Meaning |
|---|---|---|
id | string | ws- and 16 hex digits |
owner | string | the person's handle |
team | string | the team slug; "" for a personal workspace |
name | string | the name you gave it |
region | string | the team's region |
state | string | creating, ready, stopped, error, or deleted. Any other phase the node reports (starting, idle, unavailable) reads as creating |
image | string | the container image |
placement | string or null | the node that holds it; null until a node claims it |
volume | string or null | the workspace id once it has a snapshot, else null. The delete and ttl responses always carry null |
ssh | object, omitted | gateway (the tunnel URL) and host_key (the key to pin), once the workspace has reported a key |
packages | string[] | the list you asked for, not what is installed |
base_packages | string[], omitted | the platform's base set the profile was built with |
packages_status | condition, omitted | whether the profile for packages is built |
locks | object[], omitted | what each name@version entry resolves to: entry, version, rev, source (nixhub or mirror) |
clone_of | string, omitted | the source workspace id of a clone |
task | string, omitted | what a delegated clone is for |
desk | string, omitted | the desk that manages it. Set at creation, never changes |
environment | string, omitted | the environment its desk connects it to, as its node last converged it |
degraded | condition, omitted | present with reason StorageUnavailable while its volume is read-only and its host remounts it |
decommissioning | condition, omitted | present with reason NodeLeaving while its host is being retired; the workspace starts again on another host |
restart_required | condition, omitted | present with reason EnvChanged while the running workspace does not have the current env vars. The message names the variables, never the values. See POST /v1/workspaces/{id}/restart |
restart_on_env_change | boolean, omitted | true when an env change restarts the workspace |
placed | condition, omitted | present only when no node in the region has room for it (NoCapacity) |
idle_since | string, omitted | RFC 3339; set while it is idle-stopped and not woken since |
ttl_secs | integer, omitted | unused seconds before it is deleted |
expires_at | string, omitted | RFC 3339; last active (else created) plus ttl_secs |
trees | object[], omitted | the extra working directories that the platform has cut: name, path, ready, and reason while not ready |
A condition is { "ready": bool, "reason": string, "message": string }.
Managed workspaces
A workspace made through POST /v1/desks/{id}/workspaces is managed by that desk. Its desk field names the desk and never changes. Only the desk itself (its tool token) can start, stop, delete, or set the ttl of it. Every other caller (your CLI login, a personal API token, another desk) gets 409 with {name} is managed by desk {desk}; use the desk from POST /v1/workspaces/{id}/stop, DELETE /v1/workspaces/{id}, and PUT /v1/workspaces/{id}/ttl. Stop the whole desk with POST /v1/desks/{id}/stop and start it with POST /v1/desks/{id}/start; delete a workspace with DELETE /v1/desks/{id}/workspaces/{workspace}.
Reading, ssh, tools, packages, snapshots, and clone stay open to the owner. A clone of a managed workspace is standalone. PATCH /v1/workspaces/{id} with desk is 400.
POST /v1/workspaces
Creates a workspace and returns it in creating. The platform places it and starts it; poll GET /v1/workspaces/{id} until state is ready.
| Field | Type | Required | Rule |
|---|---|---|---|
name | string | yes | 1 to 40 lowercase letters, digits, or -; starts with a letter, ends with a letter or digit, no --. Not ws, env, api, admin, system, kd, bench, or bld-…, and not shaped like an id (ws- and 16 hex digits). Unique among your workspaces in that team |
image | string | no | Default is the platform workspace image |
team | string | no | A team you belong to. Absent, empty, or your own handle means a personal workspace. Case is ignored |
packages | string[] | no | Nix package names, each optionally pinned name@version (latest, N, N.N, or N.N.N). Every pin is resolved before anything is written |
ttl_secs | integer | no | 600 to 2592000. Absent means never |
env | object | no | Variables for the container, name to value. At most 64; names are [A-Za-z_][A-Za-z0-9_]*; not HOME and not KD_…; a value is at most 32 KiB. A value of exactly ${{ secrets.NAME }} is a secret of the workspace's owner (the team for a team workspace) |
device | string | no | A device of yours (its name or id) to run the workspace on instead of the cluster. See Device workspaces. The device must be online (409, device {name} is offline). The workspace takes no team, packages, or ttl_secs (400, 422), and the name must be new among your workspaces (409, a workspace named "{name}" already exists here) |
access | string[] | no | Device workspaces only (422 otherwise). Names of extra sandbox access, each 1 to 32 of a-z, 0-9, -, at most 8. The device resolves each name from its built-in definitions and its own config; see Device workspaces. A name the device does not define keeps the workspace from starting (AccessUndefined) |
{ "name": "backend", "team": "acme", "packages": ["nodejs@22", "ripgrep"] }Response: 202, the workspace document in creating, with volume null and placement null.
Errors:
400—deskin the body:create a desk's workspace with POST /v1/desks/{id}/workspaces; a workspace made here is standalone.403— your access to the team is paused.404—no such team: you are not a member, or it does not exist (plain text; JSON from the region lookup).409— a workspace of that name exists in that team (a workspace named "api" already exists here, plain text). Over quota (workspaces,diskGb,cpu, ormemoryGb). The team has no region yet (JSON).422— the name breaks the rule (JSON, the rule as the message). The name isbench(JSON,bench is a reserved name).ttl_secsis out of range (ttl_secs must be 600..=2592000 seconds). A package entry is malformed, unknown, or a pin thatcache.nixos.orgdoes not hold (JSON, names the nearest versions). Anenventry breaks the rules above (JSON,env: …). A secret reference that does not exist (`DB_URL` is not a secret of acme, plain text).503— the API cannot reach a package index to resolve a pin (JSON); or the API cannot bind the team's region.
GET /v1/workspaces
Your workspaces, as an array of documents. ?team={slug} lists your workspaces in that team; absent, or your own handle, lists the personal ones. Capsules are never listed.
Response: 200.
[ { "id": "ws-0a1b2c3d4e5f6a7b", "owner": "karthik", "team": "", "name": "backend", "region": "centralindia", "state": "stopped", "image": "ghcr.io/kloudlite/kloudlite-workspace", "placement": "node-2", "volume": "ws-0a1b2c3d4e5f6a7b", "packages": ["ripgrep"] }]Errors:
403—?teamnames a team where your access is paused.404—no such team.
GET /v1/workspaces/{id}
One workspace. {id} is the id or a name; qualify a name with ?owner={team} (see the overview). A capsule id also answers here.
Response: 200, the workspace document.
Errors:
404—not found: no such workspace, or not yours to see.409— the name matches more than one workspace; JSON withmatches(see the overview).
PATCH /v1/workspaces/{id}
Replaces the package list. Entries that did not change keep their locks; new pins are resolved first, and nothing is written if any fails.
| Field | Type | Required | Rule |
|---|---|---|---|
packages | string[] | yes | The whole new list, in the form described under create. [] clears it |
{ "packages": ["nodejs@22", "ripgrep", "jq"] }Response: 200, the workspace document with the new packages and locks.
Errors:
404— not found, or not yours.422— an entry is malformed, unknown, or an exact pin that is not cached (JSON).503— a pin needs an index and the API cannot reach one (JSON).
POST /v1/workspaces/{id}/packages/update
Re-resolves every pinned entry against the index, bypassing the 24-hour cache, and moves the locks to the newest match. The package list itself does not change. Takes no body.
Response: 200, the workspace document. If every index is down the locks it had are kept and the answer is still 200.
Errors:
404— not found, or not yours.422— a pinned entry no longer resolves (JSON).
POST /v1/workspaces/{id}/start
Runs a stopped workspace, or wakes an idle-stopped one and the desk it belongs to. Takes no body.
Response: 202 with an empty body.
Errors:
404— not found, or not yours.409— your owner is over its disk limit (a start resumes filling).409— the workspace is managed by a desk and the caller is not that desk:workspace {name} belongs to desk {desk}; start the desk: kode desks start --desk {desk}.
POST /v1/workspaces/{id}/stop
Stops a workspace. Its host syncs the disk and releases it. Takes no body.
Response: 202, the workspace document with state stopped.
Errors:
404— not found, or not yours.
POST /v1/workspaces/{id}/restart
Restarts the workspace so that it gets the current env vars. Takes no body. A changed env var or a rotated secret does not restart a workspace. It sets restart_required on the document. Use this route to apply the change, or set restartOnEnvChange with PUT /v1/workspaces/{id}/env.
When only secret values changed, the workspace container restarts in its place and keeps its address and its mounted home. Any other change, such as a literal value, a new reference or a new image, restarts the whole workspace with a new address.
A workspace on a device restarts when the device next lists its workspaces, within seconds while it is online.
Response: 202, the workspace document.
Errors:
404— not found, not yours, or a capsule.403—your access to {team} is paused.409—workspace is not running; its next start reads the current env.
DELETE /v1/workspaces/{id}
Deletes the working copy and its home. A volume that still has snapshots survives, detached, and can be restored from. Takes no body.
Response: 202, the document with state deleted and volume null.
{ "id": "ws-0a1b2c3d4e5f6a7b", "owner": "karthik", "team": "", "name": "backend", "region": "centralindia", "state": "deleted", "image": "ghcr.io/kloudlite/kloudlite-workspace", "placement": "node-2", "volume": null, "packages": ["ripgrep"]}Errors:
404— not found, or not yours.
POST /v1/workspaces/{id}/clone
On a device workspace the body is {"name", "snapshot"}, and snapshot is required. The new workspace is on the same device, and its folder is a clone of that snapshot of the source (see Devices API). Without it the answer is 422. A snapshot of another workspace, another owner or another device is 404.
Copies a workspace to a new one in the same team and region, current as of the call. The copy joins no desk unless the body names one with desk. It has no extra working directories and keeps the source's packages and locks.
| Field | Type | Required | Rule |
|---|---|---|---|
name | string | yes | The same rule as create; unique in the source's team |
task | string | no | What the copy is for; at most 200 characters, trimmed; empty means none |
desk | string | no | A desk of yours, by name, team/name, or id. The copy is born a member of it. An API token is refused with 422 |
{ "name": "api-fix", "task": "fix the login redirect" }Response: 202, the workspace document of the new workspace plus based_on: snapshot (the cut that it grafted onto), at (RFC 3339, omitted for the fresh cut), age_seconds, and interrupted. Each clone makes a new cut of the source, so age_seconds is always 0 and interrupted is always false.
Errors:
404— the source is not found or not yours; you cannot spend the team's quota.409— the name is taken in that team. The source has no volume yet (not ready yet: no volume for this workspace). A snapshot is already being cut for it. The storage pool is full (the storage pool is full; retry later or ask an operator). Over quota.422— the name breaks the rule (JSON);taskis longer than 200 characters (JSON); a package entry of the source no longer resolves.
POST /v1/workspaces/restore
A snapshot_id that starts with dsnap- is a device snapshot. The new workspace is made on the snapshot's device and cloned from it, as POST /v1/workspaces/{id}/clone does, with no source workspace needed. name and desk are the only other fields read.
Creates a new workspace from a snapshot, re-attaching to its volume even when the original workspace is gone. It works for a snapshot of a workspace only. Anything the body leaves out is what the snapshot froze.
| Field | Type | Required | Rule |
|---|---|---|---|
name | string | yes | The same rule as create; unique in the new workspace's team |
snapshot_id | string | yes | A reference: a volume name, name@N, or a snapshot id. The snapshot must be ready |
team | string | for a team volume | The team whose volumes the reference is read from. The new workspace joins this team. Absent means your own volumes |
desk | string | no | A desk of yours, by name, team/name, or id. The new workspace is born a member of it. An API token is refused with 422 |
image | string | no | Overrides the frozen image |
packages | string[] | no | Overrides the frozen list; kept locks are reused, new pins are resolved |
A volume of one owner never restores into another owner, by name or by id. To restore a team volume, send team. Without it, the call reads your own volumes, and the name of a team volume answers 404. The new workspace is in the team that team names, else personal.
{ "name": "api-restored", "snapshot_id": "ws-0a1b2c3d4e5f6a7b-8a7b6c91" }Response: 202, the workspace document in creating.
Errors:
400—this snapshot was cut from an environment; use POST /v1/environments/{id}/clone with snapshot_id.404— the snapshot does not exist, is not ready, or is not on a volume of the owner;volume {name} does not exist;volume {name} has no version {n};volume {name} has no ready version; you cannot spend the team's quota.409— the name is taken; over quota; the team has no region.422— the name breaks the rule (JSON); a package entry does not resolve (JSON); a reference has a bad version such asname@0.503— the API cannot reach a package index to resolve a pin (JSON).
POST /v1/workspaces/{id}/snapshots
On a device workspace this asks the device to take a snapshot and answers 202 {"request", "snapshot"} at once; see Devices API. GET on the same path lists the device workspace's snapshots.
Pushes a snapshot of the workspace. A snapshot is kept until it expires or you delete it, and keeps the volume alive once the workspace is gone. The first push of a volume must name it. See Names and versions.
| Field | Type | Required | Rule |
|---|---|---|---|
message | string | no | A note shown in the lineage |
ttl_secs | integer | no | 3600 to 31536000. Absent means the region's default (30 days unless an admin changed it) |
never | boolean | no | true keeps it with no expiry. Exclusive with ttl_secs |
volume | string | on the first push | The volume name: 1 to 40 characters of a-z, 0-9, and -; starts with a letter, ends with a letter or digit. After the first push it is optional, and when given it must equal the name |
{ "message": "before the migration", "ttl_secs": 604800, "volume": "go-base" }Response: 202. The snapshot is working until the platform has cut it; read it back from the lineage of the volume.
{ "id": "ws-0a1b2c3d4e5f6a7b-8a7b6c91", "phase": "working", "volume": "ws-0a1b2c3d4e5f6a7b", "expires_at": "2026-10-25T09:30:00+00:00", "name": "go-base", "version": 1 }expires_at is null when never was set. volume is the volume id. name and version are the volume name and the number of this push.
Errors:
400—invalid snapshot body; orttl_secs and never are exclusive.404— not found, or not yours; the volume no longer exists.409—not ready yet: no volume for this workspace; a snapshot is already being cut for this workspace; over thesnapshotsor disk limit.409— alsovolume {name} already exists; delete it first: another volume of the owner has the name.this volume is named {a}, not {b}:volumediffers from the name of the volume.the version counter of {name} is busy; retry.422—ttl_secs must be between 3600 and 31536000;volume: this volume has no name yet; name it on its first push;volume {name}: …when the name breaks the rule.
PUT /v1/workspaces/{id}/ttl
Sets or clears how long the workspace can stay unused before it is deleted.
| Field | Type | Required | Rule |
|---|---|---|---|
ttl_secs | integer or null | no | 600 to 2592000 to set; null or an absent key clears it |
{ "ttl_secs": 86400 }Response: 200, the workspace document with ttl_secs and expires_at; volume is null in this response.
Errors:
404— not found, or not yours.422—ttl_secs must be 600..=2592000 seconds;a capsule takes no ttl.
PUT /v1/workspaces/{id}/access
Replaces the named extra sandbox access of a device workspace. The running workspace keeps the access it started with; a restart applies the new list.
| Field | Type | Required | Rule |
|---|---|---|---|
access | string[] | yes | As in POST /v1/workspaces. An empty list removes all |
{ "access": ["xcode", "user-temp"] }Response: 200, {"access": [...], "message": "restart {name} to apply"}.
Errors:
404— not found, or not yours.422—access is for device workspaces only: …; a name that breaks the rule; more than 8 names.
GET /v1/workspaces/{id}/tools
The address of the workspace's tool server. Only the workspace's owner gets it. ?team={slug} asserts the workspace is in that team, and is refused when it is not.
Response: 200. address is the address and port 7788 of the workspace, reachable from your desks and the ssh tunnel only. token is the workspace's own credential for that server; it is left out, not null, until the platform has written it.
{ "address": "10.42.1.17:7788", "token": "eyJhbGciOi…" }Errors:
404— not found, or not yours (a team member who is not the owner gets this too).409(JSON) — the workspace is notready(workspace api is stopped; start it to run tools); it is in a different team than?team; or it is between restarts (try again).
POST /v1/workspaces/{id}/file-link
Names one file of the workspace for the person and for an MCP client. Only the workspace's owner gets an answer, with a login or from a desk's agent (the download tool). An API token gets no answer. Works for a cloud workspace and for a device workspace whose machine is online; the path stays inside the workspace's folder. Nothing in the answer is a credential.
Body: {"path": "out/report.pdf", "tree": "fix"}. path is relative to the workspace; tree is optional.
Response: 200.
{ "uri": "kode://workspace/ws-…/file/out/report.pdf?tree=fix", "url": "https://app.kloudlite.io/acme/workspaces/ws-…/files?path=out/report.pdf&tree=fix", "name": "report.pdf", "size": 48213, "mime": "application/pdf" }urlopens the file in the console. The person must be signed in as the owner; the console sends the file with their session.uriis the MCP resource. An MCP client reads it withresources/readon its own connection.
Errors:
400—path must be relative and stay inside the workspace;path is not a regular file;tree is not a tree name.403— a path that resolves outside the workspace, symlinks followed.404— the workspace is not found or not yours, or the file is not found.409— the workspace is notready, or is between restarts.413— the file is over 200 MB (200 000 000 bytes).
GET /v1/workspaces/{id}/files
The bytes of one file, ?path= and optional ?tree=, for the owner's login only (the console relays it from url above). A desk token and an API token get no answer. Images and PDFs are inline with their own type, text is inline as text/plain; charset=utf-8 (an HTML file shows as text), and anything else is an attachment. Every answer has Content-Length, Cache-Control: no-store, X-Content-Type-Options: nosniff and a Content-Security-Policy that starts with sandbox.
Errors:
400— the path leaves the workspace, or is not a regular file.401— no login.403— a path that resolves outside the workspace, symlinks followed; or the owner's access to the team is paused.404— the workspace is not found or not yours, or the file is not found.409— the workspace is notready.413— the file is over 200 MB.
GET /v1/workspaces/{id}/live/state
The document the live page draws first. Only the workspace's owner gets it. A team member, a superadmin and an API token get no answer. Every phase answers 200.
Response: workspace holds the id, name, phase (running, starting, stopped, between-pods or error), image, uptime and package status. live is true when the tool server answered; otherwise why says what to do. trees lists the working directories.
git, tasks, journal and procs come from the tool server. volume comes from the platform's records and fills while the workspace is stopped. A part that failed is null, and partErrors carries one sentence for it. cursor is the highest journal id, the after of a stream. ?tree={name} reads another working directory.
Errors:
404— not found, or not yours.400—treeis not a tree name.
GET /v1/workspaces/{id}/live/stream
A newline-delimited JSON stream of what happens in the workspace: hello, edit, task, journal, proc, reset, ping and a last closed frame that says why the stream ended. ?after={cursor} resumes after a journal id. The platform keeps the tool server's credential and never sends it to you. A workspace has at most 3 open streams, and an API replica at most 20.
Errors:
404— not found, or not yours.409— the workspace is notready, is between restarts, or runs an image without the live routes (update the workspace image).429— too many viewers; the sentence names which limit.502— the tool server did not answer.
GET /v1/workspaces/{id}/live/tree
One directory of the workspace, as { "path", "truncated", "entries" }: directories first, each entry with name, kind, size, mtime, ignored and git (its change letter, empty when clean). ?path= is relative to the workspace directory and defaults to its root, ?depth= is 1 to 3 levels (default 1) and ?tree= picks the working directory.
Errors:
400— the path is absolute, holds..or leaves the workspace, the depth is out of range, or the working directory is unknown.404— not found, or not yours; the directory does not exist.409— the workspace is notready.502— the tool server did not answer.
GET /v1/workspaces/{id}/live/file
One file of the workspace, as { "path", "size", "text" }. ?path= is relative to the workspace directory and ?tree= picks the working directory. A binary file, or one over 1 MiB, answers { "path", "placeholder" } and a sentence in place of its bytes.
Errors:
400— the path is empty, absolute, holds.., or leaves the workspace.404— not found, or not yours; the file does not exist.409— the workspace is notready.
GET /v1/workspaces/{id}/live/diff
The working-tree diff of one file against HEAD, as { "diff" } in unified format. A binary file, or a diff over the size limit, answers { "placeholder" } with the reason. ?path= and ?tree= as for live/file.
Errors: as for live/file.
GET /v1/workspaces/{id}/live/procs/{pid}/log
The output of one background process as newline-delimited JSON, one { "line" } per line. The platform masks tokens and secret assignments, and cuts a line over 8 KiB to a sentence that names its size. The stream ends when the process exits.
Errors:
400—pidis not a process id.404— not found, or not yours.409— the workspace is notready.
POST /v1/workspaces/{id}/trees
Asks for an extra working directory: a writable copy of the live one, for one subagent. Poll trees on the workspace until the entry is ready. It costs no quota, and the region limits how many a workspace holds.
| Field | Type | Required | Rule |
|---|---|---|---|
name | string | yes | 1 to 32 characters of a-z, 0-9, and -. Unique in the workspace |
{ "name": "fix-auth" }Response: 202. The working directory does not exist yet.
{ "name": "fix-auth", "path": "/home/kl/.agents/fix-auth/workspace" }Errors:
404— not found, or not yours.409— the workspace is not running;tree fix-auth exists;trees: 4 of 4 in use; the workspace changed while this was written (try again).422—a tree name is 1 to 32 characters of a-z, 0-9 and -.
DELETE /v1/workspaces/{id}/trees/{name}
Gives an extra working directory back. It leaves the list at once. The platform removes the directory later.
Response: 202 with an empty body.
Errors:
404— the workspace or the working directory is not found (a name that was never asked for is not an idempotent success).409— the workspace changed while this was written (try again).
POST /v1/workspaces/{id}/ssh-session
A one-shot ticket to open an ssh tunnel to a ready workspace, which kode presents to the gateway. Takes no body. Accepts a session or CLI login token only.
Response: 201. {id} can be a name; the answer carries the id it resolved to. token is short-lived, host_key is the key to pin.
{ "id": "ws-0a1b2c3d4e5f6a7b", "token": "eyJhbGciOi…", "gateway": "wss://ws-centralindia.khost.dev/tunnel/ws-0a1b2c3d4e5f6a7b", "expires_at": "2026-10-18T09:35:00Z", "host_key": "ssh-ed25519 AAAA…"}Errors:
404— not found, or not yours.409(JSON) —workspace is stopped(or whichever phase it is in; onlyreadygets a ticket).500— the API cannot sign the ticket.503—the workspace has not reported its host key yet; retry.
PUT /v1/workspaces/{id}/env
Replaces the workspace's whole env: variables you leave out are removed. The new environment reaches the workspace the next time it starts. A running workspace keeps what it has and shows restart_required with the names of the changed variables. A value of exactly ${{ secrets.NAME }} is read from the owner's secrets at start. Only the owner can call it, and a desk tool token is refused.
| Field | Type | Required | Rule |
|---|---|---|---|
env | object | yes | Name to value, the same rules as env on create. {} clears it |
restartOnEnvChange | boolean | no | true restarts the workspace when its env vars change, false only marks it. Default false; absent keeps the current value |
{ "env": { "LOG": "debug", "DB_URL": "${{ secrets.DB_URL }}" } }Response: 200, the workspace document.
Errors:
404— not found, or not yours.422—a capsule's env is set at create(plain text). Anenventry breaks the rules (JSON:env: at most 64 variables,env: "1X" is not a variable name,env: HOME is set by the platform,env: LOG is longer than 32 KiB).`DB_URL` is not a secret of acme(plain text); the scope is the team of the workspace, or its owner for a personal workspace.
GET /v1/workspaces/registries
Lists the container registries that this workspace's builds can push to. kd calls it with the workspace token. The list holds your own registry connectors first and then the connectors of the team of the workspace. It holds no credential.
Response: 200, an array. An owner without a connected registry gets [].
| Field | Type | Meaning |
|---|---|---|
connector | string | The connector name, for example acme-dockerhub-1 |
host | string | The registry host, for example docker.io |
namespace | string | The path that images are pushed under. Empty when the connector names none |
personal | boolean | true for a connector of yours, false for one of the team |
Errors: 403 — your access to the team is paused or removed.
POST /v1/workspaces/registry-credential
Returns the login for one registry host, created for this request. docker-credential-kd calls it with the workspace token each time docker asks for a login. Only a connector that the workspace can use is resolved: yours, then the team's. The connector of another member is never resolved.
| Field | Type | Required | Rule |
|---|---|---|---|
host | string | yes | A host from GET /v1/workspaces/registries. docker.io is Docker Hub |
{ "host": "docker.io" }Response: 200.
| Field | Type | Meaning |
|---|---|---|
username | string | The user name to sign in with |
secret | string | The secret to sign in with. Do not store it |
expiresAt | integer or null | When the login stops working, in milliseconds since the epoch. null for a pasted credential |
Errors:
404—no registry is configuredwhen no registry is connected.no connected registry has that hostwhen none of the connectors you can use has that host.409— the connector has no usable credential, for exampleconnector acme-dockerhub-1 is gone.