Configurei um assistente virtual para me ajudar com diversas atividades no meu dia a dia, desde a calcular o atraso dos meus projetos observando meus cronogramas no MS Project, até trazer as notícias do dia, registrar meu peso e treinos da academia. Utilizo o Openclaw para tal feito e hoje vou falar um pouco da arquitetura por trás.
No meu servidor pessoal uso Kubernetes para gerenciar meus containers, K3s mais especificamente. Não porque Kubernetes seja melhor para rodar em single nodes, não é na verdade. Mas me ajuda nos meus estudos de aplicações cloud-native.
A topologia
Enxergo essa topologia em 3 partes:
- Os canais que uso para me comunicar com o Agente
- As bases de dados que ele acessa
- O Openclaw e sim com suas funcionalidades.
Hoje não vou compartilhar as skills que utilizo, mas nelas está as especificações de como manipular as bases de dados que utilizo para manter meus dados

K3s
strategy: Recreate, não RollingUpdate. O openclaw utiliza SQLite no filesystem para manter seu estado. Rolling update sobe o pod novo antes de matar o velho, dois processos disputando o mesmo banco. Daí utilizo recreate, uma réplica, derruba e sobe.
hostPath, ao invés de PVC. Parece totalmente gambiarra e eu sei. A justificativa é prática para meu dia a dia: o workspace do agente é um diretório que eu edito com as mãos via acesso remoto no vscode. Arquivos de contexto, skills, notas de memória. Com hostPath eu abro por SSH ou VS Code Remote, faço commit no git do próprio host e pronto. Enfiar isso num PVC significaria trabalho a mais para um homelab
Modelos LLM
"model": {
"primary": "zai/glm-5.2",
"fallbacks": ["kimi/kimi-for-coding", "kimi/k3", "openrouter/free"]
},
"utilityModel": "openrouter/free",
"imageModel": "openrouter/free"
GLM-5.2 como primário por causa do custo, na época peguei uma ótima promoção de lançamento. Se cair, Kimi Code assume; depois Kimi K3; e no fim um modelo gratuito do OpenRouter, que é ruim mas é melhor do que nada.
O utilityModel é uma economia que quase ninguém configura. Título de sessão, narração de progresso, resumo curto, o OpenClaw dispara essas tarefas o tempo todo, e não há motivo para pagar modelo de raciocínio para gerar uma string de título de sessões. Aponto para um modelo gratuito e pronto.
Memória
Configurei três camadas empilhadas que resolvem problemas diferentes.
1. LCM para contexto
O comportamento padrão de qualquer agente quando o contexto enche é resumir o começo da conversa e jogar o original fora. Com isso, eles não são capazes de responder algo do tipo: "qual era o IP daquele servidor que eu te passei semana passada?".
O LCM substitui isso por um DAG de sumarizações em SQLite. Toda mensagem é persistida.
2. memory-core com dreaming
"memory-core": {
"enabled": true,
"config": {
"dreaming": {
"enabled": true,
"frequency": "0 3 * * *",
"timezone": "America/Sao_Paulo"
}
}
}
Três da manhã, todo dia, ele passa o material recente de curto prazo, pontua o que merece durar e promove para memória de longo prazo. Consolidação em background, sem eu pedir.
3. memory-wiki em modo bridge
"memory-wiki": {
"enabled": true,
"config": {
"vaultMode": "bridge",
"bridge": {
"enabled": true,
"readMemoryArtifacts": true,
"indexDailyNotes": true,
"indexDreamReports": true,
"followMemoryEvents": true
}
}
}
Uma wiki própria independente da memória do agente. Ele lê os artefatos que o memory-core exporta e compila as página. Deixa de ser algo do tipo: "Eu acho que X" para ser "Eu acho que X porque em 12/04 você disse Y".
E a busca por cima de tudo
"memorySearch": {
"provider": "openai-compatible",
"model": "nvidia/llama-nemotron-embed-vl-1b-v2:free",
"experimental": { "sessionMemory": true },
"sources": ["memory", "sessions"]
}
"sessions" em sources é o detalhe que mais mudou meu dia a dia. Não é só a memória curada que é pesquisável, o histórico bruto de conversa também é.
"O que a gente decidiu sobre o particionamento daquele tópico Kafka" encontra a conversa em si, não a versão resumida dela.
Outras configurações que uso bastante
Áudio responde áudio. "tts": { "auto": "inbound" }. Com inbound, se eu mando mensagem de voz no Telegram ele responde falando; se eu digito, ele digita.
O agente como endpoint OpenAI. gateway.http.endpoints.chatCompletions.enabled expõe /v1/chat/completions. Qualquer app que fala com a API da OpenAI passa a falar com o meu agente. Assim utilizo ele como "modelo LLM" para minhas aplicações financeiras, opencode e qualquer aplicação que peça por um modelo openai-compatible.
Limitações que eu configurei
- Allowlist no Telegram (
dmPolicy: "allowlist") erequireMentionem grupos. Ele ignora quem não é eu. trustedProxiesrestrito à rede de pods do K3s.allowedOriginsexplícito no Control UI.denyCommandsnos nodes pareados: câmera, gravação de tela, SMS, contatos e agenda ficam fora, mesmo com o celular pareado.- Skills por exclusão explícita: tudo que não uso está
enabled: falseno arquivo, não só ausente. - Zero segredo no config: tudo vem de secrets do Kubernetes que nunca entra no git.
Os arquivos
Deploy do kubernetes
# Pré-requisito: um Secret 'openclaw-secret' no namespace, criado FORA do git:
# kubectl -n openclaw create secret generic openclaw-secret \
# --from-literal=TELEGRAM_BOT_TOKEN=... \
# --from-literal=OPENROUTER_API_KEY=... \
# --from-literal=ZAI_API_KEY=... \
# --from-literal=KIMI_API_KEY=...
apiVersion: v1
kind: Namespace
metadata:
name: openclaw
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
namespace: openclaw
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: gateway
template:
metadata:
labels:
app: gateway
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: gateway
image: local/openclaw:1.6.0
imagePullPolicy: IfNotPresent
# 'command' do compose = args do entrypoint (exec "$@")
args: ["openclaw", "gateway", "--allow-unconfigured"]
ports:
- containerPort: 18789
env:
- name: NODE_ENV
value: production
- name: TZ
value: America/Sao_Paulo
- name: OPENCLAW_TZ
value: America/Sao_Paulo
- name: OPENCLAW_GATEWAY_BIND
value: lan
- name: OPENCLAW_HEADLESS
value: "true"
- name: OPENCLAW_DEBUG
value: "0"
# --- LCM (Lossless Context Management) ---
- name: LCM_ENABLED
value: "true"
- name: LCM_FRESH_TAIL_COUNT
value: "32" # últimas 32 mensagens nunca são compactadas
- name: LCM_INCREMENTAL_MAX_DEPTH
value: "3" # condensação em cascata até 3 níveis
- name: LCM_CONTEXT_THRESHOLD
value: "0.75" # compacta a 75% da janela, deixando folga p/ a resposta
envFrom:
- secretRef:
name: openclaw-secret
volumeMounts:
- name: home
mountPath: /home/ubuntu
- name: config
mountPath: /home/ubuntu/.openclaw
- name: skills
mountPath: /home/ubuntu/.openclaw/skills
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
volumes:
- name: home
hostPath:
path: /homelab/openclaw/home
- name: config
hostPath:
path: /homelab/openclaw/config
- name: skills
hostPath:
path: /homelab/openclaw/skills
---
apiVersion: v1
kind: Service
metadata:
name: gateway
namespace: openclaw
spec:
selector:
app: gateway
ports:
- port: 18789
targetPort: 18789
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: openclaw
namespace: openclaw
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: agente.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gateway
port:
number: 18789openclaw.json — configuração completa
{
"//": "openclaw.json — versão sanitizada. Todo segredo virou ${VAR} vindo do Secret.",
"agents": {
"defaults": {
"workspace": "/home/ubuntu/.openclaw/workspace",
"models": {
"zai/glm-5.2": { "alias": "GLM" },
"kimi/k3": { "alias": "Kimi K3" },
"kimi/k3[1m]": { "alias": "Kimi K3 1M" },
"kimi/kimi-for-coding": { "alias": "Kimi Code" },
"openrouter/auto": { "alias": "OpenRouter" },
"openrouter/free": { "alias": "OpenRouter Free" }
},
"//model": "Cadeia de failover explícita. Se o primário cai, a conversa continua.",
"model": {
"primary": "zai/glm-5.2",
"fallbacks": ["kimi/kimi-for-coding", "kimi/k3", "openrouter/free"]
},
"//utility": "Título de sessão, narração de progresso, visão: tarefa barata não precisa do modelo caro.",
"imageModel": "openrouter/free",
"utilityModel": "openrouter/free",
"//memorySearch": "Busca híbrida (vetor + keyword) sobre memória E histórico de sessões.",
"memorySearch": {
"provider": "openai-compatible",
"model": "nvidia/llama-nemotron-embed-vl-1b-v2:free",
"remote": {
"baseUrl": "https://openrouter.ai/api/v1/",
"apiKey": "${OPENROUTER_API_KEY}",
"headers": {
"HTTP-Referer": "https://example.com",
"X-OpenRouter-Title": "OpenClaw"
}
},
"experimental": { "sessionMemory": true },
"sources": ["memory", "sessions"]
},
"//heartbeat": "Turno autônomo periódico. Janela ativa: fora dela ele não acorda.",
"heartbeat": {
"every": "2h",
"target": "none",
"activeHours": {
"start": "08:00",
"end": "22:00",
"timezone": "America/Sao_Paulo"
}
},
"//bootstrap": "Padrão é 20k/60k. Subi porque meus arquivos de contexto são longos.",
"bootstrapMaxChars": 50000,
"bootstrapTotalMaxChars": 300000,
"contextInjection": "always",
"blockStreamingDefault": "on",
"blockStreamingBreak": "message_end"
},
"list": [
{
"id": "main",
"identity": {
"name": "Clarke",
"emoji": "🛰️",
"theme": "Direto, técnico quando precisa, humano quando dá"
}
}
]
},
"gateway": {
"mode": "local",
"auth": { "mode": "token", "token": "${OPENCLAW_GATEWAY_TOKEN}" },
"port": 18789,
"bind": "lan",
"tailscale": { "mode": "off", "resetOnExit": false },
"//controlUi": "Origens explícitas. Sem curinga.",
"controlUi": {
"allowInsecureAuth": true,
"allowedOrigins": [
"https://openclaw.example.com",
"http://localhost:18789",
"http://127.0.0.1:18789"
]
},
"//trustedProxies": "10.42.0.0/16 é a rede de pods do K3s — só o Ingress pode afirmar quem é o cliente.",
"trustedProxies": ["127.0.0.1", "172.0.0.0/8", "10.42.0.0/16"],
"//nodes": "Nodes pareados podem muita coisa. Câmera, SMS, contatos e agenda ficam de fora.",
"nodes": {
"denyCommands": [
"camera.snap", "camera.clip", "screen.record",
"contacts.add", "calendar.add", "reminders.add",
"sms.send", "sms.search"
]
},
"//http": "Expõe o agente inteiro (com memória e ferramentas) como se fosse um modelo OpenAI.",
"http": { "endpoints": { "chatCompletions": { "enabled": true } } }
},
"//session": "Cada par canal+remetente tem sessão isolada.",
"session": { "dmScope": "per-channel-peer" },
"tools": {
"profile": "full",
"sessions": { "visibility": "agent" },
"web": {
"search": { "provider": "tavily", "enabled": true },
"fetch": { "enabled": true }
}
},
"plugins": {
"entries": {
"zai": { "enabled": true },
"kimi": { "enabled": true },
"openrouter": { "enabled": true },
"tavily": { "enabled": true },
"telegram": { "enabled": true },
"//memory-core": "Dreaming: consolidação de memória de curto → longo prazo, 03:00 todo dia.",
"memory-core": {
"enabled": true,
"config": {
"dreaming": {
"enabled": true,
"frequency": "0 3 * * *",
"timezone": "America/Sao_Paulo"
}
}
},
"//memory-wiki": "Bridge: compila os artefatos do memory-core numa wiki com proveniência.",
"memory-wiki": {
"enabled": true,
"config": {
"vaultMode": "bridge",
"bridge": {
"enabled": true,
"readMemoryArtifacts": true,
"indexDailyNotes": true,
"indexDreamReports": true,
"followMemoryEvents": true
}
}
},
"workboard": { "enabled": true, "config": {} },
"device-pair": {
"enabled": true,
"config": { "publicUrl": "https://openclaw.example.com" }
}
}
},
"//models": "mode: merge — acrescenta providers sem apagar os embutidos. Catálogo abreviado aqui.",
"models": {
"mode": "merge",
"providers": {
"zai": {
"baseUrl": "https://api.z.ai/api/coding/paas/v4",
"api": "openai-completions",
"models": [
{
"id": "glm-5.2",
"name": "GLM-5.2",
"reasoning": true,
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 131072
}
]
},
"kimi": {
"baseUrl": "https://api.kimi.com/coding/",
"api": "anthropic-messages",
"models": [
{
"id": "kimi-for-coding",
"name": "Kimi Code",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 262144,
"maxTokens": 32768
},
{
"id": "k3[1m]",
"name": "Kimi K3 (1M)",
"reasoning": true,
"thinkingLevelMap": { "xhigh": "max", "max": "max" },
"input": ["text", "image"],
"contextWindow": 1048576,
"maxTokens": 32768
}
]
}
}
},
"channels": {
"telegram": {
"enabled": true,
"botToken": { "source": "env", "provider": "default", "id": "TELEGRAM_BOT_TOKEN" },
"//policy": "Allowlist. O bot ignora qualquer um que não seja eu.",
"dmPolicy": "allowlist",
"allowFrom": ["<TELEGRAM_USER_ID>"],
"groupAllowFrom": ["<TELEGRAM_USER_ID>"],
"groups": { "*": { "requireMention": true } }
}
},
"//skills": "Allowlist por exclusão: tudo que não uso fica desligado explicitamente.",
"skills": {
"install": { "nodeManager": "npm" },
"entries": {
"1password": { "enabled": false },
"camsnap": { "enabled": false },
"obsidian": { "enabled": false },
"spotify-player": { "enabled": false },
"trello": { "enabled": false },
"//": "... demais skills não utilizadas, todas enabled: false"
}
},
"//mcp": "Servidores MCP próprios. Autenticação por header, valor vindo do Secret.",
"mcp": {
"servers": {
"crm": {
"transport": "streamable-http",
"url": "https://crm.example.com/mcp",
"headers": { "Authorization": "Bearer ${CRM_API_TOKEN}" }
},
"jobsync": {
"transport": "streamable-http",
"url": "https://jobs.example.com/api/mcp",
"headers": { "Authorization": "Bearer ${JOBSYNC_TOKEN}" }
},
"web-search-prime": {
"transport": "streamable-http",
"url": "https://api.z.ai/api/mcp/web_search_prime/mcp",
"headers": { "Authorization": "Bearer ${ZAI_API_KEY}" },
"toolFilter": { "include": ["*"] }
},
"web-reader": {
"transport": "streamable-http",
"url": "https://api.z.ai/api/mcp/web_reader/mcp",
"headers": { "Authorization": "Bearer ${ZAI_API_KEY}" },
"toolFilter": { "include": ["*"] }
}
}
},
"//tts": "auto: inbound — se eu mando áudio, ele responde em áudio. Se eu digito, ele digita.",
"messages": {
"tts": {
"auto": "inbound",
"provider": "openrouter",
"providers": {
"openrouter": {
"apiKey": "${OPENROUTER_API_KEY}",
"model": "hexgrad/kokoro-82m",
"speakerVoice": "pm_alex",
"responseFormat": "mp3"
}
}
},
"ackReactionScope": "group-mentions"
},
"//commitments": "Follow-ups inferidos da conversa. Teto de 3/dia para não virar cobrança.",
"commitments": { "enabled": true, "maxPerDay": 3 },
"hooks": {
"internal": {
"enabled": true,
"entries": {
"session-memory": { "enabled": true },
"compaction-notifier": { "enabled": true }
}
}
},
"//commands": "Comandos de operador só do meu ID.",
"commands": { "ownerAllowFrom": ["telegram:<TELEGRAM_USER_ID>"] }
}
Geração da imagem: Dockerfile
# syntax=docker/dockerfile:1.7
FROM ubuntu:24.04
SHELL ["/bin/bash", "-c"]
ENV DEBIAN_FRONTEND=noninteractive
# ─── Versões (pinadas) ───────────────────────────────────────────────────────
ARG NVM_VERSION=v0.40.4
ARG NODE_VERSION=24.15.0
ARG YQ_VERSION=v4.44.3
# ─── 1. Pacotes de sistema ───────────────────────────────────────────────────
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
git wget curl ssh openssh-client ca-certificates gnupg lsb-release \
vim less tree tmux file procps htop jq fzf ripgrep bat unzip zip \
iproute2 net-tools iputils-ping iputils-tracepath inetutils-telnet \
tcpdump netcat-openbsd \
build-essential pkg-config gfortran \
openjdk-21-jre-headless \
python3 python3-pip python3-venv python3-dev \
libffi-dev libssl-dev libxml2-dev libxslt1-dev \
libblas-dev liblapack-dev libgomp1 \
libfreetype6-dev libpng-dev libjpeg-dev \
ffmpeg sox libsox-fmt-all portaudio19-dev python3-pyaudio \
espeak espeak-data libespeak-dev psmisc lsof
ENV NVM_DIR=/usr/local/nvm
ENV PATH=$NVM_DIR/versions/node/v${NODE_VERSION}/bin:$PATH
RUN mkdir -p "$NVM_DIR" \
&& curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash \
&& . "$NVM_DIR/nvm.sh" \
&& nvm install "${NODE_VERSION}" \
&& nvm alias default "${NODE_VERSION}" \
&& ln -sf "$NVM_DIR/versions/node/v${NODE_VERSION}/bin/"* /usr/local/bin/ \
&& node --version && npm --version
# ─── 4. GitHub CLI (gh) ──────────────────────────────────────────────────────
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh
# ─── 5. yq ───────────────────────────────────────────────────────────────────
RUN curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_$(dpkg --print-architecture)" \
-o /usr/local/bin/yq \
&& chmod +x /usr/local/bin/yq
# ─── 6. Bibliotecas Python ───────────────────────────────────────────────────
RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install --break-system-packages \
"openai>=2" "httpx>=0.27.0" langchain langchain-community \
numpy pandas polars pyarrow scipy sympy statsmodels \
scikit-learn xgboost lightgbm tensorflow-cpu \
matplotlib seaborn plotly altair jupyterlab ipython nbformat \
boto3 awsiotsdk paho-mqtt scapy netaddr paramiko websockets \
requests aiohttp \
openpyxl xlsxwriter python-docx pyyaml toml orjson \
beautifulsoup4 lxml tabulate pillow \
sqlalchemy psycopg2-binary \
edge-tts pyttsx3 pydub soundfile librosa \
mpxj JPype1 \
tqdm rich typer click python-dotenv
# ─── 7. Ferramentas npm globais ──────────────────────────────────────────────
RUN --mount=type=cache,target=/root/.npm \
npm install -g \
@google/gemini-cli \
openclaw@latest \
tsx typescript ts-node \
eslint prettier \
tldr how-2
# ─── Metadados OCI ───────────────────────────────────────────────────────────
LABEL org.opencontainers.image.title="OpenClaw — mfs.eng.br" \
org.opencontainers.image.authors="muller@mfs.eng.br" \
org.opencontainers.image.version="1.0.0" \
org.opencontainers.image.description="OpenClaw"
# ─── Runtime ─────────────────────────────────────────────────────────────────
EXPOSE 18789 8888
STOPSIGNAL SIGTERM