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.

Install websocat on your computer and prepare an Ed25519 key pair. The example reads ~/.ssh/id_ed25519 and ~/.ssh/id_ed25519.pub by default.

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, so the ssh process passes the sandbox traffic token to websocat without printing it. Remove the final remote command and connect the child process to your terminal if you want to turn the example into an interactive shell.

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