Skip to main content
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 de CHATCLI_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:
Qualquer uma destas opções satisfaz a regra: 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.
A recusa vai para o log estruturado. Num container, numa unit do systemd ou num pipe o servidor também escreve esse log no stderr, então docker logs/kubectl logs mostram a recusa sem nenhuma configuração extra; num terminal interativo ela vai só para o arquivo de log. Veja Logs.

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 como ANTHROPIC_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 gRPC authorization: Bearer <token>. chatcli connect --token faz isso tanto para o token compartilhado quanto para um JWT.
Todo mundo que tem o token é o mesmo principal, 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 recebem Unauthenticated: 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:
O certificado é lido uma vez na inicialização: um certificado renovado só vale depois de reiniciar.

Credenciais do LLM

O servidor chama o modelo com as próprias chaves, a menos que a requisição traga credenciais. Opções do chatcli 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_TOKENS do 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.
As requisições rodam em paralelo, cada uma com seu cliente.

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 o spec.fallback do 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 exemplo OPENAI_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_PROVIDERS não vazio é o único interruptor. CHATCLI_FALLBACK_ENABLED não é lida pelo servidor, e nem o Helm chart nem o operator a definem; o /config a marca como não lida quando ela está definida.
Veja Fallback de provedores para a classificação de erros e os cooldowns.

API gRPC

Serviço chatcli.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):
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=true os RPCs devolvem Unavailable: the pipeline RPCs are not enabled on this server (start it with CHATCLI_SERVER_PIPELINE=true).
  • GetServerInfo.pipeline_enabled informa se eles estão ativos.
O motor é um ChatCLI dentro do processo do servidor: os workers dele (memória, servidores MCP, scheduler) rodam ali e os turnos são serializados, então chamadores simultâneos entram em fila. Ele não divide o processo com o gateway co-localizado: com CHATCLI_SERVER_PIPELINE=true e CHATCLI_GATEWAY_IN_SERVER=true juntos, o gateway roda, o pipeline fica desligado e o log diz Pipeline RPCs disabled: CHATCLI_SERVER_PIPELINE and CHATCLI_GATEWAY_IN_SERVER cannot share one process; run the gateway separately.
Helm: 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_current o stream começa com o que o GetAlerts devolveria 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 com include_current para 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

Os RPCs de health pulam a checagem do bearer, mas com mTLS o handshake TLS ainda exige certificado de cliente (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:
No modo servidor isso cobre os turnos que rodam num motor dentro do processo, ou seja, os RPCs de pipeline e o gateway co-localizado. O tráfego de proxy 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:// exige CHATCLI_ALLOW_HTTP_PROVIDERS=true no 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::/8 ou 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.
A configuração do próprio servidor (por exemplo OLLAMA_BASE_URL) não passa por essa checagem.

Log de auditoria

Cada RPC acrescenta uma linha JSON a uma trilha encadeada por hash e protegida por lock de arquivo (a mesma trilha que o auditor de requisições LLM escreve, então os dois tipos de entrada se intercalam numa única cadeia verificável). Campos: 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 server e chatcli gateway também escrevem as mesmas entradas como linhas JSON no stderr sempre que o stderr não é um terminal — container, unit do systemd, pipe — então docker logs, kubectl logs e 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|false força de um jeito ou de outro. O stdout fica livre para os transportes stdio.
  • CHATCLI_ENV=dev troca para o console de desenvolvimento (colorido, no stdout) mais o arquivo; as linhas JSON no stderr não são duplicadas nesse caso.
  • LOG_LEVEL define o nível. Rotação: LOG_MAX_SIZE (ou CHATCLI_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ão true, gzip). São essas as variáveis que o spec.features.logRotation da Instance do operator define; o Helm chart do servidor as define pelo bloco logging (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).
Nas imagens de container o home é efêmero e a imagem distroless não tem shell para ler um arquivo; lá, o que se lê é o stderr.

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:
Deixe desligado em produção.

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 resolvem dns:/// 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

O token é lido na inicialização. Troque e reinicie:
  • binário: defina o novo CHATCLI_SERVER_TOKEN e 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 rode kubectl -n chatcli rollout restart deploy/chatcli.
Os clientes recebem 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.
  • HS256: um segredo assina e verifica; trocar CHATCLI_JWT_SECRET invalida todo token emitido no momento do restart. Mantenha os tokens de vida curta.
  • RS256: CHATCLI_JWT_PUBLIC_KEY pode 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.
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.
  • Binário: /update dentro 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; latest se 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