Skip to content

SSH access ​

AgentBox does not provide a separate SSH method. If you need an interactive SSH client, create a reusable template that runs OpenSSH behind a WebSocket bridge.

Prepare an Ed25519 key pair. JavaScript and Python use the system ssh client and websocat, reading ~/.ssh/id_ed25519 and ~/.ssh/id_ed25519.pub by default. Go uses golang.org/x/crypto/ssh and github.com/coder/websocket; pass the key paths to runSSH.

mjs
import { readFile as readSshPublicKey } from "node:fs/promises";
import { homedir as homeDirectory } from "node:os";
import pathForSsh from "node:path";
import { spawnSync as runSsh } from "node:child_process";

import { Sandbox, Template, waitForPort } from "@abox-dev/sdk";

const templateName = process.env.TEMPLATE_NAME ?? "ssh-ready";
const sshTemplate = Template()
  .fromTemplate("base")
  .aptInstall(["openssh-server", "curl"])
  .runCmd(
    "case $(uname -m) in " +
      "x86_64) asset=websocat.x86_64-unknown-linux-musl ;; " +
      "aarch64|arm64) asset=websocat.aarch64-unknown-linux-musl ;; " +
      "*) exit 1 ;; esac && " +
      "curl -fsSL -o /usr/local/bin/websocat " +
      '"https://github.com/vi/websocat/releases/download/v1.14.1/$asset" && ' +
      "chmod 755 /usr/local/bin/websocat",
    { user: "root" },
  )
  .setStartCmd(
    "sudo mkdir -p /run/sshd && " +
      "sudo ssh-keygen -A && " +
      "sudo /usr/sbin/sshd -E /tmp/sshd.log && " +
      "sudo /usr/local/bin/websocat -b --exit-on-eof " +
      "ws-l:0.0.0.0:8081 tcp:127.0.0.1:22",
    waitForPort(8081),
  );

await Template.build(sshTemplate, templateName);

const publicKeyPath =
  process.env.AGENTBOX_SSH_PUBLIC_KEY ??
  pathForSsh.join(homeDirectory(), ".ssh/id_ed25519.pub");
const privateKeyPath =
  process.env.AGENTBOX_SSH_PRIVATE_KEY ??
  pathForSsh.join(homeDirectory(), ".ssh/id_ed25519");
const publicKey = (await readSshPublicKey(publicKeyPath, "utf8")).trim();

const sandbox = await Sandbox.create(templateName, {
  network: { allowPublicTraffic: false },
});

try {
  await sandbox.commands.run(
    "sudo install -d -m 700 -o user -g user /home/user/.ssh && " +
      "printf '%s\\n' \"$SSH_PUBLIC_KEY\" | " +
      "sudo tee /home/user/.ssh/authorized_keys >/dev/null && " +
      "sudo chown user:user /home/user/.ssh/authorized_keys && " +
      "sudo chmod 600 /home/user/.ssh/authorized_keys",
    { envs: { SSH_PUBLIC_KEY: publicKey } },
  );

  const proxyCommand = process.env.AGENTBOX_SSH_PROXY_COMMAND?.replaceAll(
    "__SANDBOX_ID__",
    sandbox.sandboxId,
  ).replaceAll("__TRAFFIC_TOKEN__", sandbox.trafficAccessToken ?? "");
  const connection = runSsh(
    "ssh",
    [
      "-i",
      privateKeyPath,
      "-o",
      "IdentitiesOnly=yes",
      "-o",
      "StrictHostKeyChecking=accept-new",
      "-o",
      `ProxyCommand=${
        proxyCommand ??
        `websocat --binary -B 65536 ` +
          `-H="Agentbox-Traffic-Access-Token: ${sandbox.trafficAccessToken}" ` +
          `wss://8081-${sandbox.sandboxId}.agentbox-runtime.ru`
      }`,
      `user@${sandbox.sandboxId}`,
      "printf 'SSH connection works'",
    ],
    {
      encoding: "utf8",
      env: process.env,
    },
  );
  if (connection.status !== 0) throw new Error(connection.stderr);
  console.log(connection.stdout);
} finally {
  await sandbox.kill();
}

The template is built once and can be reused by later sandboxes. Each sandbox receives only the public key. Its WebSocket port is private. JavaScript/Python pass the traffic token to websocat without printing it; Go authenticates the WebSocket upgrade through Sandbox.RequestWithOptions and pins the SSH host key obtained through the authenticated SDK connection. For an interactive shell, connect the SSH session's input and output to your terminal instead of executing the final remote command.

Treat the private key and traffic token as secrets. Do not add them to a template, repository, sandbox metadata, or command output.