Runbook de produção — Render (fase A)

Guia de subida do Render em produção no MyPanel. Nada aqui foi executado: este

documento é o roteiro, o deploy é decisão do dono e o autoDeploy nasce desligado

(D12). Projeto do painel: workspace_nommand (947JSvgPNhtoIM97ko6ie), ao lado dos

outros 9 serviços.

Fonte do desenho: docs/desenho/desenho.md §7 (topologia), §6.2 (env), §8 (migração),

§10 (corte). Onde este runbook e o desenho divergirem, vale o desenho e a divergência

vira correção de um dos dois.


1. Topologia

Serviço (novo)OrigemPortaMemóriaVolumeDomínio
`render-backend-ws``PedroPaduelo/render`, branch `main`, buildPath `/backend`, `Dockerfile`4000 (não publicada)1024 MB`render-prod-data` → `/data``render-v0-api.mp.serendiped.com` → 4000, SSL
`render-frontend-ws`idem, buildPath `/frontend`8080 (não publicada)256 MB—`render-v0.mp.serendiped.com` → 8080, SSL
`render-shot-ws` (opcional)idem, buildPath `/shot`45001024 MB—**sem domínio** (só o backend fala)

deployments_trigger, decisão do dono.

não colide com o sandbox sb-render.

max_connections acima de 1000 e DB_CONNECTION_LIMIT=1000 na app (D11). O

Redis é opcional (só os contadores de rate limit; sem REDIS_URL a contagem é por

instância).

o painel, com SSL provisionado pelo painel.

2. Pré-requisitos (ação do dono, fora do MyPanel)

post-logout do §3 abaixo.

2. CONTA_PROVISIONING_SECRET novo e exclusivo desta app (é o que identifica o

app.contract.cancelled dela) — gerado e guardado fora do repositório.

3. Token de git com acesso a PedroPaduelo/render salvo no painel.

4. Postgres próprio criado, max_connections acima de 1000, credencial guardada fora do

repositório.

5. Entrada no AppSwitcher com slug render e papel apps["render"] liberado na empresa.

Sem isso a app não entra nem para a pessoa nem para o webhook.

3. Variáveis de ambiente (nomes, sem valores)

`render-backend-ws`

Segredos (nunca no repositório, nunca no log): DATABASE_URL, DIRECT_URL,

RENDER_AGENT_TOKEN, JWT_SECRET, MCP_SECRET_KEY, MCP_SECRET_KEY_PREVIOUS,

OIDC_CLIENT_SECRET, CONTA_PROVISIONING_SECRET, SHOT_TOKEN, REDIS_URL.

Configuração: NODE_ENV, PORT, HOST, DB_CONNECTION_LIMIT, AUTH_ENFORCED,

JWT_TTL, OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_REDIRECT_URI,

OIDC_POST_LOGOUT_REDIRECT_URI, APP_PUBLIC_URL, CORS_ORIGIN, FRONTEND_URL,

MCP_ALLOWED_ORIGINS, MCP_ALLOWED_HOSTS, TRUST_PROXY, RATE_LIMIT_MAX,

RATE_LIMIT_WINDOW_MS, RATE_LIMIT_IP_MAX, AUDIT_RETENTION_DAYS,

SHUTDOWN_TIMEOUT_MS, SHUTDOWN_DRAIN_DELAY_MS, SWAGGER_ENABLED, LOG_LEVEL,

PRISMA_LOG_QUERIES, RENDER_ASSETS_DIR, RENDER_REQUIRE_IF_MATCH,

RENDER_REVISION_KEEP, SHOT_SERVICE_URL, SHOT_SERVICE_ALLOWED_HOSTS,

SHOT_TIMEOUT_MS, CHECKPOINT_DISABLE.

Do desenho §7: MCP_ALLOWED_HOSTS recebe o host do domínio da API (o mcpHostGuard

casa request.host), CORS_ORIGIN e APP_PUBLIC_URL recebem o domínio do frontend,

TRUST_PROXY=1, OIDC_REDIRECT_URI é <domínio do frontend>/auth/callback e o

post-logout é o domínio do frontend.

`render-frontend-ws`

BACKEND_UPSTREAM (nome interno do serviço de backend, nunca como default no

Dockerfile), NODE_ENV, PORT, NOMAD_UI_TOKEN (só se o nomad-ui voltar a ser

privado).

`render-shot-ws`

SHOT_ALLOW_ORIGINS (o domínio do frontend), SHOT_TOKEN (o mesmo do backend),

NODE_ENV, PORT.

> Conferência automática: npm run env:example gera backend/.env.example a partir do

> env-schema e o gate env-example-check falha se os dois divergirem. A lista acima

> deve bater com o .env.example commitado.

4. Banco

2. Conferir depois do boot: SHOW max_connections acima de 1000 e, no app,

SELECT * FROM pg_stat_activity com o pool do Prisma dentro do DB_CONNECTION_LIMIT.

3. O schema entra pelo prisma migrate deploy do docker-entrypoint.sh: migration

falhada aborta o boot (não sobe API com schema velho). A migration é aditiva e o

reaplicar é idempotente (provado no render-migration.test.ts).

Verificação: curl -fsS https://render-v0-api.mp.serendiped.com/health responde 200 com

o campo de schema saudável; npm run test:db:drift (em qualquer checkout) confere

migrações × schema.

5. Ordem de subida

#PassoComo verificar
--------------------
1**Banco**: criar o Postgres próprio e a credencial`psql` conecta; `SHOW max_connections` > 1000
2**Backend** (`render-backend-ws`): env vars, deploy, `migrate deploy` no bootlog do boot mostra o `migrate deploy` OK e o `listen` na 4000; `/health` 200; `/metrics` 200; `GET /api/llms` 200
3**Frontend** (`render-frontend-ws`): env vars, deploy`GET https://render-v0.mp.serendiped.com/` 200 e o shell carrega; `GET /api/health` pelo proxy do nginx 200
4**shot** (`render-shot-ws`) **por último**, e só se o preview for usadoo backend loga o health do shot; `SHOT_SERVICE_URL` responde; captura de preview funciona numa tela real

Cada conferência registra o commit do build (o painel mostra) e o hash do bundle do

frontend: com autoDeploy desligado, um Dockerfile alterado sem commit faz o

deployments_trigger construir o commit antigo (desenho §7).

Detalhes de borda que a subida precisa conferir:

502 em produção duas vezes), client_max_body_size 25m, CSP com `style-src

'unsafe-inline' e img-src/media-src com https:, data:, blob:`, e proxy de

/api, /auth/sso, /auth/callback e /mcp na mesma origem.

conferido com npx npm@10.9.4 ci --dry-run (lock fora de sincronia já quebrou deploy).

apk_* da empresa responde o envelope {ok,...}; sem credencial, 401.

6. Rollback

primeiro, depois o frontend). Conferir o commit do build e o hash do bundle voltando

aos anteriores.

2. Banco: as migrations da fase A são aditivas; rollback de código não exige

rollback de schema. Só a remoção de coluna (fase B) exigiria passo próprio.

3. Volume (regra nova, obrigatória): o render-prod-data não volta sozinho. Regras:

(a) antes do corte, backup do volume ou nota de que ele está vazio; (b) o rollback

descarta ou renomeia o volume novo, ou reverte o diretório de assets do AgentPack

ao estado pré-corte; (c) o script de migração tem de ser idempotente sobre o volume

(duas execuções, mesmo resultado de bytes), não só sobre as linhas.

4. Corte não concluído: reabrir a escrita no AgentPack (o AgentPack continua dono dos

assets e do catálogo até o corte). O rollback declarado "descartar o banco novo" não

cobre os bytes — ver §7 do desenho.

7. Pendências e ressalvas verificadas no fechamento da fase A

ItemEstadoOnde
(a) `globalWriteReach` devolvia `undefined` para `design-system-id` (fail-open latente)**corrigido** — `assertDesignSystemWriteAllowed` consulta a dona da linha no banco e responde 403 `GLOBAL_WRITE_NOT_ALLOWED` à chave restrita; teste `tests/auth-design-system-write.test.ts` prova o 403`backend/src/plugins/auth.ts`
(b) Casos de isolamento da borda R05–R08 sem teste**fechados** — 4 testes novos em `tests/auth-edge.test.ts`, cada um derrubando a mutação correspondente (log em `.sbcache/wf-render-build/fechamento/logs/mutacoes.log`)`backend/tests/auth-edge.test.ts`
(c) `tests/shutdown.test.ts` do AgentPack não portado (`shutdown.ts`/`server.ts` sem cobertura direta; R13 e R14 sobreviveram às mutações)**pendente registrado** — portar na fase em que o P13 migrar a suíte—
(d) `DB_CONNECTION_LIMIT` 1000**parcialmente fechado** — o teste existe (`tests/config-env.test.ts`, default 1000, inteiro positivo, vence o `connection_limit` da URL, e `withConnectionLimit`); o que nenhum teste cobre é o `max_connections` do **servidor**, que só a verificação do §4 confere§4 deste runbook
(e) Testes que não provam o que dizem (R02, R15, R19, `/mcp`)**pendente registrado** — R02 (`RENDER_AGENT_TOKEN` podendo repetir `MCP_SECRET_KEY`), R15 (header `X-Content-Type-Options`), R19 (`CMD` sem `exec` no entrypoint) e o caso do `/mcp` continuam sem teste que derrube a mutação—
(f) `organization_id` em `audit_events` e no ator bootstrap**feito pelo P1, com uma decisão**: a coluna existe, o `auditService` grava a empresa (do principal, do projeto escrito ou da escrita) e 19 testes cobrem; o **ator bootstrap continua sem empresa por decisão do P1** (chave de plataforma): fora de projeto a linha de auditoria fica sem empresa. O CHECK `<tabela>_one_owner` impede linha de catálogo sem empresa`backend/prisma/schema.prisma`, `backend/src/services/audit.service.ts`
Piso de cobertura de `src/services` do P4 baixado 93 → 84 (medido 84,27%)registrado no merge do P4`.sbcache/wf-render-build/P4/`
Dry-run de upgrade manda texto de `agentpack_task_decision_create` (tool que não existe na app)pendente do P4, vem com o P7 (tela de decisão nova)—
`isOwnedBy` (render-db.ts) e o guard `kind !== 'manifest'` sobreviveram a mutação no P4pendente do P4: teste `getAssetTx`/`deleteAssetTx` com escopo global de A sobre asset de B → `NOT_FOUND`—
P5 (rotas REST) fora desta entrega — 2 gates falhando (`knip`, `coverage-floor`)fora do escopo deste fechamento`.sbcache/wf-render-build/P5/`

8. Fase B — o que a fase B exige do AgentPack (nada feito agora)

ItemO que éQuando
**P-1**Mover `toolError` de `tools/render/common.ts` para `mcp/tool-error.ts` (usado por `mcp/tool-call.ts` e `mcp/proxy-call.ts`, hoje reexportado pelo módulo do Render). Sem isso, apagar a pasta do Render derruba **toda** tool, inclusive as proxiedantes de tudo, sem mudança de comportamento
**P-2**`organizationId` em `McpServer` (catálogo de MCP por empresa) + normalização do prefixo upstream em `proxiedWireName`antes de cadastrar o MCP do Render
**D6**Cadastrar o `/mcp` do Render no proxy do AgentPack com `toolPrefix: agentpack_render` — **só depois do P-2** (senão os 59 nomes viram `agentpack_render_agentpack_render_*`). Ressalvas aceitas: a tool proxied recebe escopo `write` e não `render:write`; a identidade do agente não atravessa; toda chamada proxied vira linha de auditoria de escrita; `X-AgentPack-Toolsets` e `X-AgentPack-Project` precisam atravessar por header fixo no vínculono corte
**D7**Skills `agentpack-render-*` ficam no AgentPack na fase 1. **Ressalva**: se a ponte for desativada e o registro do Render sair do AgentPack, as 7 skills passam a citar tools inexistentes e o `check:skills` quebra — reescrever ou aposentar nesse momentono corte
**D13**Migração dos dados de produção: **só o script e este runbook** (`backend/scripts/migrate-render-from-agentpack.ts`, `--dry-run` obrigatório, `--apply` explícito, `--confirm-production` para sair do dry-run, log com contagens e nunca com conteúdo). **Não executar contra produção** sem o donofase B
**Corte**Remover `RENDER_TOOLS` do registro do AgentPack (depois do P-2), normalizar o prefixo e cadastrar o MCP do Render. Os 4 pontos que quebram o AgentPack no dia da remoção e viram preparo antes: `toolError` em `tools/render/common.ts` (P-1), `appBaseUrl` em `lib/render-view-url.ts` (usado por `task-decision.service.ts`), as 3 tools `project_*` morando no registro do Render e as 2 chaves de `rate-limit.ts` cujo boot falha se a rota sumirfase B
Complemento no app novo`server.json` publicando o `/mcp` da app, docs `llms-full.txt`/`docs/mcp/tools.md` próprios (P13), e a tela de decisão de upgrade (D2, P7) — o path humano do portão continua REST sem `decisionId`fase A restante / B