Hermes Agent

The hermes template is the base Linux desktop with NousResearch's Hermes Agent installed. A computer made from it opens a terminal on login and walks you through picking a model provider. This guide covers what is on the image, how to give Hermes a model key without typing it into the computer, and how to drive Hermes from your own code.

What the template runs

Hermes ships signed in to nothing. A model provider needs your own credential, and the image cannot carry one.

computer.ready fires when the desktop and the Hermes terminal are both up. It does not wait for Hermes to reach a model, because on a fresh computer nothing has been set up yet.

First run on the desktop

Open the computer from the dashboard. The terminal offers:

  1. xAI (Grok), with a SuperGrok or X Premium+ subscription. Hermes prints a URL and a code; approve it in a browser where you are signed in to xAI.
  2. Anthropic, with an API key pasted at the prompt.
  3. OpenAI, with an API key pasted at the prompt.
  4. Something else: run hermes model, which lists every provider Hermes supports, signs you in and picks a model.

Choosing 1 to 3 signs in and sets the model to use with it. hermes model changes the model later. A key or sign-in given here is stored by Hermes in ~/.hermes/auth.json on the computer's disk.

Give Hermes a key as a secret

Instead of pasting a key into the computer, store it once as a Mandala secret and bind it to the computer as an environment variable. Hermes reads provider keys such as ANTHROPIC_API_KEY and OPENAI_API_KEY from the environment. A bound value reaches the desktop session, every new shell and every exec, and Mandala keeps it on a memory-backed file system in the guest rather than on the disk. Replacing the secret's value reaches new shells on a running computer; the TUI already running keeps the old value until it restarts.

Then tell Hermes which provider and model to use. These are the three settings the welcome menu writes after a sign-in; the image's default configuration names another provider's endpoint and model:

hermes config set model.provider anthropic
hermes config set model.default claude-fable-5
hermes config unset model.base_url

The welcome menu only checks for a stored sign-in, so it still appears at each login. Choose 0 (Skip for now), and the TUI starts with the bound key.

Drive it from code

The program below binds your Anthropic key, launches a Hermes computer, configures Hermes, opens the TUI by answering the menu with keystrokes, saves a screenshot, and asks Hermes one question without a terminal. hermes chat -Q -q answers a single query and prints only the reply.

Commands run with desktop=True (Python) or { desktop: true } (TypeScript) run as the desktop user, where hermes is on the PATH. Without it, exec runs as root, which has no hermes.

pip install mandala-computer
# or
npm install mandala-computer

Python

import os
import time

from mandala_computer import Client

client = Client()  # reads MANDALA_API_KEY
key = client.secrets.set("ANTHROPIC_API_KEY", os.environ["ANTHROPIC_API_KEY"])

with client.computers.ephemeral(
    template="hermes",
    name="hermes",
    secrets=[{"secret_id": key.id, "env": "ANTHROPIC_API_KEY"}],
) as computer:
    computer.wait_for("computer.ready")  # the desktop and the welcome terminal are up
    computer.wait_for_secrets(expect_secrets=True)  # the key has reached the guest

    def run(command, timeout=120):
        """Run a command as the desktop user, where `hermes` is on the PATH."""
        res = computer.exec(command, desktop=True, timeout=timeout)
        if not res.ok:
            raise RuntimeError(f"{command} exited {res.exit_code}: {res.stdout_text[-500:]}{res.stderr_text[-500:]}")
        return res.stdout_text

    run("hermes config set model.provider anthropic")
    run("hermes config set model.default claude-fable-5")
    run("hermes config unset model.base_url || true")  # exits 1 when already unset

    # Answer the welcome menu with "Skip for now": Hermes opens with the bound key.
    computer.type("0")
    computer.key("Return")
    time.sleep(10)
    with open("hermes.png", "wb") as f:
        f.write(computer.screenshot(fresh=True))

    print(run("hermes chat -Q -q 'Which Linux distribution is this?'", timeout=300))

TypeScript

import { writeFile } from 'node:fs/promises';
import { Client } from 'mandala-computer';

const client = new Client(); // reads MANDALA_API_KEY
const key = await client.secrets.set({ name: 'ANTHROPIC_API_KEY', value: process.env.ANTHROPIC_API_KEY! });

await client.computers.ephemeral(
  { template: 'hermes', name: 'hermes', secrets: [{ secretId: key.id, env: 'ANTHROPIC_API_KEY' }] },
  async (computer) => {
    await computer.waitFor('computer.ready'); // the desktop and the welcome terminal are up
    await computer.waitForSecrets({ expectSecrets: true }); // the key has reached the guest

    /** Run a command as the desktop user, where `hermes` is on the PATH. */
    const run = async (command: string, timeoutS = 120) => {
      const res = await computer.exec(command, { desktop: true, timeoutS });
      if (res.exitCode !== 0) {
        throw new Error(
          `${command} exited ${res.exitCode}: ${res.stdoutText.slice(-500)}${res.stderrText.slice(-500)}`,
        );
      }
      return res.stdoutText;
    };

    await run('hermes config set model.provider anthropic');
    await run('hermes config set model.default claude-fable-5');
    await run('hermes config unset model.base_url || true'); // exits 1 when already unset

    // Answer the welcome menu with "Skip for now": Hermes opens with the bound key.
    await computer.type('0');
    await computer.key('Return');
    await new Promise((resolve) => setTimeout(resolve, 10_000));
    await writeFile('hermes.png', await computer.screenshot(undefined, { fresh: true }));

    console.log(await run("hermes chat -Q -q 'Which Linux distribution is this?'", 300));
  },
);

ephemeral() deletes the computer when the block or callback exits, even after an error, including a timeout while the computer comes up or its key is delivered. To keep the computer, use client.computers.launch(...) instead, which waits for the secrets itself; it is persistent and billed while it runs, and launch() leaves it in place when it fails, for you to inspect and delete.

client.secrets.set creates the secret or replaces its value, so running the program again uses the key in your environment now. A failed model call (a wrong key, say) makes hermes chat exit non-zero, and the program raises with Hermes' own message.

Using another provider

Bind that provider's key under the variable Hermes reads for it, and set the provider and a model it serves. For OpenAI, bind OPENAI_API_KEY and set model.provider to openai-api. hermes status lists the keys Hermes found in its environment, and hermes model lists the providers and their models.

Working with a running Hermes computer

See also: Troubleshooting and Run your agent next to a computer.