Docs

SSH

Log in to a running sandbox as root, at a port of its own.

For your agent

agental.io/docs/ssh.md

An SSH connection goes from your machine straight to the server the sandbox is on, not through the API. Whether a sandbox can be reached, and who may log in to it, are two things, and each has calls of its own.

Turn it on

PUT/v1/sandboxes/{id}/ssh
curl
curl -X PUT https://agental.io/v1/sandboxes/{id}/ssh \
  -H "Authorization: Bearer $AGENTAL_API_KEY"
json
{
  "enabled": true,
  "enabledAt": "2026-10-13T09:02:11.480Z",
  "address": "203.0.113.7:22041",
  "host": "203.0.113.7",
  "port": 22041,
  "user": "root",
  "hostKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI…"
}
  • The sandbox has to be running. Asked again, the call changes nothing that is already so.
  • address is where to connect now, with host and port beside it. The port leads to this sandbox's SSH server and to nothing else.
  • It lets no key in. That is the next call.
  • A machine that has no SSH server is given one the first time, and that request takes as long as the install does.

Let a key in

POST/v1/sandboxes/{id}/ssh/keys
curl
curl -X POST https://agental.io/v1/sandboxes/{id}/ssh/keys \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"publicKey":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… [email protected]"}'
  • publicKey is one line, as it stands in your ~/.ssh/id_ed25519.pub.
  • The answer has the key's id, its fingerprint as ssh-keygen -l prints it, its type and its comment.
  • A key that is already let in is not added twice.
  • GET /v1/sandboxes/{id}/ssh/keys lists the keys, and DELETE /v1/sandboxes/{id}/ssh/keys/{keyId} takes one out. Whoever is logged in with it stays until they leave.
  • The keys are the machine's own /root/.ssh/authorized_keys. A key written there from inside the sandbox is listed like any other.
  • A fork has its source's keys, and a roll-back puts back the ones its snapshot had.

Keys and SSH as a sandbox is made

A sandbox can be made with keys already let in, and with SSH already on, so that it can be logged in to as soon as the create answers.

curl -X POST https://agental.io/v1/sandboxes \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Idempotency-Key: agent-1" \
  -H "Content-Type: application/json" \
  -d '{"name":"agent-1","sshKeys":["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… [email protected]"]}'
  • sshKeys on a create lets those keys in as the machine is made, before the answer. It turns nothing on.
  • A template lets keys in to every sandbox made from it: POST /v1/templates/{id}/ssh/keys, with the same body as above, and DELETE /v1/templates/{id}/ssh/keys/{keyId} to take one out. A create's own keys come on top of its template's. While your account has two-factor sign-in on, a key new to the template, let in from the console or sent by a person signed in to the site rather than with an API key, takes a code as well.
  • PUT /v1/templates/{id}/ssh turns SSH on for each sandbox as it is made, and DELETE turns that off. GET /v1/templates/{id}/ssh says both, and the keys.
  • A sandbox made already is not changed by its template's keys or SSH. A key taken out of a template stays in the sandboxes made from it until it is taken out of each.
  • A create with keys takes the API key action ssh, as letting one in later does. So do the template's calls, apart from reading.
  • Where a sandbox is made with SSH on and no port can be opened to it, the create still succeeds: SSH is on, and PUT /v1/sandboxes/{id}/ssh opens the port.

Connect

Pin the host key the API gave you, then log in. With the answer above:

shell
echo "sbx_2a9a47e50daec64a ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI…" > ~/.ssh/known_hosts_sbx_2a9a47e50daec64a

ssh -p 22041 \
  -o UserKnownHostsFile=~/.ssh/known_hosts_sbx_2a9a47e50daec64a \
  -o StrictHostKeyChecking=yes \
  -o HostKeyAlias=sbx_2a9a47e50daec64a \
  [email protected]
  • hostKey comes over the API and not over SSH, so you never have to accept a key you are shown.
  • Pin it under the sandbox's id, as above, and not under the address: an address and a port are another sandbox's in time.
  • Only a key that was let in logs in. The SSH server takes no passwords.
  • scp, rsync and git work at the same port.
  • Only port 22 of the sandbox is reached this way. Its other ports are public URLs, or forwarded through SSH itself.

After a stop or a move

SSH stays on for the sandbox. Its port is closed when the sandbox stops, and opened again by itself a moment after it runs again. To read where it is now:

GET/v1/sandboxes/{id}/ssh
  • address is null while the sandbox is not running, and for a moment after a start.
  • The address is mostly the same after a start. It is another one once the sandbox has been moved to another server, and connections that were open are lost at a move.
  • So a client that loses its connection reads this call again, until it shows an address, and connects there.
  • The host key stays the same through a stop and a start, and through a move.

Turn it off

DELETE/v1/sandboxes/{id}/ssh

Closes the port, which cuts whoever is connected. The keys are left as they are.

In the console

A sandbox's Network view has SSH under In, after its ports. It turns SSH on and off, shows the command to log in with and the host key, and lists the keys let in, each with a way to take it out, above a field that lets one in. The keys are read from the machine, so they show while the sandbox runs.

Errors

AnswerMeaning
409 sandbox_not_runningThe sandbox is stopped. Start it, then repeat the request.
409 ssh_unavailableThe machine has no SSH server and one could not be installed or started, or no port could be opened to it. message says which.
400 invalid_requestpublicKey is not one OpenSSH public key line.
501 ssh_endpoint_not_configuredSandboxes here are not logged in to over SSH. GET /v1/policy says so as ssh.

The full machine-readable reference is OpenAPI 3.1. Every page as Markdown, for an agent: llms.txt. Questions: [email protected]