Commands, files and background jobs
This page lists the SSH gateway tools that act on a host: running a command, reading and writing files, transferring files and running background jobs, with their parameters, results and limits. The SSH gateway is the part of Athenode through which your AI agent runs commands and transfers files on your SSH hosts, each action approved by you or by a rule you set. A connection is one SSH host in the SSH gateway, with its login, its host key and its rules. The SSH gateway describes connections, and Approvals and rules describes how each action is approved.
The action tools
Your AI agent calls these tools through the Athenode MCP server, from a session with the gateway module.
| Tool | What it does | Waits for approval |
|---|---|---|
exec |
Runs one shell command and returns its output | Yes |
exec_output |
Reads more of the output of an exec call whose result was cut |
No |
read_file |
Returns a whole file | Yes |
write_file |
Overwrites a whole file | Yes |
list_dir |
Lists a directory | Yes |
upload |
Returns a one-time URL that puts a file from the agent's machine on the host | Yes |
download |
Returns a one-time URL that fetches a file from the host to the agent's machine | Yes |
start_job |
Starts a long command in the background and returns its job id | Yes |
job_status |
Returns the state of a job | No |
job_logs |
Returns the output of a job | No |
cancel_job |
Stops a job | Yes |
"Waits for approval" means the call runs at once only when the connection's confirmation mode or an allow rule lets it, and waits for your decision in Live otherwise. The tools marked "No" run nothing on the host and never wait. Every call of every tool is recorded in History.
Every action tool takes three fields of its own:
| Field | Description |
|---|---|
connection (required) |
The name or id of the connection. An unknown one answers "Unknown connection", with the advice to call list_connections. |
reason (required) |
Why the action is needed, as a non-empty text. It is shown to the person who approves and kept in History. |
sessionLabel |
A label for the agent's session, up to 200 characters, shown in the web UI. The last label written wins. |
A connection whose host key is not trusted answers "Unknown host key, confirm it in the UI", and a changed host key answers "Host key mismatch". The SSH gateway says how to trust a key. On Athenode Pro, a connection whose credentials are personal answers PERSONAL_CREDENTIALS_REQUIRED to a member who has not stored their own; the gateway on Athenode Pro describes personal credentials.
exec: run a command
exec runs one command on the host and returns when it ends. Each call is independent: the command runs in a POSIX shell (sh -c) with its standard input closed, so a cd or an export in one call does not reach the next. Use cwd and env for that.
| Parameter | Description |
|---|---|
command (required) |
The command line. |
cwd |
The working directory on the host: an absolute path, or ~/… for a path in the SSH user's home directory. |
env |
Environment variables for this command only. Each name matches ^[A-Za-z_][A-Za-z0-9_]*$. The values are not recorded in History; the names are. |
sudo |
true runs the command as root. See Run a command with sudo. |
timeoutSec |
Stops the command and every process it started after this many seconds. There is no timeout by default. |
The result holds stdout, stderr, exitCode, signal (when the command was killed by one), timedOut, durationMs and stderrMerged. Output that is not valid UTF-8 comes back in base64, marked with stdoutEncoding or stderrEncoding set to base64.
| Outcome | What the result says |
|---|---|
| The timeout passed | timedOut: true, the output up to that moment and the error "Timed out after" with the seconds |
| The agent's client cancelled the call | cancelled: true and the output up to that moment; the command is stopped |
| The SSH connection dropped before the command ended | The error "Connection dropped, no exit status" |
A command that runs longer than your AI tool waits for a tool call fails in the AI tool even when it finishes on the host. Run long commands with start_job instead.
When the daemon stops while an exec call runs, the call ends and the command may keep running on the host. History shows the call as Interrupted after the next start.
Output limits of exec
One exec result carries at most 8 MiB of output, stdout and stderr together. When both streams are long, each gets at least half of that. A longer result is cut at a character boundary and adds these fields:
| Field | Meaning |
|---|---|
truncated |
true |
outputId |
The id that exec_output reads the rest with |
stdoutBytes, stderrBytes |
The full size of each stream |
stdoutNextOffset, stderrNextOffset |
Where the rest of each stream starts, or null for a stream that came back whole |
The daemon keeps at most 64 MiB of each stream of a call. The rest is read from the host and dropped, and the result marks that stream with stdoutCut: true or stderrCut: true. Its size fields then give what was kept. To get less output, filter it on the host with grep, head or tail.
| Limit | Default | Range | Setting |
|---|---|---|---|
| Output per result | 8 MiB | 64 KiB to 12 MiB | execOutputLimitBytes |
| Output kept per stream | 64 MiB | 16 MiB to 256 MiB | execStoredOutputLimitBytes |
History and gateway settings says where the gateway settings are set.
exec_output: read the rest of a cut result
exec_output returns the next page of a cut exec result. It reads exec calls of the same connection, for as long as History keeps the call. On Athenode Pro it reads only the calls of your own agent sessions.
| Parameter | Description |
|---|---|
outputId (required) |
The outputId of the cut result. |
stream |
stdout (the default) or stderr. |
offset |
The byte to start from: stdoutNextOffset or stderrNextOffset of the result for the first page, then nextOffset of each page. The default is 0. |
limit |
The most bytes to return, up to the output limit per result. |
A page holds content, encoding, offset, nextOffset (null at the end) and totalBytes. A page of a stream that was cut at the kept limit adds cut: true, and totalBytes is then what was kept.
Run a command with sudo
With sudo: true, exec runs the command as root with the sudo password stored on the connection, entered under Sudo password (optional) in the connection's settings. Without a stored password, the call is refused before any approval with "This connection has no stored sudo password". A wrong password answers "sudo authentication failed".
A sudo command runs in a terminal, so stdout and stderr arrive merged in stdout and the result has stderrMerged: true. The password never appears in the result or in History. Where the output contains it, it reads [REDACTED]. A first-use notice that sudo prints on the host is part of the output.
An allow rule for a sudo command starts with sudo, as writing a rule pattern describes. A sudo command that is stopped after its timeout or a cancellation can survive on the host when it ignores the interrupt and the hang-up. The call reports it as stopped all the same.
File tools: read_file, write_file and list_dir
The file tools work as the SSH user of the connection, never with sudo, and need the SFTP subsystem on the host. A host without it answers with an error that says so. A path is absolute, or relative to the SSH user's home directory, with ~/ expanded.
| Tool | Parameters | Result |
|---|---|---|
read_file |
path |
path, content, encoding and size. The whole file, as UTF-8 text, or in base64 with encoding set to base64 when the file is binary. |
write_file |
path, content, encoding (utf8 by default, or base64 for binary content) |
path and bytesWritten. The whole file is overwritten. An existing file keeps its permissions and a new one gets the host's default. The parent directory must exist. |
list_dir |
path |
path and entries, sorted by name: name, type (file, directory, symlink or other), size, mode in octal and mtime. |
A file counts as binary when its first 8 KB contain a zero byte or when it is not valid UTF-8. read_file returns the whole file in one result, so a large file can exceed what your AI tool accepts in one answer. Fetch large and binary files with download.
An allow rule for a file tool names the tool and a path pattern, such as read_file /var/log/*.
File transfers with upload and download
upload and download move whole files of any kind between the agent's own machine and the host, without the file's content passing through the AI model. The tool returns a one-time URL and a curl command, and the agent runs that command on its own machine, where its files are. The host side works as the SSH user of the connection, never with sudo, over SFTP, so the host needs the SFTP subsystem.
- The agent calls
uploadordownloadwith the remote path. The call is approved like any other file action, on the remote path: an allow rule such asupload /srv/releases/*matches it. - The tool checks the remote side and returns
url,method,headers,expiresAt,maxBytes,remotePathandcurl.downloadaddsbytes, the size of the file. - The agent replaces
<LOCAL_FILE>in thecurlcommand with the path on its own machine and runs it. - The file is transferred, and History records the outcome.
The two commands look like this, with the URL and the secret header filled in by the tool:
curl --fail -T <LOCAL_FILE> -H '<header>' '<url>'
curl --fail -o <LOCAL_FILE> -H '<header>' '<url>'The header carries a one-time secret. Anyone who has the header and the URL can make the transfer, so the agent does not share or log it. --fail makes curl end with an error when the transfer is refused or fails, so the agent keeps it and checks the exit code of curl.
| Property of a transfer URL | Value |
|---|---|
| Uses | One. The first request with the secret uses up the URL, whatever its outcome, and a request with another method is refused. |
| Lifetime | 10 minutes by default, from 30 seconds to 24 hours. The URL stops working at expiresAt. |
| Size | 1 GiB per file by default, from 1 MiB to 64 GiB, given as maxBytes. |
| Restart | A restart of the daemon or the Pro server ends every unused URL and every transfer in progress. |
The lifetime and the size limit are Transfer URL lifetime and File transfer size limit on the Settings page, described in History and gateway settings. A change applies to the URLs issued after it. A URL that was used or has expired cannot be used again, so the agent calls the tool again for a new one.
When the URL is used, Athenode checks again that you may use the connection and that the credentials of the call can sign in. It never switches to other credentials.
upload
| Parameter | Description |
|---|---|
remotePath (required) |
The path of the file on the host, absolute or relative to the SSH user's home directory, with ~/ expanded. Its parent directory must exist, and the path must not be a directory. |
sha256 |
Optional: the SHA-256 checksum the file must have, as 64 hexadecimal characters. |
An upload replaces an existing file only once the whole file has arrived, and the replaced file keeps its permissions. A path that is a symbolic link is written through it, to the file it points to. When the transfer breaks, the file is larger than the size limit, or its checksum differs from sha256, the upload fails and the file on the host stays as it was. A successful upload answers with bytes and sha256, which the agent can compare with its own file.
download
| Parameter | Description |
|---|---|
remotePath (required) |
The path of the file on the host, absolute or relative to the SSH user's home directory, with ~/ expanded. It must be a regular file. |
A file larger than the size limit is refused when the tool is called, with an error that starts with FILE_TOO_LARGE and gives the size and the limit. curl writes the file to the path the agent chose and overwrites a file there.
| Error of the tool call | Meaning |
|---|---|
| "No such file or directory" | The remote file of download, or the remote parent directory of upload, does not exist. |
| "Not a directory" | The parent of the remote path of upload is not a directory. |
| "Is a directory" | The remote path of upload is a directory. |
| "Not a regular file" | The remote path of download is a directory or another kind of file. |
| "Permission denied" | The SSH user may not read or write the remote path. |
Where the agent's machine must reach the URL
The URL starts with the address of the daemon, http://127.0.0.1:3717, or with its public URL when you set one for an agent in a container, as Configuration describes. On Athenode Pro it starts with the server's public URL, and the reverse proxy must pass the transfer URLs to the server, as Configure Athenode Pro describes.
Transfers in History
Each transfer has one row in History, recorded as upload -> <remotePath> or download <remotePath>. It shows Running from the tool call until the transfer ends, then Done with the number of bytes and the checksum, Failed with the reason, or Timed out when the URL expired unused.
Background jobs
A background job is a long command that runs on the host on its own, so the agent's tool call returns at once. A job keeps running when the SSH connection drops and when the daemon restarts.
| Tool | Parameters | Result |
|---|---|---|
start_job |
command (required), cwd, env, as for exec |
jobId, state, pid and startedAt |
job_status |
job (required): the job id |
jobId, command, state, exitCode, pid, startedAt, finishedAt and outputBytes |
job_logs |
job (required); offset, a byte offset, or tail, a number of last lines |
content, encoding, offset, nextOffset, totalBytes and state |
cancel_job |
job (required) |
jobId, state, exitCode and finishedAt |
A job writes stdout and stderr to one log. job_status and job_logs fetch the job's latest state and output from the host first. When the host cannot be reached, job_logs returns the output fetched so far and adds syncError with the reason. offset and tail exclude each other. job_logs returns the output in one result, so read a long log in parts, by passing the nextOffset of each answer as the next offset, or with tail.
cancel_job stops the job and every process it started, first with a request to end and, after a grace period of 5 seconds, by force. The job ends as cancelled. Cancelling a job that has already ended returns it as it is. The approval request for cancel_job shows the job's command, so an allow rule matches the command the job runs.
States of a job
| State | Meaning |
|---|---|
starting |
The job was sent to the host and its start is not confirmed. |
running |
The job runs. |
exited |
The job ended by itself. exitCode holds its exit code. |
cancelled |
The job was stopped with cancel_job. |
lost |
The job's process is gone without an exit code. |
When start_job cannot confirm the start, for example because the SSH connection dropped, the call fails with an error that names the job id. The job may be running: follow it with job_status instead of starting it again. A job that stays starting is checked the next time its state is fetched. It turns running when it started on the host, and lost otherwise, once it is older than 60 seconds.
Limits of background jobs
- A job never runs with sudo.
- While a job runs, its files are in
~/.ssh-gateway/jobs/<id>/in the SSH user's home directory on the host. After the job ends and its output is fetched, the folder is removed. - A host whose login manager ends a user's processes at logout, with
KillUserProcesses=yes, can end jobs when the gateway's SSH session closes. Such a job shows aslost. - An agent controls the jobs of its own user only. A job id of another user, or of another connection, answers "Unknown job".
- A job that has ended, with its output, is kept as long as History keeps the call that started it: 90 days by default, as History and gateway settings describes. A job that is
runningorstartingis never removed.