/v1, the same in Python and TypeScript. The CLI is a client of these routes.
Run it
The server is a FastAPI app in Python and a Fastify app in TypeScript. Run it as your own service:Host names in allowed_hosts (MIRAGE_ALLOWED_HOSTS), 127.0.0.1, localhost and ::1 by default, so add the name your clients use. On your own machine the CLI starts the same server as a daemon, which exits 30 seconds after its last workspace is deleted. Options are on the Python and TypeScript server pages.
Auth
Every route butGET /v1/health and GET /.well-known/oauth-protected-resource needs Authorization: Bearer <token>. MIRAGE_AUTH_MODE decides what the server accepts:
In
jwt mode the server checks the signature against that public key, or against the key set your issuer publishes at MIRAGE_JWT_JWKS_URL; it keeps that set and fetches it again for a key id it does not hold, so a key rotation needs no restart (a fixed key needs one). It accepts only the pinned algorithm, requires exp and sub, and checks iss when MIRAGE_JWT_ISSUER is set. A token with aud must name one of MIRAGE_JWT_AUDIENCE, a comma-separated list. A token without aud must carry an azp in MIRAGE_JWT_AUTHORIZED_PARTIES when that is set, and is refused when only an audience is. So one server takes both your app’s session tokens and an OAuth client’s access tokens, as Clerk issues them:
CLI login
WithMIRAGE_LOGIN_CLIENT_ID set, GET /.well-known/oauth-protected-resource tells mirage login where to sign in: the issuer (MIRAGE_JWT_ISSUER) and the OAuth client. The CLI reads the issuer’s endpoints from its /.well-known/oauth-authorization-server, opens the browser on its sign-in, and swaps the code it gets back for tokens with PKCE, so no secret is kept on the user’s machine. The setting needs jwt mode and an issuer, and MIRAGE_JWT_AUDIENCE must list the client. Without it the route answers 404, and mirage login reports that the server needs no login.
In Clerk, create an OAuth application with no client secret and PKCE on, add the redirect http://127.0.0.1/callback (the CLI picks a free port), allow the offline_access scope so the CLI gets a refresh token, and turn the consent screen off so a signed-in user goes straight back to the CLI. Its access tokens last a day and the CLI refreshes them; it asks the user to log in again after 30 days.
Accounts
Injwt mode the token’s sub is the caller’s account, and an account reaches only the workspaces it created:
- Another account’s workspace answers
404on every route, the same as a missing one, and so do its sessions, jobs and asks.GET /v1/workspacesandGET /v1/jobslist only the caller’s. - The owner is kept under the state root, so after a restart a stored workspace reopens only for its owner. A workspace stored before the server ran in
jwtmode has no owner, and no account can create its id. - Snapshot keys live under
accounts/<account>/in the snapshot store. POST /v1/shutdownanswers403.
mirage-account option.
Errors
A failed request answers{"detail": "<message>"}.
Server
Check health
GET/v1/health
Answers {"status": "ok", "workspaces": 2, "uptime_s": 12.5}, without a token.
Shut down
POST/v1/shutdown
Asks the server to stop. The daemon the CLI starts closes its workspaces and exits; a server you run yourself stops only if you gave it on_idle_exit (onIdleExit in TypeScript).
Workspaces
Create a workspace
POST/v1/workspaces
object
required
The workspace YAML as JSON.
string
The workspace id. A fresh one when absent.
201 and the workspace. The same id and config again answers 200 and that workspace; another config under a taken id answers 409.
List workspaces
GET/v1/workspaces
Get a workspace
GET/v1/workspaces/{id}
boolean
default:"false"
Add cache and job internals.
Delete a workspace
DELETE/v1/workspaces/{id}
Closes the workspace and deletes its state.
Close a workspace
POST/v1/workspaces/{id}/close
Stops the workspace and keeps its state: creating the same id again picks up its sessions, history and disk files. RAM mounts are lost unless you snapshot first.
Cancel a workspace’s lines
POST/v1/workspaces/{id}/cancel
Cancels the running and queued lines of every session, from HTTP, the CLI and SSH alike, and answers {"canceled": 2} once they have ended. The sessions stay open.
Kill a workspace’s jobs
POST/v1/workspaces/{id}/kill
Kills every session’s background jobs (cmd &) and the processes runtimes started, and answers {"killed": 1}.
Clone a workspace
POST/v1/workspaces/{id}/clone
string
The new workspace’s id.
object
A config whose mounts replace the source’s.
Snapshot a workspace
GET/v1/workspaces/{id}/snapshot
Answers the tar (application/x-tar). Secrets are stored redacted. The server never writes a snapshot to its own disk. While it captures, new lines and file writes wait, and running ones get 30 seconds to end; if any are still running then, it answers 409, so cancel them and retry. A clone waits the same way.
POST /v1/workspaces/{id}/snapshot
string
required
Puts the tar in the server’s snapshot store under this key, and answers
{id, key, size}. 400 when the server has none.Load a snapshot
POST/v1/workspaces/load
Upload the tar as multipart/form-data: a request part holding the JSON fields below, then a snapshot part of up to 1 GiB. Or send JSON with a key to load from the snapshot store.
string
The tar’s key in the snapshot store; leave it out when uploading.
string
The new workspace’s id.
object
A config that re-supplies the redacted credentials.
Sessions
Create a session
POST/v1/workspaces/{id}/sessions
string
The session id. A fresh one when absent.
object
Prefix to mode (
read, write, exec) to narrow those mounts, such as {"/data": "read"}.List sessions
GET/v1/workspaces/{id}/sessions
Delete a session
DELETE/v1/workspaces/{id}/sessions/{session_id}
Cancels its lines, kills its jobs and closes it.
Cancel a session’s lines
POST/v1/workspaces/{id}/sessions/{session_id}/cancel
Cancels the session’s running and queued lines, from every way in, and answers {"canceled": 1} once they have ended. The session stays open.
Kill a session’s jobs
POST/v1/workspaces/{id}/sessions/{session_id}/kill
Kills the session’s background jobs and the processes its runtimes started, and answers {"killed": 1}. The session stays open.
Change a session’s profile
PATCH/v1/workspaces/{id}/sessions/{session_id}
Replaces the session’s modes, hides and rules. Its cwd, env and history stay.
VFS.md and SKILL.md
GET/v1/workspaces/{id}/vfs-md · /v1/workspaces/{id}/sessions/{session_id}/vfs-md
PUT the same routes
string
GET only, on the workspace route: preview this profile.
string
required
PUT only: expose the document as a live, read-only file at this path.
text/markdown. skill-md in place of vfs-md serves SKILL.md. A missing session or parent is 404, a path that is taken 409, a bad path or profile 422.
Shell, VFS calls and tools
Each call acts as the session?session_id= names, the default one when absent. ?explain=true on the shell or a VFS call answers what the call would do instead of doing it; see Explain.
Run a shell line
POST/v1/workspaces/{id}/shell
string
required
The line.
string
A working directory for this line only.
string
The session. The default when absent.
boolean
default:"false"
Answer
202 with {job_id, workspace_id, submitted_at} at once.boolean
default:"false"
Answer the line’s explanation; run nothing.
{kind, exit_code, stdout, stderr, refusal} when the line finishes, with the job id in X-Mirage-Job-Id. A dropped request cancels its job. With explain, only the line is read: stdin, cwd, runtime and background are refused.
To send stdin, post multipart/form-data with a request part holding the JSON body, then a stdin part. The line starts when the stdin part begins and reads it as it arrives. A body that stops before its closing boundary cancels the line and answers 400. With ?background=true, the whole upload is read before the 202.
Call the VFS
POST/v1/workspaces/{id}/vfs/{call}
string
The session. The default when absent.
boolean
default:"false"
Answer the call’s explanation; run nothing.
{}.
A failed call answers
{detail, errno}, and a policy’s refusal adds its refusal record. The errno picks the status: 404 for ENOENT and NO_XATTR, 403 for EACCES, EPERM and EROFS, 409 for EEXIST, ENOTEMPTY and EBUSY, 400 for ENOTDIR, EISDIR, EINVAL, EXDEV, ELOOP and ENOTSUP, and 500 for anything else, such as EIO.
Match paths
POST/v1/workspaces/{id}/glob
string
required
A pathname pattern, such as
/src/**/*.py.string
The session. The default when absent.
{paths}, every path the session sees that matches.
Call a tool
POST/v1/workspaces/{id}/tools/{tool}
string
The session. The default when absent.
Answers
{text, is_error}. A tool that fails is still 200, with is_error set.
Jobs
Every shell line is a job:pending, running, then done, failed or canceled.
List jobs
GET/v1/jobs
string
Only this workspace’s jobs.
Get a job
GET/v1/jobs/{job_id}
Wait for a job
POST/v1/jobs/{job_id}/wait
number
Seconds to wait. A timeout answers the job as it is, without canceling it.
Cancel a job
DELETE/v1/jobs/{job_id}
Asks
List asks
GET/v1/workspaces/{id}/asks
string
Only this session’s asks.
boolean
default:"false"
Include settled ones.
Answer an ask
POST/v1/workspaces/{id}/asks/{ask_id}
string
required
allow or deny.string
default:"once"
once, or session to allow every matching line in the session.