chatcli server (alias chatcli serve) roda o ChatCLI como serviço gRPC. As chaves de API ficam no servidor; os clientes se conectam com chatcli connect, com o operator ou com qualquer cliente gRPC. O mesmo binário roda no notebook, em container (Docker) e no Kubernetes (Helm chart).
Início rápido
1
Suba um servidor local
Sem credencial, o servidor só escuta em loopback:
2
Conecte de outro terminal
O listener é texto puro, e o cliente disca TLS a menos que você libere o texto puro explicitamente:
3
Deixe acessível por outras máquinas
Faça bind em todas as interfaces e defina uma credencial, senão o servidor se recusa a subir (veja Endereço de bind e a regra da credencial):
Endereço de bind e a regra da credencial
O endereço de escuta vem deCHATCLI_BIND_ADDRESS. Sem ele, o servidor escolhe:
Um
CHATCLI_BIND_ADDRESS explícito sempre vence. Não existe flag --bind.
Em endereço de loopback (127.0.0.1, ::1, localhost) o servidor pode rodar sem credencial: a fronteira de confiança é a máquina. Em qualquer outro endereço, um servidor sem credencial aceitaria todo chamador como administrador, então ele se recusa a subir e sai com status 1:
Material JWT configurado que não carrega não conta como credencial. Quando o JWT é a única credencial, o servidor se recusa a subir com
refusing to start: CHATCLI_JWT_PUBLIC_KEY is set but no RSA public key could be loaded: ... (ou a mensagem equivalente de CHATCLI_JWT_SECRET). Quando também há token ou mTLS, o servidor sobe, registra o erro do JWT no log e aceita só as credenciais que funcionam.
Flags
Uma flag passada na linha de comando vence a variável.
chatcli server --help lista as flags.
Variáveis de ambiente
Tudo o que a CLI local lê (chaves de provedor comoANTHROPIC_API_KEY, OPENAI_API_KEY, *_MODEL, *_MAX_TOKENS, CHATCLI_CA_BUNDLE) também configura o servidor. Estas são específicas do modo servidor:
Autenticação do servidor
O chamador envia a credencial como metadata gRPCauthorization: Bearer <token>. chatcli connect --token faz isso tanto para o token compartilhado quanto para um JWT.
- Token compartilhado
- JWT (HS256 ou RS256)
- TLS mútuo
legacy-token, com role admin. Isso também significa que todos dividem um único bucket de rate limit.Roles
Onde o servidor as aplica:
Limitador de falhas de autenticação
Cada host de cliente tem uma cota de autenticações bearer/JWT que falharam: rajada de 5, reposta a uma a cada 12 segundos; a tabela é zerada a cada 5 minutos. Só uma autenticação que falha consome a cota (credencial ausente, malformada, errada ou expirada); credenciais válidas nunca são limitadas, então um cliente movimentado, as sondas do operator e um laço de chamadas one-shot passam em velocidade normal. Quando um host esgota a cota, suas chamadas recebemUnauthenticated: authentication failed antes de a credencial ser conferida, até uma vaga ser reposta, e o log do servidor registra auth failure rate limit exceeded. Chamadores identificados só pelo certificado de cliente não passam por ele.
TLS
O servidor liga o TLS quando--tls-cert e --tls-key estão definidos; a versão mínima é TLS 1.3. Nenhum dos dois significa texto puro de propósito. Um sem o outro é recusado antes de qualquer coisa subir, em vez de cair num listener em texto puro: FATAL: --tls-cert foi definido (server.crt) mas --tls-key não: recusando iniciar um listener em texto puro. Defina os dois, ou nenhum para texto puro (e a mensagem espelhada para chave sem certificado). Certificado que não carrega é fatal e aparece no stderr: FATAL: TLS certificate load failed: ... (cert=..., key=...).
O certificado precisa valer para o nome que os clientes discam. Uma CA privada para testes:
Credenciais do LLM
O servidor chama o modelo com as próprias chaves, a menos que a requisição traga credenciais. Opções dochatcli connect:
A imagem do servidor não inclui o Devin CLI, então
DEVIN não está disponível como provedor do servidor.
Roteamento de requisições
Todo RPC de prompt (SendPrompt, StreamPrompt, InteractiveSession, AnalyzeIssue, AgenticStep) resolve o cliente do modelo do mesmo jeito:
- Uma requisição que encaminha credencial (
--llm-key,--use-local-auth, campos da StackSpot, URL do Ollama) ou que nomeia provedor ou modelo ganha um cliente dedicado só para ela. - Uma requisição que não nomeia nada nem encaminha nada passa pela cadeia de fallback quando há uma instalada; senão, usa o provedor e o modelo padrão do servidor.
max_tokensé o valor da requisição quando definido; senão o override*_MAX_TOKENSdo provedor (ANTHROPIC_MAX_TOKENS,OPENAI_MAX_TOKENS,BEDROCK_MAX_TOKENS, …); senão o teto do catálogo para o modelo.- Um 401/403 do provedor nas credenciais do servidor reconstrói os provedores uma vez e tenta de novo; as reconstruções são limitadas a uma a cada 30 segundos no processo inteiro. Credenciais encaminhadas pelo chamador nunca são renovadas.
- Uma resposta interrompida por classificador de segurança (
stop_reason: refusal) é reenviada uma vez para o modelo irmão do mesmo provedor; em stream, só enquanto nenhum texto foi enviado.
Cadeia de fallback
- A cadeia é montada só a partir de
--fallback-providers/CHATCLI_FALLBACK_PROVIDERS, nessa ordem. O servidor não acrescenta o--provider: coloque você mesmo o primário na frente. O Helm chart e ospec.fallbackdo operator já colocam o primário na frente para você. - Cada entrada usa
CHATCLI_FALLBACK_MODEL_<PROVEDOR>quando definido. Sem ela, o provedor primário (--provider) fica com o modelo do servidor (--model), e qualquer outro provedor roda o próprio modelo padrão: a variável de modelo dele (por exemploOPENAI_MODEL), depois o padrão embutido. Um provedor de fallback nunca recebe o id de modelo do primário. - Um provedor cujo cliente não pode ser criado (sem chave) é pulado com um aviso. A cadeia só é instalada com pelo menos duas entradas restantes; aí o log mostra
Fallback chain initialized. - Ela atende só requisições que não trazem credenciais nem nomeiam provedor ou modelo. A resposta informa o provedor e o modelo que de fato responderam.
- Um
CHATCLI_FALLBACK_PROVIDERSnão vazio é o único interruptor.CHATCLI_FALLBACK_ENABLEDnão é lida pelo servidor, e nem o Helm chart nem o operator a definem; o/configa marca como não lida quando ela está definida.
API gRPC
Serviçochatcli.v1.ChatCLIService (proto: proto/chatcli/v1/chatcli.proto no repositório), mais o padrão grpc.health.v1.Health:
Streaming
StreamPrompt repassa cada fragmento assim que o provedor o emite. A mensagem final (done: true) traz o usage e o stop_reason do provedor. Quando a rota escolhida não faz streaming, a resposta é gerada numa chamada e enviada como um único chunk.
Atribuição da resposta e uso de tokens
SendPrompt, AnalyzeIssue, AgenticStep e a mensagem final do StreamPrompt trazem um TokenUsage, um stop_reason, e o provider e o model que responderam (a cadeia de fallback pode responder com outro provedor que não o padrão):
chatcli connect alimenta o rastreador de custo com esse usage, então o /cost numa conexão remota precifica tokens reais.
RPCs de pipeline
SendPrompt é um proxy do modelo: com chatcli connect, memória, anexos de /context, skills, knowledge e compactação rodam na sua CLI e o servidor só chama o modelo. Os RPCs de pipeline fazem o servidor assumir o turno inteiro, com o mesmo motor que os servidores MCP e ACP via stdio expõem:
- O motor lê o provedor e o modelo padrão de
LLM_PROVIDER/LLM_MODEL, não de--provider/--model. - As sessões ficam no namespace do principal autenticado (
<subject>/<sessão>), então dois chamadores nunca dividem uma conversa por escolherem o mesmo id. - Sem
CHATCLI_SERVER_PIPELINE=trueos RPCs devolvemUnavailable: the pipeline RPCs are not enabled on this server (start it with CHATCLI_SERVER_PIPELINE=true). GetServerInfo.pipeline_enabledinforma se eles estão ativos.
pipeline.enabled: true no chart do servidor. Operator: spec.pipeline.enabled: true na Instance.
RPCs de AIOps
GetAlerts, StreamAlerts, AnalyzeIssue e AgenticStep alimentam o pipeline de remediação do operator. Os alertas são os do K8s watcher:
StreamAlerts
- Com
include_currento stream começa com o que oGetAlertsdevolveria e depois só chegam alertas novos. O watcher deduplica por tipo e objeto dentro da janela, então cada alerta é enviado uma vez. - Heartbeats a cada 15 segundos distinguem um watcher quieto de uma conexão morta. Sem watcher, o stream só traz heartbeats.
- Um assinante que estoura o buffer limitado é derrubado com
ABORTED; reabra cominclude_currentpara ressincronizar.
AnalyzeIssue
Descoberta de recursos
ListRemotePlugins, ExecuteRemotePlugin e DownloadPlugin expõem os plugins instalados no servidor; ListRemoteAgents, GetAgentDefinition, ListRemoteSkills e GetSkillContent expõem seus agentes e skills. O comando /connect dentro do REPL registra os plugins do servidor na sua sessão; veja Conexão Remota.
Health checks
grpc-health-probe -tls-client-cert … -tls-client-key …). A imagem do container traz o grpc-health-probe em /usr/local/bin/grpc-health-probe. Probe grpc do kubelet não faz TLS, por isso o Helm chart usa /healthz e uma checagem TCP.
Métricas
--metrics-port (padrão 9090) serve métricas Prometheus em /metrics (com negociação OpenMetrics) e /healthz. O listener de métricas faz bind em todas as interfaces, independente de CHATCLI_BIND_ADDRESS, e não tem autenticação: coloque firewall ou use --metrics-port 0.
Os coletores de runtime Go e de processo (
go_*, process_*) também são registrados.
Exportação OpenTelemetry (OTLP)
O motor do ChatCLI pode enviar os contadores de sessão para um coletor OpenTelemetry via OTLP/HTTP (JSON), configurado só pelas variáveis padrão do OTel:SendPrompt/StreamPrompt é medido pelas métricas Prometheus acima. Somas exportadas: chatcli.llm.tokens, chatcli.llm.cost, chatcli.context.compactions, chatcli.context.compaction_cost, chatcli.cache.requests, chatcli.cache.storage_cost e, só com OTEL_RESOURCE_ATTRIBUTES contendo chatcli.session=attr, chatcli.session.cost.
Limites e keepalive
O rate limiter é um token bucket por chamador que roda depois da autenticação. A chave é o subject do chamador:
sub do JWT, mtls:<nome>, legacy-token para o token compartilhado (então todos os clientes do token compartilhado dividem um bucket), system quando não há credencial configurada; os RPCs de health usam o endereço do peer. Acima do limite, uma chamada unária ou um stream falha com ResourceExhausted: rate limit exceeded, retry after N seconds e um header retry-after com o mesmo N: o tempo para repor um token, arredondado para cima e nunca abaixo de 1 segundo (1 no padrão de 10 rps).
Keepalive: o servidor aceita pings de cliente a cada 20 segundos ou mais (mesmo sem stream ativo) e manda ping em conexões ociosas a cada 60 segundos, fechando-as depois de 10 segundos sem resposta. O chatcli connect e o operator mandam ping a cada 30 segundos.
Proteção contra SSRF
URLs de provedor enviadas por um chamador (por exemplo--ollama-url, campos base_url, api_base, endpoint, url, host, server_url, realm_url) são checadas antes do uso:
- só
https://;http://exigeCHATCLI_ALLOW_HTTP_PROVIDERS=trueno servidor; - o host (ou cada endereço para o qual ele resolve) não pode estar em
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8,169.254.0.0/16,100.64.0.0/10,::1,fc00::/7,fe80::/10,ff00::/8ou faixas IPv4 mapeadas; não há override para endereços privados; - hostnames de metadata de nuvem (
metadata.google.internal,metadata.goog,instance-data) são recusados.
OLLAMA_BASE_URL) não passa por essa checagem.
Log de auditoria
timestamp, kind (grpc), request_id, action (nome do RPC), actor (user:<subject> ou anonymous), role, ip, client_id, method, resource, result (success, error, denied), duration. Caminho relativo desliga o arquivo com um erro no log. As entradas também vão para o log estruturado, no logger audit.
Logs
O servidor usa um logger estruturado em JSON:- Toda entrada vai para o arquivo rotativo
LOG_FILE(padrão~/.chatcli/app.log;CHATCLI_LOG_FILEé um alias). chatcli serverechatcli gatewaytambém escrevem as mesmas entradas como linhas JSON no stderr sempre que o stderr não é um terminal — container, unit do systemd, pipe — entãodocker logs,kubectl logse coletores de log recebem as recusas de inicialização e tudo o que vem depois sem configuração extra. Num terminal interativo o arquivo é o único destino.CHATCLI_LOG_STDERR=true|falseforça de um jeito ou de outro. O stdout fica livre para os transportes stdio.CHATCLI_ENV=devtroca para o console de desenvolvimento (colorido, no stdout) mais o arquivo; as linhas JSON no stderr não são duplicadas nesse caso.LOG_LEVELdefine o nível. Rotação:LOG_MAX_SIZE(ouCHATCLI_LOG_MAX_SIZE_MB, em MB),CHATCLI_LOG_MAX_BACKUPS(padrão 3),CHATCLI_LOG_MAX_AGE_DAYS(padrão 28),CHATCLI_LOG_COMPRESS(padrãotrue, gzip). São essas as variáveis que ospec.features.logRotationda Instance do operator define; o Helm chart do servidor as define pelo blocologging(20 MB, 3 backups por padrão, para o log caber no volume de dados de 200Mi do pod; veja Logs, health e métricas).
gRPC reflection
O reflection exige a flag--enable-reflection e CHATCLI_GRPC_REFLECTION=true. A flag assume o valor de CHATCLI_GRPC_REFLECTION como padrão, então só a variável já liga (é o que o server.grpcReflection do chart e o spec.server.security.enableReflection da Instance definem). As chamadas de reflection exigem a mesma credencial de qualquer RPC:
Múltiplas réplicas
O gRPC mantém uma conexão HTTP/2 aberta, então um Service ClusterIP prende cada cliente a um pod. Com mais de uma réplica, use um Service headless: os clientes resolvemdns:/// para cada endereço de pod e balanceiam em round-robin. Helm: service.headless: true; o operator muda para headless sozinho quando spec.replicas > 1. Sessões, o banco do hub e a trilha de auditoria são por pod, a menos que o armazenamento seja compartilhado. Com o Helm chart, várias réplicas no volume de sessões ReadWriteOnce padrão só funcionam num nó; veja Rollouts no volume de sessões.
Operando o servidor
Rotacionar o token compartilhado
Rotacionar o token compartilhado
O token é lido na inicialização. Troque e reinicie:
- binário: defina o novo
CHATCLI_SERVER_TOKENe reinicie o processo; - Helm com
server.token:helm upgrade … --reset-then-reuse-values --set server.token="$(openssl rand -hex 32)"; a anotação de checksum do Secret recria os pods; - Helm com
secrets.existingSecret: atualize o Secret e rodekubectl -n chatcli rollout restart deploy/chatcli.
authentication failed até usarem o token novo. Para evitar uma virada brusca, adicione JWTs antes (as duas credenciais funcionam ao mesmo tempo) e aposente o token compartilhado depois.Rotacionar chaves JWT
Rotacionar chaves JWT
- HS256: um segredo assina e verifica; trocar
CHATCLI_JWT_SECRETinvalida todo token emitido no momento do restart. Mantenha os tokens de vida curta. - RS256:
CHATCLI_JWT_PUBLIC_KEYpode ter vários blocos PEM, e um token verificado por qualquer um deles é aceito. Acrescente a chave pública nova ao lado da antiga, reinicie, passe o emissor para a chave privada nova e remova a chave antiga quando os tokens antigos expirarem.
Renovar certificados TLS
Renovar certificados TLS
O certificado e o bundle de CA de clientes são carregados na inicialização. Depois de renovar os arquivos (ou o Secret), reinicie o servidor. O operator recria os pods da Instance quando um Secret referenciado muda; com o Helm chart rode
kubectl rollout restart.Atualizar
Atualizar
- Binário:
/updatedentro do ChatCLI ou o gerenciador de pacotes, e reinicie o servidor. - Imagem: fixe a tag (
ghcr.io/diillson/chatcli:1.212.2) e troque de propósito;latestse move. - Helm:
helm upgrade chatcli oci://ghcr.io/diillson/charts/chatcli --version 1.212.2 -n chatcli --reset-then-reuse-values.
GetServerInfo e chatcli_server_info{version=…} informam a versão em execução.Solução de problemas
Próximos passos
Conexão Remota
Conecte ao servidor
Docker e Kubernetes
Rode o servidor em container ou com Helm
K8s Watcher
Contexto do Kubernetes em todo prompt
K8s Operator
Instances gerenciadas e AIOps