# ssh_exec — Manual

## What it does
Runs a shell command on a remote host over SSH (`action: "exec"`, the default), or moves a file with SCP (`action: "scp"`). Both credential styles work out of the box: inline `host`/`username`/`password`/`key_path` for a one-off, or a named `connection` from `ssh_hosts.json` for a host you visit repeatedly.

Success is `exit_status == 0`. Any other status returns `success: false` with the full output preserved in the error — read it, it is the remote command's own stderr, not a Navi failure.

## Parameters

| Parameter | Required | Description |
|-----------|----------|-------------|
| `action` | no | `exec` (default) or `scp`. |
| `command` | for `exec` | Shell command to run on the remote host. |
| `host` | for a direct connection | Hostname or IP. |
| `username` | no | SSH user. Defaults to `$USER` on the Navi host, so pass it for anything but the obvious case. |
| `password` | no | Password auth. |
| `port` | no | Default 22. |
| `key_path` | no | Private key, e.g. `~/.ssh/id_rsa`. `~` is expanded. |
| `connection` | no | Named entry from `ssh_hosts.json`. |
| `local_path` / `remote_path` / `direction` | for `scp` | `upload` (local→remote) or `download` (remote→local). |
| `timeout` | no | Seconds, default 60. |
| `background` | no | Detach; returns a `task_id` immediately. See [Detaching](#detaching-long-commands). |

## Connecting

**Inline** — anything ad-hoc:

```json
{"command": "uptime", "host": "192.168.1.168", "username": "ubuntu"}
{"command": "df -h /", "host": "10.0.0.5", "username": "root", "key_path": "~/.ssh/id_ed25519"}
{"command": "whoami", "host": "10.0.0.5", "username": "root", "password": "..."}
```

**Named connection** — the host is in `ssh_hosts.json`. Pass `connection` instead of the host triple:

```json
{"command": "systemctl status navi", "connection": "prod"}
```

`connection` takes precedence, and inline `host`/`username`/`password`/`port`/`key_path` override the stored values one by one — useful for the same host under a different user.

With neither a `connection` that exists nor a `host`, the call fails with *No SSH target specified* before any network attempt.

## Credentials and host keys

- **Key auth** — `key_path` (or `client_keys` in `ssh_hosts.json`) is expanded and used first; a `password` given alongside it becomes a fallback.
- **Password-only** — key lookup is disabled so asyncssh does not try your local keys first. This is the difference between a clean login and *too many authentication failures*.
- **No credentials at all** — asyncssh tries the default keys under `~/.ssh/`.
- **Host key verification** is decided by `known_hosts`:
  - key absent from the config → verification is **skipped** (the default for ad-hoc calls);
  - `"known_hosts": null` (as in the template) → the system `~/.ssh/known_hosts` is used;
  - a path → that file is used.
  
  To verify a host you connect to often, put the path (or an explicit `null`) in its `ssh_hosts.json` entry. Skipping is fine for a fresh VPS and wrong for production.

## Connections are pooled

A connection is reused within a session (keyed by session + host + port + user, 20-minute TTL). Two sessions touching the same server keep separate pools and never interfere. If the server drops the connection (`DisconnectError`, `ConnectionLost`, EOF) the entry is evicted and the command is retried **once** on a fresh connection; a second failure is returned as-is. `PermissionDenied` is never retried — fix the credentials instead.

## Transferring files

```json
{"action": "scp", "direction": "upload", "connection": "prod",
 "local_path": "/tmp/build.tar.gz", "remote_path": "/opt/app/build.tar.gz"}
{"action": "scp", "direction": "download", "connection": "prod",
 "remote_path": "/var/log/navi.log", "local_path": "/tmp/navi.log"}
```

Both paths are required; the result says `Uploaded:` or `Downloaded:` with both ends. SCP honours the same `timeout` (default 60 s) — raise it for a large file.

## Detaching long commands

Pass `"background": true` for builds, package installs, backups and anything else that outlives the turn. You get a `task_id` back immediately and the result arrives as a note at the start of a later turn; inspect it with the `tasks` tool. When you detach **without** passing `timeout`, the executor raises the limit to 300 s for that run — do not leave a detached command at the 60 s default and assume it finished.

## Common mistakes

- Omitting `username` and landing as the Navi host's user, not the intended one.
- Putting the host in `command` (`ssh root@10.0.0.5 uptime`) — that depends on local SSH config and key forwarding. Use `host`/`username`, or `connection`.
- Reading a non-zero `exit_status` as a broken tool. The tool worked; the command failed.
- `scp` without `direction` — it defaults to `upload`, so a download silently needs it.
- Expecting host-key verification. It is off unless the connection says otherwise.
