8 min de leitura

Arquivos de inicialização do SSH: por que os comandos remotos enganam os agentes

Os arquivos de inicialização do SSH podem alterar comandos de agentes remotos por meio de perfis, aliases, funções, mudanças no PATH, comportamento do TTY e regras do servidor.

Arquivos de inicialização do SSH: por que os comandos remotos enganam os agentes

Um comando SSH não é automaticamente o programa que você digitou. Antes que a máquina remota execute git, python ou um script de implantação, o daemon SSH seleciona uma conta, inicia o shell dessa conta e entrega a ele uma sequência de comandos. Arquivos de inicialização e regras do servidor podem alterar o ambiente, substituir o comando ou tornar uma execução interativa enquanto outra permanece não interativa.

Isso importa ainda mais quando um agente relata o resultado. Uma pessoa que vê um banner inesperado, um alias ou um prompt colorido pode parar e investigar. Um agente pode interpretar o código de saída 0 e uma linha de saída com aparência familiar como prova de que o programa esperado foi executado. Já vi essa suposição transformar uma verificação de status inofensiva em uma chamada para um script wrapper, e uma verificação de implantação em um resultado vindo do executável errado.

A solução não é apagar todos os arquivos de perfil. As pessoas precisam de shells interativos utilizáveis. O caminho é identificar o fluxo de execução, separar a conveniência humana da automação e deixar explícito o contrato do comando remoto.

Um comando remoto passa primeiro pelo shell da conta

O OpenSSH normalmente não executa as palavras depois de ssh host como um execve direto do binário de destino. O manual do sshd diz que, depois da autenticação, o sshd executa o comando solicitado por meio do shell do usuário, usando a opção -c do shell. O caminho do shell de login vem do banco de dados da conta, não da máquina local que abriu a conexão SSH.

Esse detalhe explica muitos relatos confusos. Suponha que um agente envie:

ssh deploy@buildbox git -C /srv/app rev-parse HEAD

A conta remota pode usar Bash, zsh, fish, um shell restrito ou um wrapper do ambiente. O shell selecionado recebe um comando equivalente a git -C /srv/app rev-parse HEAD. Primeiro, ele pode se inicializar. Também pode receber um comando substituto do sshd antes de chegar a esse ponto.

Isso é separado do shell que o agente usou localmente. Um agente pode ser executado em um processo limpo em um Mac, enquanto a conta remota deploy tem dez anos de personalizações pessoais de shell. Um prompt local limpo não torna o outro lado limpo.

Também não confunda a análise de comandos do shell com o transporte SSH. O SSH criptografa a conexão e autentica a conta. Ele não promete que python significa o binário esperado, que o PATH permaneceu inalterado ou que um perfil não imprimiu texto na saída padrão.

A primeira pergunta prática, portanto, não é «O SSH se conectou?». É «Qual programa interpretou o comando remoto, sob qual conta e com quais regras de inicialização?»

O modo do shell decide quais arquivos participam

O comportamento de inicialização depende da família do shell e do modo de invocação. Os rótulos usados casualmente, «um shell SSH» ou «um shell Bash», não são precisos o bastante para prever o comportamento.

No Bash, o GNU Bash Reference Manual distingue a invocação de login, interativa e não interativa. Um shell Bash de login lê /etc/profile e depois o primeiro arquivo pessoal legível entre ~/.bash_profile, ~/.bash_login e ~/.profile. Um shell interativo que não é de login lê ~/.bashrc.

Um comando remoto comum geralmente é executado em um shell não interativo. Isso não significa que ele não carregue nada. O Bash lê o arquivo indicado por BASH_ENV, se essa variável estiver definida, antes de executar um comando não interativo. O manual do Bash também documenta um tratamento especial quando o Bash detecta que o sshd o iniciou com a entrada padrão conectada a uma conexão de rede: ele pode ler ~/.bashrc. Não baseie a automação na regra informal de que .bashrc afeta apenas o uso interativo.

O zsh tem um mapa diferente. /etc/zshenv e ~/.zshenv se aplicam a todas as invocações do zsh, o que torna um .zshenv barulhento ou cheio de efeitos colaterais especialmente perigoso. O zsh lê .zprofile para shells de login e .zshrc para shells interativos. Quem coloca alterações de PATH e mensagens de status em .zshenv modifica comandos remotos, scripts e muitas vezes também a inicialização de aplicativos gráficos.

O sh POSIX não oferece uma saída universal. As implementações diferem. A especificação do shell POSIX descreve o arquivo ENV para shells interativos, mas o /bin/sh de um sistema pode ser dash, Bash em modo de compatibilidade ou outra implementação com detalhes próprios. Teste o host real em vez de presumir que um nome de arquivo significa a mesma coisa em todos os lugares.

Essa distinção tem uma consequência operacional importante. Um comando que funciona em uma sessão interativa de diagnóstico pode falhar para um agente porque a sessão interativa lê um perfil que a sessão de comandos ignora. O inverso também acontece: um hook de automação pode afetar as sessões de comandos enquanto o terminal de uma pessoa não o percebe.

Mantenha as configurações interativas em arquivos lidos apenas por shells interativos. Prompts, configuração de conclusão, títulos de terminal, padrões de cores e saudações pertencem a esses arquivos. Coloque os requisitos de ambiente da automação em um arquivo pequeno e documentado, que um launcher controlado carregue deliberadamente. Não deixe que um agente descubra seu ambiente de execução herdando os dotfiles de um desenvolvedor.

Alterações no PATH mudam a identidade, não apenas a conveniência

O PATH costuma ser tratado como uma configuração de conveniência. Em execuções sem supervisão, ele seleciona a identidade do programa. Se um perfil coloca /opt/team/bin no início, curl, git, ssh ou python pode apontar para um wrapper em vez do programa do sistema.

Esse wrapper pode ser intencional. Equipes usam wrappers para escolher credenciais de nuvem, impor verificações de repositório, adicionar telemetria ou selecionar runtimes de linguagem. O erro é permitir que um agente presuma que um comando sem caminho qualificado significa o executável do fornecedor. Ele significa apenas «o primeiro executável correspondente no PATH que este shell tem neste momento».

Aliases e funções criam uma ambiguidade semelhante, embora seu comportamento dependa do modo do shell. O Bash normalmente não expande aliases em um shell não interativo, a menos que expand_aliases esteja habilitado. Isso torna os aliases menos comuns em execuções padrão de comandos SSH, mas não impossíveis. Um arquivo carregado pode habilitar a expansão. Funções não precisam da expansão de aliases. Um perfil pode definir uma função chamada git, exportar o estado do ambiente e fazer com que todos os comandos seguintes naquele shell chamem a função.

Comece perguntando ao shell, mas trate a resposta como evidência do ambiente que está sendo inspecionado, não como uma prova universal:

ssh -T deploy@buildbox '
printf "shell argv0: %s\n" "$0"
printf "shell flags: %s\n" "$-"
printf "PATH: %s\n" "$PATH"
command -V git
command -V python
command -V curl
'

Um resultado típico pode ser:

shell argv0: -bash
shell flags: hBc
PATH: /opt/team/bin:/usr/local/bin:/usr/bin:/bin
git is /opt/team/bin/git
python is /usr/local/bin/python
curl is /usr/bin/curl

O builtin command -V pode informar se o nome é um alias, uma função, um builtin ou um caminho. Nos shells que oferecem esse recurso, type -a git pode exibir mais de um caminho correspondente. No Bash, declare -f git imprime o corpo de uma função se git for o nome de uma função. Essas verificações revelam uma falha comum: um perfil define uma função git() que executa um comando Git real e depois envia discretamente um evento de status. A saída continua parecendo a saída do Git. O efeito colateral acontece em outro lugar.

O armazenamento em cache de comandos acrescenta outra complicação. Alguns shells armazenam o local onde encontraram um comando. Se um perfil altera o PATH depois que o shell resolveu um nome, o shell pode continuar usando o local armazenado até que hash -r, ou o equivalente, limpe o cache. Isso aparece sobretudo em shells interativos de longa duração, mas agentes que mantêm um processo de shell ativo também podem sofrer com isso.

Use caminhos absolutos para comandos cuja identidade afeta uma decisão de segurança ou um resultado de implantação. Não escreva /usr/bin/git apenas porque ele existe no seu laptop. Confirme o caminho esperado no sistema operacional de destino, registre-o e teste-o com a conta que executará o trabalho. Se a implantação usa deliberadamente um gerenciador de versões ou wrapper, nomeie esse wrapper de forma explícita e faça do contrato dele parte do job.

Um pseudo-terminal muda mais do que a formatação

ssh host command normalmente não aloca um pseudo-terminal. ssh -t host command força um. Essa única opção pode mudar os caminhos de inicialização do shell, afetar o buffer de saída, fazer programas emitirem códigos de cor e solicitar uma entrada que uma execução sem TTY nunca pediria.

Muitos arquivos de configuração do shell começam com um teste como este:

case $- in
  *i*) ;;
  *) return ;;
esac

Essa verificação encerra o restante do arquivo, a menos que o shell se identifique como interativo. Um pseudo-terminal pode tornar um shell interativo em circunstâncias nas quais uma conexão de comando comum não seria. O comando passa então a receber aliases, configuração de conclusão, auxiliares de prompt, alterações no PATH e exportações específicas do terminal que não existiam na execução do agente.

Os programas também reagem diretamente ao terminal. Um comando pode exibir uma barra de progresso, paginar a saída, escolher outro formato de diagnóstico ou ler uma confirmação. Um parser de máquina que espera um único objeto JSON pode falhar porque um perfil imprime uma saudação antes do início do programa ou porque o programa detecta um TTY e emite caracteres de controle.

Teste os dois modos ao investigar uma diferença:

ssh -T deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
ssh -tt deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'

O primeiro comando pede ao SSH que não aloque um terminal. O segundo força um, mesmo quando a entrada padrão local não é um terminal. Compare a saída e depois compare a resolução dos comandos e o PATH em cada modo. Se os resultados forem diferentes, não corrija primeiro o parser. Encontre o caminho de inicialização que causou a diferença.

A automação deve usar -T por padrão. Aloque um terminal apenas para um comando que realmente precise dele, como uma tarefa controlada de recuperação interativa. Se um agente precisa de um terminal para executar uma consulta de status rotineira, ele já está operando em um ambiente menos previsível.

As regras do servidor podem substituir o comando solicitado

Veja todas as solicitações SSH
Revise cada chamada SSH no diário de Atividade, em vez de confiar apenas no resumo do agente.

Os arquivos de inicialização são apenas uma camada. O servidor pode substituir ou restringir um comando antes que o shell da conta o veja. Se você inspecionar .bashrc e parar por aí, pode deixar passar a regra que realmente mudou o resultado.

O manual de sshd_config documenta ForceCommand. Um administrador pode aplicá-lo globalmente ou dentro de um bloco Match para um usuário, grupo, endereço ou outra condição. Com ForceCommand internal-sftp, por exemplo, o servidor ignora comandos comuns do shell e executa o serviço SFTP interno. Com um wrapper personalizado, o wrapper recebe o contexto do comando solicitado e decide o que fazer.

Uma entrada de chave pública SSH também pode incluir uma restrição command="..." no arquivo authorized_keys da conta. Isso é comum em contas de backup, acesso a repositórios, transferência de arquivos e automação com escopo limitado. Pode ser uma boa prática de segurança. Torna-se um problema de diagnóstico quando alguém fornece uma credencial restrita a um agente e espera execução arbitrária de comandos.

Os controles de ambiente também precisam ser revisados. AcceptEnv informa ao sshd quais variáveis enviadas pelo cliente ele aceita. SetEnv pode definir variáveis no servidor. PermitUserEnvironment, quando habilitado, pode permitir que o arquivo de ambiente SSH da conta ou as opções da chave autorizada definam valores. Essas configurações podem influenciar o PATH, a localidade, o comportamento do runtime de uma linguagem e hooks como BASH_ENV.

Use ssh -G no cliente, mas conheça seu limite:

ssh -G buildbox | grep -E '^(hostname|user|port|requesttty|remotecommand|sendenv|setenv) '

O OpenSSH exibe a configuração do cliente depois de aplicar os blocos Host locais e os valores padrão. A saída pode revelar um RemoteCommand inesperado, uma configuração de terminal forçada, uma regra de encaminhamento de ambiente ou outro host de destino. Ela não revela ForceCommand do servidor, o shell da conta remota nem as restrições armazenadas em authorized_keys.

Peça ao responsável pelo servidor o sshd_config relevante e as restrições da conta quando a conta for destinada à automação. Se você não administra o host, peça uma interface de execução documentada em vez de tentar reconstruir o comportamento de um shell pessoal por meio de execuções de teste. Uma conta com um wrapper forçado opaco não é um endpoint SSH genérico, mesmo que aceite autenticação.

Inspecione o fluxo de execução sem confiar em uma única verificação

Um comando de diagnóstico é executado exatamente no ambiente sob suspeita. Isso não torna o diagnóstico impossível. Significa que você deve coletar vários fatos independentes e indicar o que cada um comprova.

Primeiro, inspecione a configuração do cliente local com ssh -G. Depois, determine o shell configurado para a conta remota por meio do banco de dados de contas do host. Em muitos hosts Linux, getent passwd deploy retorna um registro separado por dois-pontos cujo último campo é o caminho do shell. No macOS, um administrador pode inspecionar a conta com ferramentas do Directory Service. Quando possível, faça isso por um canal administrativo confiável, em vez de usar o shell potencialmente personalizado da conta.

Em seguida, obtenha e revise os arquivos de inicialização do shell real: arquivos globais, arquivos pessoais e todos os arquivos que eles carregam. Procure estes tipos de comportamento:

  • atribuições a PATH=, inicialização de gerenciadores de versões e comandos hash
  • alias, definições de funções e opções do shell, como expand_aliases
  • comandos de saída, como echo, printf e auxiliares de controle do terminal
  • BASH_ENV, ENV, variáveis de localidade e hooks de ambiente específicos de linguagens
  • ramificações condicionais que testam -t, $-, $SSH_TTY ou $TERM

Não procure apenas pela palavra alias. Uma linha como . ~/.local/share/tool/init.sh pode carregar o arquivo que define a função importante para você. Siga a cadeia de arquivos carregados até o fim. Arquivos de perfil frequentemente se transformam em uma pilha de includes condicionais, e é aí que o comportamento da automação deixa de ser revisável.

Depois, faça uma coleta de impressões digitais limitada, usando um comando inofensivo. Registre as flags do shell, o diretório atual, o PATH, os nomes de ambiente relevantes e a resolução exata das ferramentas que o job executará. Evite despejar todas as variáveis de ambiente nos logs. Tokens e credenciais de nuvem frequentemente ficam ali. Uma transcrição de diagnóstico deve comprovar as condições de execução sem se transformar em um novo depósito de segredos.

Por fim, teste o comando exato de produção no mesmo modo que o agente usará: mesma conta, sem terminal, a menos que seja necessário, mesmo transporte de entrada e mesmo diretório de trabalho. Um resultado obtido por meio do login interativo de um administrador não é equivalente. Ele responde a outra pergunta.

Mantenha as evidências junto da definição da implantação ou da automação. O artefato útil não é uma captura de tela de um terminal funcionando. É um documento curto que nomeia o shell da conta, os arquivos de inicialização autorizados a afetar a execução, os caminhos exigidos dos executáveis, o ambiente esperado, a política de terminal e as restrições de comandos do servidor.

Torne o shell da automação deliberadamente monótono

Mantenha as chaves SSH longe dos agentes
O Sallyport executa o SSH por meio do sp-ssh, para que o agente nunca receba a chave SSH.

Uma ação remota confiável deve entrar em um shell conhecido com um ambiente curto e então executar binários nomeados. Isso não remove o shell inicial da conta usado pelo sshd, mas limita quanto do estado herdado chega ao programa que realiza o trabalho.

Para um script pequeno e fixo, envie o script pela entrada padrão e execute um shell conhecido com um ambiente vazio. A sequência de comandos remotos permanece fixa, o que também evita construir uma torre frágil de escapes de aspas locais e remotas.

ssh -T deploy@buildbox '/usr/bin/env -i PATH=/usr/bin:/bin /bin/sh -s' \u003c\u003c'REMOTE'
set -eu
PATH=/usr/bin:/bin
export PATH

/usr/bin/git -C /srv/app rev-parse HEAD
REMOTE

Isso oferece propriedades úteis. -T evita um terminal. /usr/bin/env -i limpa as variáveis herdadas. /bin/sh -s lê o script literal da entrada padrão, e o heredoc entre aspas impede que o shell local expanda $PATH ou outro texto antes da transmissão. set -eu interrompe a execução diante de um parâmetro não definido ou de um comando simples que falhe, respeitando a semântica normal do shell.

Também há limites. /usr/bin/env, /bin/sh e /usr/bin/git são exemplos, não caminhos que devam ser copiados sem verificação para todos os hosts. Confirme os caminhos em cada classe de destino. Limpar o ambiente pode remover variáveis de que um programa realmente precisa, incluindo o diretório inicial, a localidade, a configuração de proxy ou o local do runtime. Adicione apenas as variáveis nomeadas de que o programa necessita e documente o motivo de cada uma.

Não use env -i para esconder uma configuração de conta quebrada. Se um arquivo de inicialização altera o shell inicial de forma tão agressiva que impede a execução do launcher fixo, a própria conta não é adequada para trabalho sem supervisão. Use uma conta dedicada, com shell de login controlado e poucos arquivos de inicialização.

Evite inserir texto arbitrário do agente em uma sequência remota sh -c. Erros de aspas podem transformar dados em sintaxe do shell antes que o script pretendido os receba. Passe os dados por um canal restrito, use um script remoto fixo com argumentos validados ou utilize um protocolo criado para solicitações estruturadas. O shell é poderoso porque interpreta texto como código. Um agente não deve poder apagar essa fronteira por acidente.

Separe contas interativas de contas de agentes

Bloqueie o caminho das ações SSH
O cofre bloqueado nega todas as ações, incluindo comandos SSH encaminhados pelo aplicativo.

O design mais limpo dá identidades remotas diferentes para pessoas e automação. Uma conta de desenvolvedor pode manter seu prompt, a conclusão de comandos, gerenciadores de linguagens e auxiliares pessoais. Uma conta de automação deve ter um shell declarado, um diretório inicial pequeno e arquivos de inicialização que não façam nada em sessões de comandos ou executem uma configuração estreitamente revisada.

Essa separação não é burocracia. Ela permite responder a perguntas simples, mas necessárias: Qual binário do Git faz a implantação? Qual é o diretório inicial? A conta aceita um terminal? Quais nomes de ambiente podem afetar uma versão? Quem pode alterar o wrapper que executa os comandos?

Uma conta dedicada também facilita o raciocínio sobre restrições de comandos. Você pode usar um wrapper forçado para as poucas ações que a conta deve executar e rejeitar todo o restante. Isso não transforma o wrapper em uma linguagem de políticas. Mantenha-o simples o bastante para ser inspecionado: valide os argumentos, defina o ambiente documentado, invoque um caminho absoluto de programa e escreva um registro de auditoria.

Não tente resolver isso colocando dezenas de exceções em um grande case dentro do .bashrc. Essa abordagem é popular porque todo usuário já tem um arquivo de perfil e nenhuma alteração de implantação é necessária. Ela falha porque a semântica do perfil varia conforme o modo do shell, a ordem dos arquivos carregados fica obscura e uma edição interativa inofensiva pode alterar o comportamento sem supervisão. Coloque a configuração da automação no launcher ou no script dedicado que administra o contrato da automação.

O Sallyport pode manter as credenciais SSH fora do processo do agente e executar ações SSH por meio do helper sp-ssh integrado, mas não pode tornar determinística uma conta remota sem controle. Trate a revisão da conta remota como parte do design da ação e use o registro de atividades para comparar o que o agente solicitou com o que o lado remoto retornou.

Confie nos resultados apenas depois de definir o contrato

O resultado de um comando de agente é confiável quando você consegue declarar o que foi executado sem depender dos hábitos atuais do shell de uma pessoa. Essa declaração precisa de mais do que um hostname e um código de saída. Ela deve identificar a conta SSH, a identidade do host de destino, a configuração do terminal, a restrição de comando do servidor, se houver, o shell selecionado, a construção do ambiente, a origem do script e o caminho do executável.

Existe um limite incômodo aqui. Você não pode provar, a partir da própria saída de um comando remoto, que o shell remoto não alterou o comando antes de imprimir aquela saída. Se o shell da conta, a restrição da chave autorizada ou o comando forçado estiver fora do seu controle, você precisa de evidências administrativas ou de outra conta. Repetir a mesma verificação dez vezes apenas repete a mesma suposição.

Comece pelos comandos que os agentes já executam e que podem alterar código, infraestrutura ou dados de produção. Para cada um, execute-o com e sem terminal, inspecione como o executável é resolvido e substitua a configuração herdada por um launcher explícito quando esse resultado for relevante. A primeira surpresa geralmente é o PATH. A segunda costuma ser um arquivo de inicialização que alguém esqueceu que ainda era carregado.

Depois de encontrar uma dessas surpresas, não adicione outra condicional ao perfil e considere o problema resolvido. Mova o comportamento para uma conta controlada ou um script fixo, registre o contrato do comando e torne a próxima execução do agente tão previsível que a saída signifique exatamente o que diz.

FAQ

O comando ssh host carrega os arquivos de inicialização do shell?

Não. O SSH inicia um processo por meio do shell configurado para a conta, e esse shell pode ler arquivos de inicialização antes de executar o comando. Um comando simples pode herdar um PATH alterado, variáveis exportadas, funções ou um wrapper forçado.

O .bashrc é executado quando o SSH executa um comando remoto?

Normalmente não em um comando Bash não interativo comum, mas esse «normalmente» é justamente o motivo pelo qual os agentes enfrentam problemas. O Bash pode ler BASH_ENV e tem um comportamento especial quando o sshd o inicia com a entrada padrão conectada à rede. Outros shells têm suas próprias regras.

Como executar um comando remoto limpo pelo SSH?

Use ssh -T para evitar um pseudo-terminal, invoque um shell conhecido por seu caminho absoluto, limpe o ambiente com env -i e use caminhos absolutos para os programas relevantes. Isso reduz variações acidentais, mas não impede um ForceCommand nem um shell controlado por outra pessoa.

Por que ssh -t muda a saída do meu comando?

Um pseudo-terminal pode fazer o shell se classificar como interativo, o que geralmente faz com que ele carregue um conjunto diferente de arquivos. Ele também pode alterar a formatação da saída, os prompts, as cores, as quebras de linha e o comportamento de programas que detectam se a saída padrão é um terminal.

Como saber se um comando remoto é um alias ou uma função?

command -V tool mostra como o shell atual resolveria um nome, incluindo aliases, funções, builtins e caminhos. type -a tool é útil nos shells que oferecem esse comando, porque pode revelar vários executáveis encontrados no PATH.

Agentes de IA devem usar uma conta SSH compartilhada?

Use uma conta de automação dedicada, com shell documentado, diretório inicial mínimo e sem lógica de perfis pessoais. Não compartilhe a conta interativa de um engenheiro com um agente não supervisionado e depois trate a saída como evidência de implantação.

Qual é a diferença entre shells de login, interativos e não interativos?

Um shell de login lê arquivos de login, como /etc/profile e um dos perfis pessoais de login do Bash. Um shell não interativo executa uma cadeia de comandos e pode ler outro mecanismo, como BASH_ENV. Um shell interativo habilita recursos voltados ao usuário, como prompts e, muitas vezes, aliases.

O ssh -G mostra o que acontece no servidor remoto?

ssh -G host exibe a configuração do cliente depois que o OpenSSH aplica os blocos Host e os valores padrão locais. Ele não revela os arquivos de inicialização do servidor, o shell da conta remota, ForceCommand nem uma restrição de comando em uma entrada de authorized_keys.

O que devo auditar antes de confiar no resultado SSH de um agente?

Inspecione o shell da conta, os arquivos de inicialização, o PATH, a resolução dos comandos, as regras do daemon SSH e as restrições específicas em authorized_keys. Teste o comando exato com e sem TTY e registre o contrato de execução resultante, em vez de confiar em um resultado isolado de um terminal.

Um gateway SSH pode impedir que arquivos de perfil alterem os comandos?

Não. Um gateway pode manter as credenciais SSH longe do agente e registrar a ação, mas o host remoto ainda decide qual shell da conta e quais regras do servidor processarão a solicitação. Trate o controle das credenciais e a previsibilidade da execução remota como tarefas separadas.

Sallyport

O Sallyport executa chamadas de API e comandos SSH pelo seu agente de IA. As chaves ficam em um cofre local no seu Mac; você aprova cada execução e toda ação vai para um registro selado.

© 2026 Sallyport · Código aberto sob Apache-2.0 · Oleg Sotnikov