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:
- Defina
supports_streams = Trueno provider. Com isso,bot_desktop.placement: autopassa a tratar o seu backend como destino válido, em vez de recusar todo backend de plugin. - 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 dedocker exec -i <container>— ouNonequando 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.