Backends de terminal via plugin não ficam mais presos em /root: default_cwd e supports_streams


Você liga um backend de terminal via plugin no Hermes — Cua Spaces, uma VM QEMU ou o serviço interno de execução da sua empresa. O plugin carrega, terminal.backend aponta para ele, e o primeiro comando cai em /root, mesmo com o projeto em /home/acme/project. Você pede ao agente para entrar lá com cd; o comando seguinte volta para /root. Curiosamente, read_file continua resolvendo o caminho do projeto. Tente colocar o Bot Desktop dentro desse sandbox e ele recusa, sem uma razão clara.

Nada disso era culpa do seu plugin. Três suposições fixas no código do core causavam o comportamento, e o PR #142225, mergeado em 20 de outubro de 2026, transformou essas suposições em opções que o provider pode declarar. A seguir: o mecanismo, a API e as duas armadilhas que vale a pena considerar no design.

As três suposições fixas

Um: o diretório inicial era /root ou nada. Uma tabela _DEFAULT_CWD_BY_BACKEND cobria apenas ssh e vercel_sandbox. Todo backend restante caía em /root, e um plugin não tinha como dizer onde deveria começar.

Dois: a guarda de container descartava caminhos de home do sandbox. O diretório de trabalho de cada comando vem do cwd registrado na sessão, senão do padrão da config (_resolve_command_cwd) — e execute() prefere esse valor ao env.cwd do próprio ambiente. Resultado: os comandos seguiam indo para /root, e quando você fazia cd para /home/acme/project, a guarda classificava /home/... como caminho do host e descartava no comando seguinte. As ferramentas de arquivo mantinham o caminho — daí comandos e ferramentas de arquivo acabarem em planetas diferentes.

Três: backends com suporte a stream eram uma tupla fixa. exec_prefix() em tools/environments/streams.py reconhecia as classes de ambiente Docker, SSH e Singularity, e tools/bot_desktop/placement.py trazia _STREAM_BACKENDS = ("docker", "ssh", "singularity") no código. Um plugin só entrava editando o core e adicionando o próprio nome.

Atributo um: default_cwd

O provider adiciona uma propriedade read-only. Devolva um caminho absoluto dentro do sandbox, ou "~" para dizer “a home de cada sandbox”:

from agent.terminal_env_provider import TerminalEnvironmentProvider


class AcmeBoxProvider(TerminalEnvironmentProvider):
    name = "acmebox"
    is_container = True

    @property
    def default_cwd(self):
        return "/home/acme"        # home conhecida: um caminho absoluto basta

Para sandboxes cuja home só se conhece depois da conexão — o caso normal de um produto de espaço hospedado — devolva "~":

    @property
    def default_cwd(self):
        return "~"

O Hermes nunca expande esse til no host. Ele passa cwd="~" para create_environment() sem mexer, e o seu ambiente resolve para a home que de fato recebeu:

class AcmeBoxEnvironment(BaseEnvironment):
    def __init__(self, *, cwd, timeout, sandbox):
        if cwd == "~":
            cwd = sandbox.home()      # por exemplo, /home/acme
        super().__init__(cwd=cwd, timeout=timeout)

Depois o Hermes grava esse caminho resolvido no objeto de ambiente, então qualquer cd dentro da home do sandbox sobrevive aos comandos seguintes e as ferramentas de arquivo ancoram caminhos relativos no mesmo lugar — a divisão entre o cwd dos comandos e o cwd das ferramentas de arquivo desaparece.

Três detalhes importam na prática. default_cwd é usado quando terminal.cwd não está definido, ou quando a guarda de container descarta um terminal.cwd com cara de caminho do host. Ele é lido a cada comando, então mantenha barato — nada de chamadas de rede. Caminhos relativos são ignorados, porque o Hermes os ancoraria no host.

Atributo dois: supports_streams e exec_prefix()

A tela, o computer_use e o navegador acompanham o terminal para dentro de um sandbox criando processos de vida longa lá, com stdin e stdout abertos. Docker, SSH e Singularity sempre fizeram isso. Agora um backend de plugin pode optar por entrar:

  1. Defina supports_streams = True no provider. Com isso, bot_desktop.placement: auto passa a tratar o seu backend como destino válido, em vez de recusar todo backend de plugin.
  2. Dê ao ambiente um método exec_prefix(*, user=None, interactive=True). Ele devolve um argv local que executa o que vier depois dentro do sandbox — no formato de docker exec -i <container> — ou None quando não dá naquele momento.
class AcmeBoxEnvironment:
    def exec_prefix(self, *, user=None, interactive=True):
        return ["acmebox", "exec", "--stdin", self.sandbox_id, "--"]

Três convenções: o Hermes anexa bash -c <script> como palavras separadas depois do seu argv, então o cliente precisa repassá-las sem alteração e o sandbox precisa ter bash; devolva uma lista, nunca uma string de comando única; user nomeia a conta de execução, então ignore se o seu backend não sabe trocar de usuário.

O lado da imagem também tem exigências: Xvnc, Xfce, Chromium e cua-driver precisam estar no sandbox — nousresearch/hermes-sandbox:desktop já traz tudo. Bot Desktop e o navegador guardam arquivos temporários no get_temp_dir() do ambiente.

Os dois atributos são opcionais e vêm desligados. Um plugin que não define nenhum deles se comporta exatamente como antes.

Duas armadilhas

O modo de isolamento engole os dois recursos. Tanto exec_prefix() quanto a resolução de home com ~ leem o próprio objeto de ambiente, então só funcionam para plugins carregados in-process. Com plugins.isolation: host, o ambiente chega ao Hermes como um placeholder opaco: o Bot Desktop recusa um backend com supports_streams e cita a configuração na mensagem, e default_cwd = "~" continua um til literal. Se o seu backend vai hospedar a tela, não rode esse plugin isolado.

Não entregue caminhos do host. default_cwd aceita um caminho absoluto dentro do sandbox ou "~". Passar um caminho do host como /Users/you/project não levanta erro — ele é tratado como inutilizável e cai no fallback. Da mesma forma, a guarda só mantém um cd que caia sob a home declarada, então um sandbox cuja home só parece /home/... ainda precisa de default_cwd definido.

Já dá para usar hoje?

A mudança está na main (mergeada em 20 de outubro de 2026), mas não em nenhuma release: a última versão publicada é a v0.22.2, anterior a essa interface. Atualize quando sair o próximo patch, ou acompanhe a main. Para o panorama mais amplo, leia o guia do plugin de backend de terminal; os comandos de instalação, ativação e isolamento de plugins estão na referência de hermes plugins; e o trabalho de ciclo de vida de plugins está no post sobre hooks de ciclo de vida.

Resumo rápido

Um backend de terminal via plugin agora responde sozinho a duas perguntas: onde os comandos começam (default_cwd, caminho absoluto ou "~") e se a tela pode morar dentro dele (supports_streams mais exec_prefix()). Os dois são opt-in, com padrões inalterados — e o ponto a planejar é o modo de isolamento, onde ambos degradam para um placeholder.