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
- The base desktop: Debian with Xfce, Chromium and Firefox, git, Node and Python. The desktop user is
user. - Hermes, installed for
userunder~/.hermes, with thehermescommand in~/.local/bin.hermes --versionnames the pinned release. The computer does not update it;hermes updatedoes, when you choose to. - A launcher that opens a terminal titled "Hermes Agent" when the desktop starts, and again from the desktop icon. It shows a sign-in menu until a provider is signed in, then starts the Hermes TUI (
hermes --tui). - 2 vCPU, 4 GB of memory and a 20 GB disk unless you ask for another size at create.
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:
- 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.
- Anthropic, with an API key pasted at the prompt.
- OpenAI, with an API key pasted at the prompt.
- 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_urlThe 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-computerPython
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
- Reopen the window. If the Hermes terminal was closed, the desktop icon opens it again, or from code:
computer.exec("nohup /usr/local/bin/hermes-terminal >/dev/null 2>&1 &", desktop=True). - Send it text. The TUI has keyboard focus when it opens.
computer.type()andcomputer.key("Return")type into it, andcomputer.paste(text, shift=True)pastes longer text with Ctrl+Shift+V. - Watch a long command.
computer.start_exec(...)returns at once, and its output can be polled while the command runs. The image setsPYTHONUNBUFFERED=1for every shell, so Hermes prints a sign-in code as soon as it has one rather than when it exits. - Open a shell.
mandala terminal <id>opens an interactive shell on the computer as the desktop user.
See also: Troubleshooting and Run your agent next to a computer.