Skip to content

Cloud sessions

For app builders. How a desktop app such as monocode signs in with a code, brings its agent's sign-in and runs agent sessions in a box.

Desktop apps that drive coding agents, such as monocode, usually start the agent CLI on your laptop. A cloud session starts the same process in your box instead. The app keeps its interface and talks to the process the same way, but the work runs in the cloud and keeps going when the laptop sleeps.

This page is for people building such an app. As a user, all you do is approve the app once, and its sessions then show up on the box page, where you can stop them.

1. Sign in with a code

Apps get an API key through a device sign-in, the same flow TVs and CLIs use (RFC 8628). No password or key is typed into the app.

Start a request with your app's name:

curl -X POST https://api.wack.sh/v1/device/code \
  -H 'content-type: application/json' \
  -d '{"clientName": "monocode"}'
{
  "deviceCode": "…",
  "userCode": "BCDF-GHJK",
  "verificationUri": "https://wack.sh/device",
  "verificationUriComplete": "https://wack.sh/device?code=BCDF-GHJK",
  "expiresIn": 600,
  "interval": 5
}

Open verificationUriComplete in the browser and show userCode in the app so the person can check they match. They sign in to wack if needed and click Allow.

Meanwhile, poll for the key every interval seconds:

curl -X POST https://api.wack.sh/v1/device/token \
  -H 'content-type: application/json' \
  -d '{"deviceCode": "…"}'

Until the request is approved, the response is 400 with one of these errors:

error What to do
authorization_pending Not approved yet. Keep polling.
slow_down You polled too fast. Wait longer between polls.
access_denied The person clicked Deny. Stop.
expired_token The code expired after 10 minutes. Start again.

Once approved, you get the key, and you get it only once:

{ "accessToken": "wack_…", "tokenType": "bearer", "keyId": "…" }

Store it in the system keychain. It appears under Settings → API keys with the app's name, and the person can revoke it there at any time. Send it as Authorization: Bearer wack_… on every /v1 call.

When the person signs out of the app, revoke the key from the app too:

DELETE /v1/key

This revokes the key that makes the call and returns 204. From then on the key gets 401. Revoking a key, here or under Settings → API keys, also stops the processes it started. It doesn't change a box's connect URL that the key read (see Give local agents the box): rotate that before you revoke the key.

2. Bring the agent's sign-in

The agent should run in the box signed in the way it is on the person's computer, without signing in again. wack keeps no list of agents or of where they keep their sign-in: the app knows that already, and uploads it per agent. agent is any lowercase name the app uses for it:

PUT /v1/agents/codex/sign-in
{
  "source": "computer",
  "env": {},
  "files": [{ "path": ".codex/auth.json", "content": "{…}" }],
  "hash": "9f2c…"
}
  • env holds the agent's own variables (API keys, tokens), and files its sign-in files by their path under the home folder.
  • hash is the app's hash of the whole sign-in. Sending the same one again changes nothing, so the app can call this before every start and only a real change is stored.
  • source is computer for a copy from the person's computer, or override for a token the person gave for wack, such as a long-lived one for scheduled runs. A computer upload never replaces an override: wack answers with the override's status, and keeps using it until it is replaced by another override or deleted.

GET /v1/agents shows what wack holds for each agent, { "codex": { "source", "hash", "updatedAt" } }, never the values. DELETE /v1/agents/{agent}/sign-in removes one.

In the box, the files are written readable by root only, and only when the saved sign-in changed. An agent that refreshes its token in place keeps the refreshed one. Before an agent starts, the other agents' sign-in files are taken out of the box, and a token one of them refreshed there is saved to its sign-in first, so an agent finds only its own sign-in. Everything in a box runs as root, though: a plain process or box_exec can read the files of the agent that last started, and two agents running side by side can read each other's. Files that leave the sign-in are removed from awake boxes right away and from the others before the agent's next start. A file the person made in the box, say by signing in there, is left alone.

3. Start a process

Pick a box from GET /v1/boxes, then start the agent there, exactly as you would locally:

POST /v1/boxes/{boxId}/procs
{
  "sessionId": "tab-42",
  "command": "codex",
  "args": ["app-server"],
  "cwd": "/root/projects/app",
  "install": "npm install -g @openai/codex",
  "signIn": "codex"
}
  • install runs when command isn't in the box yet, with up to 10 minutes. It also finds programs an installer only added to the login shell's PATH.
  • signIn writes that agent's sign-in files and puts its variables in the process's environment. The process gets nothing from Toolhouse unless you send "secrets": "all", which is safer for chats that read content you don't control. Without a saved sign-in the call fails with 409 needs_credentials.
  • env adds your own environment variables on top.
  • sessionId makes the call idempotent: while a process with that id is running on the box, calling again returns it instead of starting another.
  • idleTimeoutSeconds (60 to 86400) stops the process once it has gone that long with no output and no stdin. Watching its events doesn't count. Set it for agents that wait on stdin between turns, such as --input-format stream-json: if the app goes away (the laptop sleeps or quits), the process stops and the box can sleep instead of using awake hours. The timeout carries over if wack restarts while the process runs.

Scheduled jobs work the same way through external automations: the app sends the agent's command, args and install, and wack runs it on schedule with the saved sign-in, even when the app is closed.

The response is a Proc with an id and status: "running", and its client is the API key's name. While it runs, the box stays awake and never sleeps under the session.

4. Stream output and send input

GET /v1/procs/{procId}/events?since=0
Accept: text/event-stream

This is a server-sent event stream. Each event's id is its sequence number, and its data is one of:

{ "t": "out", "seq": 12, "line": "…" }
{ "t": "err", "seq": 13, "line": "…" }
{ "t": "exit", "seq": 14, "code": 0 }

Output is split into lines per stream, so a JSON-lines protocol arrives one message per event. The stream replays what you missed and then follows live output until exit. To reconnect, pass the last seq you saw as since (or send it as Last-Event-ID). The last 1,000 lines replay instantly.

Write to the process's stdin with:

POST /v1/procs/{procId}/stdin
{ "data": "{\"type\":\"user\",…}\n" }

Add your own newlines: data is written exactly as sent.

5. Stop

POST /v1/procs/{procId}/kill stops the process and everything it started in the background. A process that exits on its own leaves its background jobs running. GET /v1/boxes/{boxId}/procs?sessionId=… finds a session's process again after the app restarts. The person can also stop any session from the dashboard.

If wack restarts while a process is running, it reconnects to the process when it comes back. A process it can't find again is marked failed.

Give local agents the box

Agents that run on the person's computer can use the box as a tool too, through its MCP server. Read the box's connect URL with the API key:

GET /v1/boxes/{boxId}/connect
{
  "boxId": "box_…",
  "boxName": "my box",
  "mcpUrl": "https://api.wack.sh/mcp/…",
  "connections": [],
  "firstExecAt": null
}

Add mcpUrl to the agent's MCP servers as a streamable HTTP server; Connect has the setup for each agent. The URL is a secret scoped to that box, so keep it out of command lines and logs. The person can rotate it on the box page, after which you read it again. When they disconnect your app or turn the box off for local agents, rotate it yourself with POST /v1/boxes/{boxId}/connect/rotate, so no agent that kept a copy can still use the box. A key revoked under Settings → API keys can't do that, so the URL keeps working until the person uses Rotate URL in the box's Settings.

The full request and response shapes are in the API reference.

Last updated