SSH SDK
InternSSH is a small, opinionated wrapper around Paramiko for talking to a Dev Edition Intern from your workstation. It exposes synchronous run, file transfer via SFTP, and a few convenience helpers for one-off use.
Source: ssh/ssh_sdk.py in intern-developer-sdk.
Install
pip install paramiko
No platform-specific deps — Paramiko ships pure-Python.
Quick example
from ssh_sdk import InternSSH
with InternSSH("172.168.20.145", "system", password="12345") as ssh:
result = ssh.run("systemctl status openclaw --no-pager")
print(result.exit_code, result.duration_s)
print(result.stdout)
Constructor
InternSSH(
host: str,
user: str,
password: str | None = None,
key_path: str | None = None,
port: int = 22,
connect_timeout: float = 10.0,
)
| Arg | Default | Notes |
|---|---|---|
host | required | IP address of the device (find it via Preflight) |
user | required | Usually system |
password | None | Provide this or key_path |
key_path | None | Path to a private key (e.g. ~/.ssh/id_ed25519) |
port | 22 | Override for non-default SSH ports |
connect_timeout | 10.0 | Seconds before the connect call gives up |
Connection lifecycle
InternSSH is a context manager:
with InternSSH("172.168.20.145", "system", key_path="~/.ssh/id_ed25519") as ssh:
...
Or manage the connection by hand:
ssh = InternSSH("172.168.20.145", "system", password="...")
ssh.connect()
try:
...
finally:
ssh.close()
Running commands
result = ssh.run(
"df -h /",
sudo=False, # prepend `sudo`
timeout=None, # per-command timeout in seconds
check=False, # raise CommandError on non-zero exit
)
CommandResult is a dataclass:
| Field | Type |
|---|---|
stdout | str |
stderr | str |
exit_code | int |
duration_s | float |
Run with sudo when you need to touch /root/:
ssh.run("jq . /root/config/config.json", sudo=True)
check=True raises CommandError on any non-zero exit, which is useful in scripts:
ssh.run("test -f /usr/local/bin/intern-server", check=True)
File transfer
ssh.put("./local/build/intern-server", "/tmp/intern-server")
ssh.get("/var/log/intern.log", "./logs/intern.log")
ssh.put_dir("./audio", "/home/system/sdk/audio")
ssh.get_dir("/root/.openclaw", "./backup/openclaw")
put / get are single files; put_dir / get_dir recurse.
Filesystem helpers
ssh.exists("/usr/local/bin/intern-server") # → bool
ssh.read_text("/root/config/config.json") # → str
ssh.write_text("/tmp/note.txt", "hello")
ssh.listdir("/root/.openclaw") # → list[str]
These wrap SFTP and avoid the round-trip of spawning cat or ls over a shell.
One-shot helpers
For scripts that don't want to manage a context, the module exposes:
from ssh_sdk import run_once, scp_to, scp_from
result = run_once(
"172.168.20.145", "system",
"uptime",
password="...",
)
scp_to(
"172.168.20.145", "system",
local="./build/intern-server",
remote="/tmp/intern-server",
key_path="~/.ssh/id_ed25519",
)
scp_from(
"172.168.20.145", "system",
remote="/var/log/intern.log",
local="./intern.log",
password="...",
)
Each helper opens a connection, performs one action, and closes — fine for ad-hoc work, not for loops (you'd open a connection per call).
Errors
| Exception | When |
|---|---|
SSHError | Base class for all SDK errors |
ConnectionError | TCP, auth, or handshake failure |
CommandError | run(check=True) saw a non-zero exit |
TransferError | SFTP put/get failed mid-transfer |
from ssh_sdk import InternSSH, ConnectionError, CommandError
try:
with InternSSH(...) as ssh:
ssh.run("false", check=True)
except CommandError as e:
print(f"command failed: exit {e.exit_code}")
except ConnectionError as e:
print(f"could not connect: {e}")
When to use SSH vs the HTTP API
intern-server exposes a small HTTP API on :5000 (proxied to :80 by nginx). For pairing, channel changes, and factory reset, prefer the HTTP API — it's idempotent and doesn't require credentials. For service inspection, log tailing, and arbitrary maintenance, SSH is the only option.