Para o público técnico
Como o serviço funciona
O MathTrail fica entre o chat que a família já usa e o Google Drive dela. O modelo do chat escreve cada problema, e o serviço decide se ele chega à criança. É um programa só: não chama nenhum modelo próprio e não guarda nada de uma requisição para outra.
- Um binário Go no Cloud Run
- Sem estado
- Sem banco de dados
- Sem chamadas próprias a LLM
- MCP 2026-07-28 · MCP Apps
Cliente de chat1 · cliente
- Claude
- ChatGPT
- outros hosts de MCP Apps
O modelo do chat escreve o problema, e o cartão o mostra à criança.
MCP · chamadas de ferramentas por HTTPS
MathTrail no Google Cloud2 · execução
Cloud Run · um binário Go · sem estado
O núcleo — computação pura
- A regra
- Pontuações
- Verificações
- Resolvedor
- Conteúdo
Ao redor: OAuth, o servidor MCP, o cartão, a selagem, os limites.
Drive API v3 · com o próprio token da família
Google Drive3 · armazenamento
MathTrail/mathtrail-profile.json
Um arquivo por criança, no próprio Drive da família — a única coisa guardada a longo prazo.
01
O mapa
Quem fala com quem. Nem a criança nem a pessoa adulta acessam o MathTrail diretamente — falam com o host do chat. Tudo o que está no bloco do meio é parte de um só processo, não um serviço separado. Escolha qualquer parte para ver o que ela faz e com o que se liga.
02
Uma aula, chamada por chamada
O modelo e o cartão chamam as mesmas ferramentas e se apoiam em uma única fonte da verdade — o arquivo do perfil no Drive da família. Para a aula seguir, eles não têm nada a dizer um ao outro.
Problema
- Pessoa adulta
- Cartão
- Modelo do host
- MathTrail
- Drive
1Pessoa adulta → Modelo do host: “um problema novo, por favor”
2Modelo do host → MathTrail: next_task
O host desenha o cartão assim que a chamada começa: “Preparando o próximo problema…”
limites · a regra escolhe o tema, o nível e a dificuldade
3MathTrail → Drive: gravar o pedido aberto
4MathTrail → Modelo do host: o pedido está aberto — busque o pacote
5Modelo do host → MathTrail: get_package
6MathTrail → Modelo do host: briefing · 3 problemas de referência · armadilhas · modelos de resolvedor · exemplos de desenhos
O modelo escreve o problema, um resolvedor em Starlark e uma autoverificação
7Modelo do host → MathTrail: submit_task — até 3 tentativas
verificações · o resolvedor deve encontrar exatamente uma alternativa correta
8MathTrail → Drive: o problema passa a ser o atual, com a resposta selada
9Cartão → MathTrail: read_task — o cartão pergunta a cada 4 s
10MathTrail → Cartão: texto · desenho · cinco alternativas · dica — sem a resposta
Toda chamada também lê o perfil do Drive — essas leituras foram omitidas. O cartão que espera nunca vê o pacote: os problemas de referência do pacote trazem as respostas. Enquanto a criança resolve, o modelo escreve o próximo problema com antecedência, com prepare_task, e quando outro é pedido, next_task o coloca em um cartão novo na hora — sem esta espera.
Resposta
- Pessoa adulta
- Criança
- Cartão
- Modelo do host
- MathTrail
- Drive
1Criança → Cartão: aperta C
2Cartão → MathTrail: submit_answer — o chat não gasta nenhum turno
só a primeira resposta · o selo é aberto agora · θ e δ se movem só pelo acerto ou erro
3MathTrail → Drive: gravar as pontuações e o histórico
4MathTrail → Cartão: certa ou não · a armadilha por trás de C · o desenho da solução · os passos · a pontuação antes → depois
O cartão vira a análise da resposta, no lugar do problema
5Cartão → Modelo do host: ui/update-model-context — uma linha, lida com a próxima mensagem da pessoa adulta
6Pessoa adulta → Modelo do host: “por que um poste a mais?”
7Modelo do host → Pessoa adulta: responde sobre este mesmo problema, completando o cartão
Apertar a alternativa registra a resposta antes de qualquer explicação, e o cartão vira na hora a análise da resposta. O chat não gasta nenhum turno, e o modelo não desenha nada. Ele lê a linha do cartão com a próxima mensagem da pessoa adulta e responde a uma pergunta sobre o problema sem recontar o que o cartão mostra. Se a linha que o host envia ao modelo se perder, o modelo se atualiza na sua próxima chamada.
Login
- Navegador da família
- Host
- MathTrail
- Drive
1Host → MathTrail: POST /mcp, sem token
2MathTrail → Host: 401 · onde estão os metadados
3Host → Navegador da família: abre /oauth/authorize · PKCE S256
4Navegador da família → MathTrail: authorize
Uma tela de permissão própria: qual cliente, e para onde ele vai devolver a pessoa adulta
5Navegador da família → Google: login com o Google · só drive.file
6Google → Navegador da família: de volta, com o código do Google
7Navegador da família → MathTrail: /oauth/callback
8MathTrail → Google: trocar o código
9MathTrail → Drive: /about — de quem é este Drive?
id de usuário = HMAC da conta do Drive · os tokens do Google selados no nosso código
10MathTrail → Navegador da família: 302 · code = mt1.c.…
11Navegador da família → Host: devolve o código
12Host → MathTrail: /oauth/token · código + verificador
13MathTrail → Host: acesso 15 min · atualização 30 dias
A pessoa adulta faz login uma vez. O serviço não guarda nada dessa troca: a requisição viaja em um parâmetro state selado, e os tokens do Google, em um código selado.
O problema atual como máquina de estados
Cada regra de “só uma vez” é um campo do perfil, verificado dentro da mesma leitura, cálculo e gravação. Nada mais se lembra de nada.
nonenext_taskbriefing prontorequestedsubmit_task ✓selado, entregueissuedsubmit_answerregistrada uma vezanswered
requested ↺recusado, ainda há tentativas — no máximo três por pedidorequested → nonetrês recusas, ou 15 minutos desde a abertura do pedido; os problemas do dia não são gastos, e três recusas contam como um pedido malsucedido do diaissued → requested“Outro problema” antes de uma resposta — fica registrado como em branco, e a pontuação não mudaanswered ↺qualquer resposta posterior — de uma segunda aba ou do modelo — recebe de volta o primeiro resultado e não grava nadaanswered → requestedo próximo problema, em um cartão novo; o respondido deixa de ser o atualanswered → issuedo próximo problema foi escrito com antecedência — next_task o coloca em um cartão novo na hora, pulando requestedissued → noneo selo não pode mais ser aberto — a chave passou por duas rotações; nada é registrado
03
Onde fica o estado
Tudo o que um servidor com estado guardaria em um banco de dados fica em outras mãos: em um token selado que o chat guarda, em um cookie no navegador da família ou no próprio Drive da família. Reinicie o serviço ou inicie uma segunda cópia — nada se perde.
O próprio MathTrail
Nada
Nenhuma conta, nenhuma sessão, nenhum cadastro, nenhum token do Google. Cada requisição traz tudo de que precisa.
O armazenamento de tokens do chat
Tokens selados, opacos para o host
- Token de acesso
- 15 min
- Token de atualização
- 30 dias, ≤ 90
Dentro deles vão selados os tokens do Google, um id de usuário opaco para os limites e o país de onde a pessoa adulta fez login.
O navegador da família
Dois cookies, sem sessão
- Clientes aprovados
- 180 dias
- Proteção do login (CSRF)
- 10 min
Perdê-los custa uma tela de permissão a mais, ou um novo login.
O Drive da família
A única coisa guardada a longo prazo
mathtrail-profile.json- até ser excluído
O perfil, as pontuações e o problema atual, com a resposta selada. A pessoa adulta pode abrir o arquivo, e o Drive guarda o histórico dele.
Secret Manager
Leitura única, quando uma instância inicia
- Chaves de selagemdepois de uma rotação, a chave anterior ainda é aceita
- 90 dias
- Chave secreta do cliente do Google
- fixada
A memória da instância
Só caches: perdidos em um reinício, e nada quebra
- Contadores do ritmo de requisições
- minutos
- Onde está o arquivo do perfil
- 10 min
- Documentos dos clientes
- de 5 min a 24 h
O que a v1 deixa de fora de propósito
- Um banco de dados
- Filas
- Um armazenamento de sessões
- Um cache compartilhado
- Um banco de problemas prontos
- Um cliente de API de LLM
- REST e OpenAPI
04
O arquivo do perfil
Uma criança, um arquivo JSON: não um cache de alguma verdade guardada em um servidor, mas a própria verdade. Cada bloco tem um teto, respeitado a cada gravação, então o arquivo nunca cresce sem limite: um arquivo comum fica dentro da meta, e um que esteja perto dos tetos passa dela. Os problemas passados ficam guardados como impressões digitais, não como texto.
O tamanho do arquivo
≈ 61 KB / ≈ 79 KB
| Bloco | O que guarda | Quem grava | Tamanho | |
|---|---|---|---|---|
| Impressões digitais | Até 200 esboços dos problemas entregues, para a verificação de quase repetições — sem texto, sem tema, sem data | next_tasksubmit_task | 27 KB / 29 KB | |
| Histórico da pontuação | Onde estavam θ e o δ do tema antes de cada resposta recente, e θ e cada tema no início de cada um dos últimos 7 dias com respostas | submit_answer | 14 KB / 15 KB | |
| Pontuações e temas | θ e o ponto de partida da criança; para cada tema: δ, as respostas, as armadilhas em que caiu, o domínio e o seu nível | next_tasksubmit_tasksubmit_answer | 8 KB / 14 KB | |
| Respostas recentes | As últimas 20 respostas e problemas em branco — e nunca menos que as 5 respostas que a regra lê | next_tasksubmit_answer | 4 KB / 5 KB | |
| Problema atual, selado | A resposta, a armadilha por trás de cada alternativa errada, a solução e o resolvedor — uma única string opaca | next_tasksubmit_tasksubmit_answer | 4 KB / 8 KB | |
| Problema atual, aberto | O que o cartão mostra: o texto, o desenho, cinco alternativas, a dica — e, depois da resposta, a própria resposta | next_tasksubmit_tasksubmit_answer | 2 KB / 4 KB | |
| Pedido aberto | Enquanto o problema está sendo escrito: o id dele, o briefing, as tentativas usadas e quem escolheu o tema | next_taskprepare_tasksubmit_task | 1,5 KB / 2 KB | |
| Serviço e criança | A versão do esquema, um UUID aleatório, a revisão; o pseudônimo, o ano escolar, os interesses, as anotações; os contadores do dia | save_profileedit_profiletoda gravação | 0,6 KB / 1,5 KB |
Um problema escrito com antecedência fica ao lado do atual e pesa mais ou menos o mesmo. O que o arquivo não guarda: os textos dos problemas passados, a resposta às claras até a criança responder, qualquer coisa que nomeie a pessoa adulta e uma etapa — as etapas são calculadas quando são mostradas. O Drive não tem gravação condicional, então o arquivo é lido de novo antes de salvar, e a gravação é recusada se ele tiver mudado: duas abas nunca sobrescrevem uma à outra em silêncio.
05
O que é selado — e por quanto tempo
Um só mecanismo cumpre duas funções: sela o login dentro dos tokens que o chat guarda, e a resposta de um problema — a do problema atual e a do que foi escrito com antecedência — dentro do perfil. Cada selo leva a sua finalidade, então um token de acesso não pode se passar por um token de atualização, e uma resposta selada não pode se passar por mais nada.
Um token, desmontado
mt1.a.kQ7fWx.5rJ0Yb2mQ8vK1nT4pXc9…
- mt1
- a versão do formato
- finalidade
- c código · a acesso · r atualização · d cliente · s state · k consentimento · t resposta de um problema
- id da chave
- tirado da própria chave, então uma rotação passa despercebida
- texto cifrado
- XChaCha20-Poly1305, um nonce aleatório de 192 bits
Não é um JWT: só o serviço o lê, e nenhum claim pode vazar de uma string opaca para um log.
Validades, em escala logarítmica
- Código de autorização
60 s - Login em andamento
10 min - Token de acesso
15 min - Token de atualização
30 dias deslizantes, no máximo 90 - Chave de selagem
rotação a cada 90 dias - Clientes aprovados
180 dias
Um servidor sem estado não consegue marcar um código como usado, por isso a janela do código é mantida curta. Depois de uma rotação, a chave anterior ainda é aceita, então nem um token nem uma resposta selada se perdem quando a chave muda.