O documento completo do desenho, renderizado do markdown. Arquitetura, stack, autenticacao, banco, rotas, tools, deploy, migracao, riscos e as 28 criticas respondidas.
Desenho do workflow wf-render, revisão final 2026-10-13. Sandbox agent-package-audit, base lida origin/main 26268c0 (é o que está no ar em agentpack-backend-ws).
Mapas que alimentam: /workspace/.sbcache/wf-render/mapa/{render-backend,render-frontend,render-acoplamento,padroes-infra}.md. Críticas que este texto responde: /workspace/.sbcache/wf-render/desenho/critica-arquitetura.json e critica-paridade.json (28 objeções). Resposta item a item na §14 Críticas e respostas.
Nada foi escrito no repo do AgentPack, nada foi criado no painel, nada foi buildado. Isto é planta, não execução.
| O que entra na app nova | 43% do backend (108 arquivos, 28.749 linhas + 30.048 de snapshot) e 41% do frontend (196 arquivos, 32.473 linhas) do AgentPack: 47 rotas REST, 59 tools MCP, 8 tabelas, 6 envs, o serviço shot/ |
| O que fica no AgentPack | projetos, decisões humanas de task, chaves apk_*, o núcleo Fastify/Prisma/MCP, o catálogo de skills, a auditoria |
| O que NÃO vai | nada do core (tasks, knowledge, memories, skills, mcps, search, portal de markdown), o proxy de MCP externo, o catálogo de MCP cadastráveis, prompts/resources do /mcp |
| Compatibilidade | mesmos caminhos REST (/api/render/**, /api/projects/:projectId/render/**, /api/designs/**, /api/library/**), mesmos nomes de wire agentpack_render_*, gateway /mcp próprio |
| O que trava a virada | 4 FKs para projects, o portão de upgrade que lê task_decisions, o toolError do gateway morando em tools/render/common.ts, o catálogo global sem escopo de empresa, e os arquivos no volume /data/render-assets |
Correções da revisão 1 → 2 (todas verificadas no código, detalhe na §14): a contagem de rotas é 47, não 54; o portão de upgrade não é lido pelo frontend (só pelo agente); quatro arquivos de carga viva estavam fora de qualquer pacote (lib/render-legacy-catalog.ts, services/render-agent-bundle.service.ts, tools/render/design-systems.ts, mcp/prompts.ts + mcp/resources.ts); e o prefixo de wire é aplicado uma vez só, o que obriga uma correção no catálogo do AgentPack para a ponte funcionar.
┌─────────────── navegador ────────────────┐
│ agentpack-v0.mp.serendiped.com │
│ render-v0.mp.serendiped.com (NOVO) │
└───────┬──────────────────────────┬───────┘
│ │
┌────────────────▼──────┐ ┌────────────▼───────────────────┐
│ agentpack-frontend-ws │ │ render-frontend-ws (NOVO) │
│ nginx :8080 · 256 MB │ │ nginx :8080 · 256 MB │
│ proxy /api /mcp │ │ proxy /api /mcp /auth/sso │
│ item Render vira link │ │ CSP + resolver do container │
└───────┬───────────────┘ └──────┬──────────┬──────────────┘
│ │ │
┌────────────────▼──────────────┐ ┌──────────▼──────┐ │
│ agentpack-backend-ws :4000 │ │ render-backend-ws :4000 (NOVO) │
│ 1024 MB · volume /data │ │ 1024 MB · volume /data │
│ projetos · tasks · decisões │ │ Fastify 5 + Prisma 6 + zod 3 │
│ chaves apk_* · gateway /mcp │ │ /mcp próprio (59 tools) │
│ auditoria · skills │ │ SSO Conta (client "render") │
└───┬───────────────┬───────────┘ └─┬────────┬─────┴──────────────┘
│ │ │ │ (opcional)
│ │ ┌───────▼─────┐ │ ┌──────────────┐
│ │ │ Postgres │ └──▶│ shot-ws │
│ │ │ do Render │ │ Chromium 1GB│
│ │ │ 8 tabelas │ │ /shot /health│
│ │ │ render_* │ └──────────────┘
│ │ │ + espelhos │ abre
│ │ │ users/orgs/ │ /render/preview/**
│ │ │ keys/audit │ (credencial apv_*
│ │ │ + approvals │ → cookie de preview)
│ │ └─────────────┘
│ │
│ └─── POST https://render-v0-api/mcp (MCP cadastrado,
│ prefixo de wire `agentpack_render`, opcional)
│
├──▶ Conta Nommand (OIDC, webhook de provisionamento, resync humano)
└──▶ Postgres do AgentPack (projetos + task_decisions, hoje em conjunto)
── fase A (entrega) ────────────────────────── ── fase B (corte) ─────────────────
Render mora aqui dentro Render mora sozinho; aqui vira ponte
diagrama · fonte mermaid
flowchart TB
subgraph CONTA["Conta Nommand — fonte única"]
OIDC[OIDC/JIT]
WH[webhook de provisionamento]
end
subgraph AP["AgentPack (fica)"]
APB[backend :4000]
APF[frontend :8080]
MCPGW[gateway /mcp<br/>+ catálogo MCP com organizationId]
PROJ[(projects)]
DEC[(task_decisions)]
KEYS[(agent_keys apk_*)]
SK[(skills agentpack-render-*)]
end
subgraph RD["Render (app nova)"]
RDB[backend :4000 · /mcp próprio]
RDF[frontend :8080 · Studio]
RSH[shot/ Chromium]
RPR[(render_projects<br/>espelho com id do AgentPack)]
RAP[(render_approvals<br/>portão do upgrade)]
RDB2[(render_* 8 tabelas<br/>+ users/orgs/keys/audit)]
end
OIDC --> APB
OIDC --> RDB
WH --> APB
WH --> RDB
APB --> PROJ
APB --> DEC
APB --> KEYS
APB --> SK
APF -.->|"link externo"| RDF
MCPGW -.->|"MCP cadastrado (fase B)"| RDB
APB -->|"espelho de projeto: webhook<br/>+ DELETE /purge autenticado"| RPR
RDB --> RPR
RDB --> RDB2
RDB --> RAP
RDF --> RDB
RDB -->|"POST /shot x-shot-token"| RSH
RSH -->|"/render/preview/** + cookie de preview"| RDF
Repo novo privado: PedroPaduelo/render (já existe, só com README.md). Mesma anatomia do AgentPack, porque três geradores dependem do layout relativo: backend/scripts/gen-render-contract-types.ts escreve frontend/src/render/contract.generated.ts e expr.generated.ts; 4 testes do frontend importam ../backend/src/services/render-starter-kit/index e ../backend/schemas/render/expr-v1.fixtures.json; render-security-engine.test.ts importa ../../frontend/src/render/engine/template. Sem backend/ ao lado de frontend/, esses caminhos mudam e os gates caem.
| Caminho | Ação | Observação |
|---|---|---|
AGENTS.md + CLAUDE.md | copiar e adaptar | sandbox render (20 GB), banco local, sem .github (decisão do dono de 2026-10-02), lista de comandos. Duas regras novas: (a) mudou documento de contrato → npm run contracts:gen no backend e commitar os 2 arquivos do frontend; (b) SIZE_EXCEPTIONS são medidas no app novo, nunca copiadas do AgentPack |
README.md | escrever | app nova, não "hub de contexto" |
CHANGELOG.md | copiar, começa em 0.1.0 | Keep a Changelog 1.1.0 |
docs/ | copiar o esqueleto, sem o do Render | ADRs 010/011/012/013 + 0014 (versionamento de design system, é do Render) |
llms.txt, llms-full.txt | symlink para backend/public/ | gerados por npm run docs:gen |
server.json | copiar e trocar host/descrição | só se a app tiver /mcp próprio — recomendação: sim, o app novo publica os 59 tools no wire |
frontend/src/api/generated/ | não existe | o Render não usa client gerado (as 48 operações são à mão). O api:check, se existir, valida contra o spec gerado em build, nunca contra um arquivo commitado |
.nvmrc (24), .gitignore, .gitleaks.toml, .gitleaksignore (nasce vazio), .pre-commit-config.yaml, renovate.json | copiar | sem workspaces, um lock por pacote |
docker-compose.test.yml | copiar | name: render-test, banco render_test, portas próprias |
HANDOVER.md, .validation-evidence/ | não copiar | histórico do AgentPack |
| Peça | Versão |
|---|---|
| Node | ^24.11.0, imagem node:24.21.0-alpine3.24@sha256:ebfe2f90… |
| Fastify | 5.12.5 |
| Prisma CLI e client | 6.19.3 (iguais) |
| zod | 3.25.76 (o frontend está no 4; o contrato do Render é gerado do zod 3) |
| fastify-type-provider-zod | 4.0.2 |
| MCP SDK | 1.30.1 (servidor do /mcp) |
| tailwindcss 3.4.19 + postcss 8.5.28 | só do Render: compilação de props.cls no servidor |
| jsdom | 26.1.0 — só services/render-bundle/asset-files.ts (SVG passivo) |
| jsonwebtoken | token de preview apv_* e sessão |
| Jest 30.4.2 / @swc/jest / TS 5.9.3 | idem |
React 19.3.0 · Vite 8.3.1 · @vitejs/plugin-react 6.1.1 · TypeScript ~6.0.3 · Tailwind 4.3.3 (@tailwindcss/vite) · @tanstack/react-query 5.102.3 · react-router-dom 7.18.4 · zod 4.6.5 · zustand 5.0.15 · axios 1.20.0 · @dnd-kit/* · lucide-react · Vitest 5.0.2 + @vitest/browser-playwright
@nomad/ui git+https://github.com/PedroPaduelo/nomad-ui.git#v1.13.5(lock resolve #19063f9). Sem react-markdown, rehype-raw, remark-*, react-syntax-highlighter, date-fns, isomorphic-dompurify, @tanstack/react-virtual: o Render não usa nenhum deles.
Escolha da tag do kit: v1.13.5, a mesma do AgentPack e do Motor, para o check-lock do pacote bater e para o preset de lint/tsconfig ser o mesmo. A v1.13.7 (usada pelo LB e pela Conta) difere num componente de teclado; subir depois é bump de uma linha. Nunca #main: a main do nomad-ui tem 1 arquivo e qualquer spec sem tag instala nada.
| Classe | O quê | Onde |
|---|---|---|
| Copiado igual (literal) | shot/** (4 arquivos, 1.837 linhas — só o Render usa); backend/src/lib/render-contracts/** (19 arquivos); backend/src/services/render-starter-kit/** (17 arquivos + snapshot); backend/src/services/render-{catalog,system,screen,bundle}/**; backend/src/services/render-agent-bundle.service.ts; backend/src/lib/render-legacy-catalog.ts; backend/src/tools/render/** menos core.ts; backend/schemas/render/**; frontend/src/render/** (69 arquivos, incluindo os 2 .generated); frontend/src/pages/Render/** (112); frontend/src/test/render/** (59, menos 1 exceção); frontend/vitest.config.ts com os mesmos pisos; frontend/vite.config.ts, vite-plugins/*; frontend/nginx.conf com o mesmo resolver ${NGINX_LOCAL_RESOLVERS} valid=5s ipv6=off e o proxy de /api, /auth/sso, /auth/callback e /mcp | §10 do mapa infra |
| Copiado com adaptação mecânica | backend/src/app.ts (mesma ordem de 17 plugins, sem o bloco do Render que fica no AgentPack e sem o catálogo de skills); plugins/* (12; mcp-guard só se houver /mcp); middleware/{actor,actor.types,authz,user-auth}.ts; services/{oidc-client,oidc-auth,oidc-jit,user-session,user,organization,agent-key}.service.ts; config/env-schema.ts (só as envs da app, sem nenhum AGENTPACK_); services/readiness.service.ts + schema-health.service.ts (3 índices parciais do Render + o shot); plugins/audit.ts + services/audit.service.ts (trilha própria); scripts/{env-example,docs-gen,openapi-spec,lint-ratchet,coverage-floor}.ts; Dockerfile + docker-entrypoint.sh (usuário render, sem TASK_ATTACHMENTS_DIR, sem a validação de skills do build); frontend/Dockerfile (ARG RENDER_API_URL/RENDER_MCP_URL/RENDER_CONTA_URL, BACKEND_UPSTREAM sem default) | §10 do mapa infra, itens 5, 9, 13-19 |
| Adaptado por decisão | mcp/** (só 11 dos 15 arquivos — ver P6) e mcp-server.ts; services/render-access.ts (o AsyncLocalStorage do tools/call continua, mas canAccessProject passa a consultar o espelho de projeto e o escopo global passa a ser por empresa); lib/render-db.ts (assinatura de readLibrary/writeLibrary recebe a organização ativa); mcp/metrics.ts (o gauge do pool LRU de MCP proxiado sai, o resto fica) | §3, §14 |
| Não vai | tudo de core (tasks, knowledge, memories, skills, mcps, search, portal markdown, dashboard), services/project.service.ts (vira espelho), services/task-decision.service.ts, mcp/prompts.ts, mcp/resources.ts, mcp/registry.ts (vira o registro só do Render), mcp/proxy-call.ts, services/mcp-proxy*, routes/{knowledges,memories,skills,tasks,todos,mcps,search,stats}.ts, features/** do frontend menos projects/hooks/useProjects.ts, frontend/src/api/generated/**, .github/ | §12 do mapa acoplamento |
Um achado que vem junto: frontend/src/api/generated/openapi.json na main do AgentPack não é JSON válido — json.load falha em line 16801 column 27 (3 vírgulas sobrando no enum creator_inactive, linhas 16800, 17047 e 17291) desde bbc205c. Não é do Render, e a app nova não herda api/generated. Se o AgentPack quiser o api:check verde, corrigir lá é uma linha.
Cliente OIDC próprio, OIDC_CLIENT_ID = render (hoje o padrão é agent-package e o acesso é o claim apps["agent-package"]; a app nova usa apps["render"]). Mesmo padrão do AgentPack: authorization code + PKCE, cookie de sessão httpOnly na própria origem, /auth/sso e /auth/callback fora do /api, GET /api/auth/me, GET /api/auth/oidc/topbar, POST /api/auth/logout, POST /api/client-errors, GET /health/live. Espelho local de users, organizations, memberships, user_sessions, oidc_provisioning_events — a app guarda só o espelho, nunca é a fonte.
Contrato do claim apps (novo, aSpecify — ver §14). O desenho anterior só dizia "usa o claim apps["render"]", o que não basta para derivar papel de forma determinística. O Membership do AgentPack guarda externalAppRole como coluna única, ou seja, o que a Conta disse por último. O contrato que a app nova consome:
| Payload da Conta | O que a app Render grava | ||
|---|---|---|---|
| `apps: { "render": "owner" \ | "admin" \ | "member" }` | memberships.externalAppRole = esse valor; role derivado = owner → owner, admin → admin, member → member |
apps: { "render": { "role": "member", ... } } | mesma derivação, lendo .role (o JIT aceita as duas formas; teste de contrato com um payload de exemplo de cada) | ||
apps ausente, ou sem a chave render | sem vínculo; a pessoa cai na página de sem acesso (/auth/no-access), que já existe no padrão | ||
usuário com role: "admin" de plataforma na conta | resolveActive cai na organização padrão com papel owner, como no AgentPack. O app Render não pode assumir que existe userRole admin sem essa semântica de plataforma |
O render_organizations local recebe o external_org_id da Conta; a chave de organização local é o id espelhado, e toda checagem de escopo usa o local.
apk_*A app emite as próprias chaves, com o mesmo formato (apk_<12 hex>_<43 base64url>, SHA-256 no banco) e a mesma semântica de kind:
project: projectIds = os projetos da empresa; sai da empresa ⇒ revogada (só as pessoais).admin: chave da empresa; sai o criador, perde o papel, é desativado ⇒ não revoga.render (20) e render-admin (39) como teto; escopo render:write para escrita.Semântica declarada do criador desativado (novo). No AgentPack, AgentKey.createdBy tem onDelete: SetNull e authenticateAgentKey não consulta o estado do criador: desativar o criador hoje não derruba a chave admin. A app nova herda esse comportamento (é a razão de a chave admin ser da empresa), com uma regra a mais que é nova e precisa de teste próprio: o criador desativado não pode mais gerenciar chaves, mesmo que a chave que ele criou continue valendo. Se o dono quiser o contrário (desativar o criador derruba a chave admin da empresa), isso é uma checagem nova — authenticateAgentKey passa a consultar o estado do criador — e muda o contrato de D4. Isto é uma pergunta nova para o dono (D17).
O AgentPack não suspende por contrato encerrado: app.contract.cancelled e org.deleted caem no mesmo case, que remove todos os vínculos e derruba as sessões — o que apaga o vínculo em vez de suspender o acesso. A app nova nasce com a regra do dono, que é a mesma já implementada e testada no LB (backend/src/services/oidc/jit.ts, LB-ORG-SUSP-01):
| Evento da Conta | O que a app nova faz |
|---|---|
org.upserted | espelha nome/slug; se a empresa está suspensa, marca resumeRequestedAt — não reativa |
member.upserted | espelha a pessoa e o papel do dono; nada de acesso |
app_access.granted | cria o vínculo da pessoa (JIT); nunca reativa empresa suspensa |
member.removed / app_access.revoked | remove o vínculo, derruba as sessões, revoga só as chaves de projeto do criador naquela empresa (revokedAt, linha preservada); a chave admin da empresa fica |
app.contract.cancelled | suspende: suspendedAt, suspendedReason, derruba as sessões, e o middleware de agente passa a recusar as chaves apk_* da empresa. Nada é apagado |
org.deleted | terminal: suspende e nunca volta |
| resync humano (plataforma-admin na Conta, "Reenviar estado") | única forma de liberar: clearOrgSuspension limpa a suspensão; os eventos reemitidos reespelham |
Ordenação: proxyAccessChangedAt = occurredAt do último evento que avançou o estado, em UPDATE … WHERE condicional atômico; evento sem occurredAt é rejeitado; empate não avança. Ou seja: reenvio de evento velho não ressuspende uma empresa recontratada.
apv_* — caminho de credencialMuda de segredo: hoje deriva de JWT_SECRET do AgentPack por HKDF. A app nova tem o seu JWT_SECRET, o seu rótulo (render/render-preview-token/v1) e a sua audiência.
Isto já está quebrado no AgentPack e a app nova nasce consertada: o shot grava a credencial em sessionStorage['agentpack:auth-token'] (shot/server.mjs:95,704), mas o frontend parou de ler essa chave no commit 7ac8e39 (a sessão virou cookie httpOnly) e /render/preview/** está dentro do RequireAuth (routes.tsx:136, com as rotas de preview em :397-402 fora do MainLayout mas dentro do RequireAuth do topo): o Chromium cai no SSO. As duas tools de screenshot devolvem só a URL.
O caminho novo, completo e verificável (decisão D15, detalhada em P12). Três alternativas foram comparadas; o risco de segurança de cada uma está na §11 risco 19:
| Opção | Credencial na página de preview | Veredito |
|---|---|---|
(a) cookie httpOnly de escopo curto, emitido pelo backend para o Chromium, SameSite=Strict, desafio one-shot | nada legível por script | escolhida — mesmo caminho do OIDC, já implementado no app |
(b) sessionStorage com getToken no cliente HTTP | legível por qualquer script da origem, e a página executa o motor do Render (conteúdo do usuário) | recusada: um XSS no motor expõe o apv_ e dá leitura de qualquer tela do projeto por 5 min |
| (c) sessão real via cookie de serviço dedicada, TTL 5 min | nada legível por script, mas cria sessão de verdade por captura | aceitável, mais caro; fica como plano B se o desafio one-shot atrapalhar o Chromium |
O caminho (a) em 4 passos, todos testáveis:
apv_ com audiência e rótulo próprios e o devolve num campo que o shot grava no cookie de preview do Chromium.shot grava cookie render:preview=<apv_>; HttpOnly; SameSite=Strict; Path=/render/preview antes do primeiro load da página.RequireAuth ganha exceção para /render/preview/**: aceita o apv_ (via cookie httpOnly, enviado pelo Chromium automaticamente) e não redireciona para a Conta.api/client.ts recebe o getToken escopado ao path, configurado no createHttpClient do @nomad/ui/data — que hoje é deliberadamente sem token (api/client.ts:37-42).Teste de segurança obrigatório (render-security.test.tsx): um <script> injetado na página de preview não consegue ler a credencial. E authed-media.test.tsx passa a cobrir o caminho do apv_ e não só o cookie.
SHOT_SERVICE_URL sem validação de host (novo)O backend emite o apv_ por captura e o readiness consulta GET <SHOT_SERVICE_URL>/health (readiness.service.ts:166-176). Sem validação, um SHOT_SERVICE_URL apontando para fora faria o backend fazer POST para host arbitrário com o SHOT_TOKEN no header x-shot-token a cada screenshot. O AgentPack hoje não tem SHOT_SERVICE_URL em produção (conferido nas envs do painel), o que significa que esse caminho nunca foi exercitado.
Correção: env-schema.ts valida SHOT_SERVICE_URL no mesmo estilo do EGRESS_ALLOWLIST — host interno ou allowlist explícita, sem http:// público, e sem https:// genérico. Teste de config que recusa URL pública. E o runbook declara que SHOT_SERVICE_URL é opcional: sem ele, as duas tools de screenshot devolvem só a URL (comportamento degrado, já implementado em render-screenshot.ts:89).
Decisão do desenho (sujeita ao dono): o Render não duplica o projeto. Ele guarda um espelho local do projeto do AgentPack, com a mesma chave primária.
prisma
model RenderProject { // nome interno; a coluna é render_projects
id String @id // = o id do projeto no AgentPack (UUID), 1:1
externalOrgId String @map("external_org_id") // org_id da Conta
organizationId String? @map("organization_id") // id local, espelho
name String
deletedAt DateTime? @map("deleted_at") // lixeira
deletedBatchId String? @map("deleted_batch_id")
syncedAt DateTime @map("synced_at")
ports RenderPort[]
designs RenderDesign[]
libraries RenderLibrary[]
systems RenderDesignSystem[]
@@index([organizationId]) @@map("render_projects")
}
Por que espelho e não chamada ao AgentPack a cada requisição:
requireProject e activeOwnerWhere (lib/render-db.ts:29-41, services/render-access.ts:79-80) são consultados em toda leitura e escrita de projeto. Uma chamada de rede por operação transforma transação SERIALIZABLE em duas, e o X-AgentPack-Project pode vir de um agente que não tem rede.actor.projectIds, render-access.ts:52) precisa do vínculo projeto→empresa sem ida ao AgentPack.O desenho anterior tratava o espelho como "webhook + reconciliador" e só mencionava o 404 enganoso como risco. Isso é insuficiente: um projeto novo criado no AgentPack não aparece no Render até o webhook chegar, e o webhook é assíncrono e pode falhar. Nesse intervalo o Studio mostra 404 para um projeto que existe, e o agente recebe NOT_FOUND em vez de um erro retryable — e requireProject devolve NotFoundError por padrão (render-db.ts:30), que o RequireAuth e o QueryClient transformam em erro de tela cheia.
O contrato corrigido:
REPLICATING (retryable, com details.reason), não 404, durante um período de graça (5 min) após a última sincronia bem-sucedida./health/ready e numa métrica Prometheus. Sem sync dentro do SLA, o /health/ready fica vermelho.O purge no AgentPack não é um UPDATE: project.service.ts:245-282 pega um pg_try_advisory_xact_lock, valida deleted_at, conta taskIds e renderAsset com storageKey, apaga as linhas de renderAsset, apaga o projeto (cascata) e só depois do commit remove os arquivos de RENDER_ASSETS_DIR e as atribuições de task. Nada disso é observável por um webhook de "purge": é transação seguida de I/O de arquivo. Pior: purgeExpired roda sozinho, por job de retenção (project.service.ts:285), sem ninguém avisar o Render.
O contrato corrigido:
DELETE /api/render/projects/:id/purge antes de apagar a linha local, com o id e a lista de storageKeys. Falha alta ou fila de retry com alerta — nunca seguir e deixar órfão.RENDER_ASSETS_DIR, e só por essa chamada explícita: nunca por job próprio (ele não consulta o AgentPack).agentpack-prod-data é dele, render-prod-data é do Render).render_projects com linhas de render_* sem projeto ativo, já que a FK é local) — render-project-sync.test.ts ganha o caso "purga no AgentPack sem purge no Render deixa linhas, e a próxima reconciliação sinaliza".Teste em P7: decisão de outro projeto devolve 404 (não há oráculo de existência), decisão já usada devolve APPROVAL_REQUIRED com stale: true, e agente (não humano) que tenta responder recebe 403.
project_create/update/delete (agentpack_project_*), a lixeira, o purge e a lista de projetos. As três tools moram hoje no registro do Render (tools/render/core.ts:11-28) e precisam ir para tools/core/projects.ts com defineTool antes do registro do Render sair, sem mudar o nome de wire nem o formato { ok, error } (o snapshot mcp-registry.test.ts.snap e os agentes em uso dependem disso).
O caminho real hoje é o REST, não o task_decisions. Em render-agent-system-versions.service.ts:390-419, assertUpgradeApproved só é chamado quando a chamada traz decisionId. O frontend nunca traz: useRenderDesignSystemVersions.ts:58-80 (useUpgradeDesignSystem) manda o upgrade sem decisionId, e o comentário é explícito — "decisionId (o usuário que chama a rota É a aprovação)". UpgradeDesignSystemDialog.tsx:16 repete. O POST /projects/:projectId/render/design-systems/:id/upgrade (routes/render.ts:971, :988) chega com approvedByHuman, e runAsRenderCaller (routes/render.ts:339) é o que transforma o usuário em aprovador. O caminho por task_decisions é usado só pelo agente, pela tool render_design_system_upgrade com decisionId. E o frontend não tem tela de decisão de upgrade: DecisionReview/TaskDecisionPanel vivem em features/tasks, que não vem.
Logo: render_approvals serve ao caminho do agente, e a "tela de decisão" precisa ser uma tela nova da app Render. Duas partes:
(1) O caminho humano (REST, sem decisionId) é preservado sem mudar nada. Mesma verificação de pins, mesmo 409 APPROVAL_REQUIRED, mesmo envelope. Não precisa de tabela nenhuma.
(2) O caminho do agente (tool com decisionId) é o que render_approvals substitui.
prisma
model RenderApproval { // render_approvals
id String @id
projectId String @map("project_id") // NOT NULL — ver abaixo
renderDesignSystemId String @map("render_design_system_id")
key String @unique // render-ds-upgrade-<8>-vX-to-vY-rN
status String // open | answered
optionId String? @map("option_id")
options Json // rótulos e descrições preservados
actorType String @map("actor_type") // human (único que responde)
actorId String? @map("actor_id")
createdAt DateTime @map("created_at")
decidedAt DateTime? @map("decided_at")
usedAt DateTime? @map("used_at") // uma decisão = uma transição
createdTransition Json @map("created_transition") // from/to/r que a key pinou
@@index([projectId]) @@map("render_approvals")
}
project_id NOT NULL porque a garantia de que a decisão pertence ao projeto certo vinha da relação com a task (decision.task.projectId === opts.projectId, …:406). Num banco novo sem tasks, essa relação precisa ser reproduzida por coluna: a verificação passa a ser render_approvals.projectId === opts.projectId.usedAt em UPDATE condicional, do mesmo jeito que a key já funciona hoje.Aplicar a atualização / Manter a versão atual), a forma do details.decision e o action do 409 são preservados — o texto de hoje já cita agentpack_task_decision_create e precisa virar a forma nova (P6/P7 ajustam o texto, sem mudar o envelope).details.decision da app nova aponta para a tela do Render, não para a rota de tasks do AgentPack. Registrar: isso muda o contrato de erro e as skills que hoje leem esse link precisam ser conferidas.A pergunta que precisa ir ao dono é esta (D2 reescrita): o dono aceita que a app Render ganhe uma tela de decisão de upgrade nova (dentro de pages/Render/Versions) para o caminho do agente? Se não, o caminho do agente volta a precisar do AgentPack e P7 trava. A alternativa é remover o caminho do agente (a tool render_design_system_upgrade passa a exigir approvedByHuman e responde APPROVAL_REQUIRED para qualquer agente, sem tabela nova) — é mais simples e mais segura, mas tira do agente a capacidade de solicitar e executar um upgradeaprovado.
O catálogo global (project_id NULL, visível por todas as empresas) passa a ser por empresa (organization_id). Verificado no código, o que isso toca:
| Onde | Hoje | O que tem que ficar |
|---|---|---|
services/render-access.ts:68-72 visibleProjectsWhere() | OR: [{projectId: null}, {projectId: {in: ids}}] | projectId: null e organizationId do actor |
services/render-access.ts:80 activeOwnerWhere() | OR: [{projectId: null}, {project: {deletedAt: null}}] | idem |
| 3 índices únicos parciais | ON (base_key/kind/name) WHERE project_id IS NULL | ON (organization_id, X) WHERE project_id IS NULL no SQL da migration inicial |
middleware/actor.ts:234-236 canWriteOutsideProjects() | só autoriza fora de projeto com reach === "org" e chave admin | GlobalWriteReach do Render muda de "platform" para "org" — senão a chave admin da empresa recebe 403 GLOBAL_WRITE_NOT_ALLOWED escrevendo no catálogo da própria empresa |
plugins/auth.ts:345-353 assertDesignSystemWriteAllowed() | decide por projectId === null | decide por organização, não por nulidade de projeto |
lib/render-db.ts:80-81 herança global→projeto | readLibrary cai na global quando não há linha local | filtro {kind, projectId: null, organizationId} — e a assinatura de readLibrary/writeLibrary passa a receber a organização ativa (35 chamadores em lib/render-db.ts:3-5) |
lib/render-legacy-catalog.ts assertLegacyCatalogWritable / isReservedDesignSystemName | giram em torno de project_id IS NULL | precisam de org_id no predicado |
lib/render-catalog/validate.ts:15 INHERITANCE_DEPRECATION | deprecia a herança global→projeto | idem, sem project_id IS NULL |
Isto é uma mudança de dado, política e consulta — não uma adaptação mecânica — e por isso virou o pacote P4b (§10), com dependência dura da decisão D3. Sem ele, os 15 pacotes assumem um "global" que não existe mais.
Regra do dono aplicada: a Conta é a fonte das empresas; o Render não cria empresa, não cria membro e não apaga nada por contrato encerrado.
Os endpoints entram com os mesmos caminhos sob o prefixo /api, mesmos cabeçalhos (ETag, If-Match, Prefer: return=minimal, X-Total-Count, X-Page, X-Page-Size, X-Next-Cursor), mesmo problem+json RFC 9457, mesmo 409 com details.reason, mesma ordem de plugins.
Critério de contagem, escrito para o implementador não errar: pares método + caminho, somando routes/render.ts e routes/render-scoped.ts juntos, e contando as rotas com ${prefix} (/designs/** e /library/**) uma vez cada. Isso dá 47. A revisão 1 dizia 54; o número conferido no código é 47 (parser sobre os dois arquivos, ver §14).
A lista completa, para conferência:
DELETE /projects/:projectId/render/screens/:baseKey
DELETE /render/design-systems/:id
GET /designs
GET ${prefix}/designs/:baseKey
GET ${prefix}/designs/:baseKey/assets
GET ${prefix}/designs/:baseKey/diff
GET ${prefix}/designs/:baseKey/revisions
GET ${prefix}/designs/:baseKey/revisions/:revisionId
GET ${prefix}/designs/:baseKey/summary
GET /library/:kind
GET /projects/:projectId/render/design-systems
GET /projects/:projectId/render/design-systems/updates
GET /projects/:projectId/render/designs
GET /projects/:projectId/render/export
GET /projects/:projectId/render/ports
GET /render/assets/:storageKey
GET /render/design-systems
GET /render/design-systems/:id
GET /render/design-systems/:id/library/:kind
GET /render/design-systems/:id/versions
GET /render/export
GET /render/schemas
GET /render/schemas/:doc/:version
PATCH /library/ui/components
PATCH /projects/:projectId/render/design-systems/:id/library/ui/components ← via ${prefix}
PATCH /projects/:projectId/render/designs/:baseKey/revisions/:revisionId
PATCH /projects/:projectId/render/library/ui/components
PATCH /projects/:projectId/render/screens/:baseKey
POST /projects/:projectId/render/design-systems/:id/apply
POST /projects/:projectId/render/design-systems/:id/upgrade
POST /projects/:projectId/render/import
POST /projects/:projectId/render/port/design/:baseKey
POST /projects/:projectId/render/port/library/:kind
POST /projects/:projectId/render/screens
POST /projects/:projectId/render/tailwind/compile
POST /render/design-systems
POST /render/design-systems/:id/apply
POST /render/design-systems/:id/publish
POST /render/import
POST /render/port/design/:baseKey
POST /render/port/library/:kind
POST /render/tailwind/compile
PUT /designs/:baseKey/screen
PUT /library/:kind
PUT /projects/:projectId/render/design-systems/:id/library/:kind
PUT /projects/:projectId/render/designs/:baseKey/screen
PUT /projects/:projectId/render/library/:kind
As rotas legadas sem /render no caminho (/api/designs/**, /api/library/**) vêm como estão: o frontend atual as usa e o objetivo é que a troca seja só de URL. Escrever um alias novo e deixar o antigo é a única forma de quebrar um cliente que alguém já guardou.
GET /api/render/schemas/:doc/:version continua público (PUBLIC_ROUTE + allowlist em tests/auth-guard.test.ts): é o contrato, e ele é público por desenho. O $id do JSON Schema é a URL pedida, com Vary: Host — que o AgentPack já emite (routes/render.ts:283-294, mesclando com o Origin que o CORS põe). A app nova copia esse código e ganha um teste que confere o Vary: Host na resposta, porque sem ele um cache intermediário serve o $id de um host para outro.
Os mesmos 59 nomes (confere com o snapshot mcp-registry.test.ts.snap, que tem 222 tools e 59 com render_), os mesmos dois toolsets (render 20, render-admin 39, mapa em tools/render/toolsets.ts), mesmo envelope { ok: true, ... } / { ok: false, error: { code, ... } }, mesma ordem dos argumentos, mesma divergência entre schema publicado (wire.ts, compacto, teto de 60 KB no tools/list) e schema validado no handler, mesma função renderTool com a imagem antes do JSON, mesmo help.ts.
Onde moram as 59 (contagem conferida no código, casa exatamente com o snapshot):
| Arquivo | Tools | O que é |
|---|---|---|
tools/render/design-systems.ts | 18 | ciclo de vida de design system (list/get/create/update/delete/apply/publish/status/upgrade), library_get/library_update, starter_import, render_port, render_port_list, component_view, screenshot |
tools/render/screens.ts | 17 | telas, nós, histórico, mapa e preview de projeto |
tools/render/catalog.ts | 15 | componentes, estilos e tokens (os 9 render_component_*/_style_*/_token_* entram por targetTool/styleTool/tokenTool, não por renderTool) |
tools/render/bundles-assets.ts | 7 | import/export de bundle e CRUD de assets |
tools/render/extract.ts | 1 | extração |
tools/render/help.ts | 1 | render_help |
tools/render/core.ts | 0 (as 3 project_*) | não vai — ver §9 passo 3 |
| total | 59 | igual ao snapshot mcp-registry.test.ts.snap (222 tools, 59 render_*) |
Atenção ao implementador: design-systems.ts e catalog.ts usam factories diferentes de renderTool (targetTool/styleTool/tokenTool); uma contagem por renderTool( fecha só 50 e faz o pacote parecer incompleto quando está certo.
No AgentPack, mcp/catalog.ts:73 define BUILTIN_TOOL_PREFIX = "agentpack_" e :160 monta `${BUILTIN_TOOL_PREFIX}${tool.name}` . O código das tools registra render_screen_list (sem prefixo). No caminho proxied, proxiedWireName(prefix, toolName) (mcp/catalog.ts:357-365) monta `${prefix}_${safe}` sempre — e, como agentpack_render_ (16) + render_screen_list (18) = 34 caracteres, está muito abaixo do teto de 64, o nome sai literal, sem hash.
Consequência que a revisão 1 não viu: se o /mcp do Render publicar agentpack_render_screen_list e a ponte for cadastrada com toolPrefix: agentpack_render, o wire fica agentpack_render_agentpack_render_screen_list — os 59 nomes mudam, que é exatamente o que D5 diz que não pode acontecer. Nenhum dos dois lados normaliza hoje.
Decisão (nova, e ela substitui parte de D5/D6): o prefixo é aplicado em UM lugar só, e é o /mcp da app Render.
| Caminho | Quem prefixa | O que chega ao agente |
|---|---|---|
/mcp do Render direto (fase A) | o catálogo do Render, com BUILTIN_TOOL_PREFIX = "agentpack_" | agentpack_render_screen_list |
Ponte: AgentPack cadastra o /mcp do Render (fase B) | o Render, e a ponte cadastrada com toolPrefix: render | render_agentpack_render_screen_list ✗ |
Não fecha. A correção mínima é fazer proxiedWireName normalizar: se o nome upstream já começa com ${prefix}_, não prefixa de novo. Isso é uma mudança no AgentPack (não só configuração), de ~5 linhas, e é o que a crítica de paridade classifica como bloqueante. Duas saídas, escolha do dono:
proxiedWireName reconhece o prefixo upstream e não dobra. Preserva tudo: o Render publica agentpack_render_* (D5, P6, §5.2 e o snapshot continuam válidos) e a ponte cadastrada usa toolPrefix: agentpack_render e reproduz agentpack_render_* (D6 continua válido). Recomendada: um único ponto de mudança, e as duas metades do documento fecham sem se contradizer./mcp direto passaria a devolver render_screen_list, que quebra a compatibilidade direta que a fase A promete e as skills agentpack-render-* (D7) que ficam no AgentPack. Recomendamos (a).Com (a), a resposta às duas perguntas é: o Render publica agentpack_render_* (D5: manter), e a ponte é cadastrada com toolPrefix: agentpack_render (D6: sim, no corte), e o catálogo do AgentPack passa a normalizar o prefixo upstream — nova tarefa, em pacote próprio do AgentPack (P-2 na §10), pré-requisito do corte e não do app novo.
Testes que fecham isso: no app novo, um teste que monta o /mcp do Render, roda tools/list e afere os 59 nomes agentpack_render_*; no AgentPack, um teste de contrato que, com o MCP do Render cadastrado, tools/list devolve exatamente o snapshot atual. E o preflight de P14 confere que os dois lados concordam sobre quem prefixa.
X-AgentPack-Project e X-AgentPack-Toolsets continuam se chamando assim por compatibilidade de cliente (o server.json da app nova os descreve). Três correções:
render e render-admin são byDefault: false (mcp/toolsets.ts:85-93): um agente precisa pedir o toolset explicitamente. Na app nova ambos são byDefault: true — o Render é o app inteiro, não um domínio dentro dele. Decisão registrada, com teste.buildUpstreamConfig repassa config.headers (services/mcp-headers.ts:336), o override por vínculo já pode carregá-los como header fixo. O override do vínculo AgentPack→Render passa a mandar X-AgentPack-Toolsets e X-AgentPack-Project, senão o Render aplica o default dele e um agente que usava render por header receberia um catálogo sem as 59 tools.resolveDefaultProjectId (mcp/default-project.ts:52-60) usa o header só quando o principal alcança o projeto. Na ponte o principal no Render é a chave da configuração do MCP, não a do agente — é a ressalva de identidade que a §5.4 já assume./mcp próprioSim, a app Render sobe o gateway Streamable HTTP stateless do AgentPack (ADR-011), com 11 dos 15 arquivos de mcp/: catalog, toolsets, tool-access, tool-call, tool-kit, instrument, json-schema, meta-tools, default-project, metrics, instructions — mais mcp-server.ts, routes/mcp.ts, mcp-tool-content.ts.
O que NÃO vai, com destino explícito (a revisão 1 listava 11 e omitia 4 sem dizer que saíam):
| Arquivo | Destino | Por quê |
|---|---|---|
mcp/registry.ts | sai — vira o registro só do Render | hoje é o registro do app inteiro (core + Render + GitHub) |
mcp/prompts.ts | sai | importa project.service, skill-files, task.service: prompts executar-task e handover, que são de task |
mcp/resources.ts | sai | importa knowledge.service, memory.service, skill.service, project.service: resources skill://, agentpack://knowledge, agentpack://memory |
mcp/proxy-call.ts | sai | o proxy de MCP externo é do AgentPack |
mcp-server.ts é adaptado, não copiado: mcp-server.ts:67 importa PROMPTS/getPrompt/ completePromptArgument de ./mcp/prompts.js e :68-76 importa ./mcp/resources.js. Na app nova esses 5 imports saem junto com os handlers correspondentes (prompts/get, resources/list, resources/read, resources/templates/list, completion/complete — mcp-server.ts:172-190), e ficam initialize, tools/list, tools/call, ping e as meta-tools search/describe. Cuidado com stubs: se alguém criar prompts.ts/resources.ts vazios, o SDK aceita lista vazia e o buraco passa em silêncio — com o /mcp publicando prompts de task e resources de skill que não existem. Por isso mcp-registry.test.ts ganha a asserção de que PROMPTS e RESOURCES são listas vazias na app nova (ou, melhor, de que os dois módulos simplesmente não existem e mcp-server.ts não os referencia).
mcp/metrics.ts é adaptado, não copiado literal. metrics.ts:16 importa mcpProxy em runtime (não é import type, como o UpstreamTrace de tool-access.ts:21, que pode ser copiado literal). metrics.ts:77 chama this.set(mcpProxy.poolSize()) para publicar o tamanho do pool LRU de MCP proxiado — que a app Render não tem. Copiado literal, o import de services/mcp-proxy.js quebra o typecheck; cortado às cegas, perde-se o resto do arquivo (KNOWN_METHODS, errorId, as métricas do protocolo). Correção: manter tudo e trocar só o gauge mcpProxy.poolSize() por métrica própria do gateway do Render, com nota no arquivo.
O proxy de /mcp no nginx do frontend: já está no desenho desde a revisão 1 (§6.3 lista /api, /auth/sso, /auth/callback e /mcp) e o nginx.conf do AgentPack já tem a location ~ ^/mcp(?:[/?]|$) (linha 193). O que faltava era o teste, e ele entra em P8: um teste de segurança que falha se /mcp não estiver no proxy, porque a Fase A é justamente quando o agente aponta direto para o /mcp do Render. O frontend precisa do build arg RENDER_MCP_URL mesmo com o proxy, porque o agente pode chamar a URL de fora.
Publica server.json com https://render-v0-api.mp.serendiped.com/mcp e Authorization: Bearer apk_*.
Como funciona: o AgentPack guarda MCPs cadastrados em mcp_servers com um toolPrefix único (^[a-z0-9_]{1,24}$, CHECK no banco) e publica cada tool proxied no wire como <toolPrefix>_<nome upstream>. Com a normalização do §5.2.1, cadastrar o /mcp da app Render com toolPrefix: agentpack_render reproduz exatamente os nomes de hoje.
Ordem obrigatória:
RENDER_TOOLS, o proxy não publica nada. loadProxiedCatalog faz if (builtin.has(name) || byName.has(name)) continue; (mcp/catalog.ts:292). A ponte só funciona depois de remover o Render do registro built-in. É mecanismo de virada, não de convivência.scope: "write" (PROXIED_TOOL, middleware/authz.ts:169), não render:write. Quem usa a ponte precisa de write na chave do AgentPack.buildUpstreamConfig monta só config.headers + segredos + override do vínculo, mais x-request-id/traceparent (services/mcp-headers.ts:336). O Render vê a chave da configuração do MCP, não a do agente que chamou: escopo por projeto e teto por toolset deixam de valer por requisição. O override por projeto é estático, não dinâmico.audit_events como escrita (mcp/tool-call.ts:88), mesmo as de leitura. Mais registro, nunca menos./mcp do Render é stateless (ADR-011), então o pool LRU do proxy é desperdício mas não quebra. O proxy e o /mcp do Render falam a mesma versão do SDK (1.30.1), então a correção do protocolo 2026-07-28 não é pré-requisito.C4): McpServer não tem organizationId (conferido no schema.prisma da 26268c0). Qualquer usuário lista, edita e vincula qualquer MCP, e a credencial dele vai junto. Precisa da coluna antes de cadastrar o MCP do Render — é o pacote P-2.secretHeaders com Authorization: Bearer apk_* da app Render, de escopo mínimo (leituratinha: a lista de envs/segredos do runbook (§7), a rotina de rotação, e a ordem de revogação (apagar o MCP no AgentPack antes de desligar o Render).
Fases:
/mcp da app Render (chave própria), e a UI do AgentPack aponta por link. Sem ponte.RENDER_TOOLS do registro do AgentPack (P-2 já feito), normaliza o prefixo (P-2) e cadastra o MCP do Render. A partir daí os agentes mantêm uma URL só e os mesmos nomes. O /mcp direto continua disponível para quem quiser escopo por chave.agentpack_render_* gravado em prompt, skill ou config.Recomendação final: ponte no corte (Fase B), /mcp direto como alvo. Quem avalia a chamada é o Render; a ponte só entrega.
Uma migration inicial só do Render (squash das 7 do AgentPack), com:
render_* e 3 enums (RenderTheme, RenderLibraryKind, RenderPortSourceKind);project_id apontam para render_projects;render_designs_global_base_key_key, render_libraries_global_kind_key, render_design_systems_global_name_key) — escritos à mão no SQL da migration e conferidos por schema-health.service.ts. Com o catálogo por empresa (D3), os três viram ON (organization_id, X) WHERE project_id IS NULL;render_design_system_versions_positive (o Prisma também não expressa);ltree, pg_trgm nem citext. O schema.prisma da app nova não declara previewFeatures = ["postgresqlExtensions"] nem as três extensões;users, organizations (com suspended_at, suspended_reason, proxy_access_changed_at, resume_requested_at), memberships, user_sessions, oidc_provisioning_events;agent_keys (com kind, organization_id, project_ids, scopes, toolsets);audit_events (trilha própria, append-only, AUDIT_RETENTION_DAYS);render_approvals (§4.4), se D2 for aceita na forma da tabela;render_project_links: o espelho é render_projects (D1).DB_CONNECTION_LIMIT mantém o padrão 1000 (decisão do dono) e exige max_connections acima disso no Postgres da app — o padrão do Postgres é 100, e o pool do Prisma vence o que vier na URL.
| Grupo | Variáveis |
|---|---|
| Só do Render | RENDER_ASSETS_DIR (/data/render-assets), RENDER_REQUIRE_IF_MATCH (true), RENDER_REVISION_KEEP (100), SHOT_SERVICE_URL (opcional, validado: host interno ou allowlist), SHOT_TIMEOUT_MS (60000), SHOT_TOKEN (segredo, ≥32) |
| Padrão do AgentPack | NODE_ENV, PORT (4000), HOST, DATABASE_URL, DIRECT_URL, DB_CONNECTION_LIMIT, REDIS_URL (opcional), JWT_SECRET, MCP_SECRET_KEY, RENDER_AGENT_TOKEN (o bootstrap), CORS_ORIGIN, FRONTEND_URL, MCP_ALLOWED_ORIGINS, MCP_ALLOWED_HOSTS, TRUST_PROXY, CHECKPOINT_DISABLE, LOG_LEVEL, SWAGGER_ENABLED, RATE_LIMIT_*, AUDIT_RETENTION_DAYS, VERSION_RETENTION_*, SHUTDOWN_* |
| SSO | OIDC_ISSUER, OIDC_CLIENT_ID (= render), OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_POST_LOGOUT_REDIRECT_URI, APP_PUBLIC_URL, CONTA_PROVISIONING_SECRET |
shot/ (as que o shot lê) | PORT (4500), SHOT_HOST, SHOT_ALLOW_ORIGINS (obrigatória em produção), SHOT_TOKEN, SHOT_MAX_QUEUE, SHOT_READY_TIMEOUT_MS, SHOT_HARD_TIMEOUT_MS, NODE_ENV, CHROMIUM_PATH |
| Frontend (runtime) | BACKEND_UPSTREAM (sem default, senão 503), NGINX_LOCAL_RESOLVERS, NGINX_ENVSUBST_FILTER |
| Frontend (build args) | NOMAD_UI_TOKEN (opcional), RENDER_API_URL, RENDER_MCP_URL, RENDER_CONTA_URL |
Correção da revisão 1: SHOT_TIMEOUT_MS é env do BACKEND (env-schema.ts:527, lida por render-screenshot.ts:72), não do shot/. Quem seguir a tabela antiga punha SHOT_TIMEOUT_MS no serviço do shot, onde nada lê, e deixava o backend no default — a captura estouraria o timeout do backend antes do hard timeout do Chromium. As duas tabelas agora são separadas. SHOT_HOST e CHROMIUM_PATH são lidos por server.mjs mas não precisam ir no runbook de produção (CHROMIUM_PATH só em teste), para o teste de env-example não acusar variável faltando.
RENDER_AGENT_TOKEN renomeado em tudo (novo). O prefixo AGENTPACK_ está espalhado em 6 pontos que a revisão 1 não listou: env-schema.ts:389, app.ts:49,193,197, middleware/actor.ts:13,144,272, plugins/auth.ts:106, plugins/swagger.ts:255. A app nova chama RENDER_AGENT_TOKEN em todos — se só a env mudar, o texto de ajuda do Swagger e os ids de log continuam dizendo AGENTPACK_AGENT_TOKEN numa app que não é o AgentPack. Teste: config-env.test.ts afirma que nenhuma env com prefixo AGENTPACK_ sobrevive no env-schema da app nova.
RENDER_AGENT_TOKEN é segredo com prazo (novo). No AgentPack, createBearerResolver (middleware/actor.ts:311-330) resolve o Bearer nesta ordem: bootstrap, apv_ de preview, chave de agente, JWT de usuário. O Actor BOOTSTRAP_AGENT tem escopo de escrita e alcança tudo. Um token de bootstrap que nunca gira é uma chave de plataforma permanente com acesso total de escrita. O runbook define: é rotacionado, por quem, e pode ser removido depois que a primeira chave admin da primeira empresa for criada. env-schema.ts exige segredo forte, com teste.
.env.example é gerado (npm run env:example) e um teste compara. Nome do pacote em backend/package.json = render-backend (é de onde sai a versão). O frontend precisa do build arg da Conta desde o primeiro deploy: sem ele o bundle embute o padrão e "Minha Conta" aponta para um sandbox morto — exatamente o defeito que está no ar no AgentPack hoje.
backend/Dockerfile: mesma imagem base e digest, npm ci --omit=dev com o CLI do Prisma como dependência de produção, usuário render (uid 1001), VOLUME /data, RENDER_ASSETS_DIR=/data/render-assets, PORT=4000, HEALTHCHECK em http://[ip-interno]:${PORT}/health/live (IPv4 explícito), ENTRYPOINT docker-entrypoint.sh com prisma migrate deploy que aborta o boot se falhar. Sem a linha que valida skills no build e sem TASK_ATTACHMENTS_DIR.frontend/Dockerfile: npm install -g npm@10.9.4 (o prepare do @nomad/ui precisa de git e o npm novo ignora o insteadOf), ARG NOMAD_UI_TOKEN opcional, insteadOf para ssh e https, build args RENDER_*_URL, CSP montada no build com __BACKEND_ORIGINS__, nginxinc/nginx-unprivileged:1.30.5-alpine3.24@sha256:…, NGINX_ENVSUBST_FILTER, NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1, BACKEND_UPSTREAM sem default, nginx -t no build, HEALTHCHECK em [ip-interno]/healthz.frontend/nginx.conf: igual, com o resolver ${NGINX_LOCAL_RESOLVERS} valid=5s ipv6=off (lista fixa de nameserver já deu 502 em produção duas vezes), client_max_body_size 25m, CSP com style-src 'unsafe-inline' (o Render injeta <style>) e img-src/media-src com https:, data:, blob: (mídia autenticada por blob), hash do script de tema do index.html, e proxy de /api, /auth/sso, /auth/callback e /mcp na mesma origem (a location /mcp já existe no nginx.conf do AgentPack, linha 193; a app nova copia e ganha teste).npx npm@10.9.4 install --package-lock-only, conferido com npx npm@10.9.4 ci --dry-run. Já quebrou deploy em produção por lock fora de sincronia.Projeto: workspace_nommand (947JSvgPNhtoIM97ko6ie), ao lado dos outros 9.
| Serviço | Origem | Porta | Memória | Volume | Domínio |
|---|---|---|---|---|---|
render-backend-ws (NOVO) | 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 (NOVO) | idem, buildPath /frontend | 8080 (não publicada) | 256 MB | — | render-v0.mp.serendiped.com → 8080, SSL |
render-shot-ws (NOVO, opcional) | idem, buildPath /shot | 4500 | 1024 MB | — | sem domínio (só o backend fala) |
Os nomes de container no painel são globais (minipanel-<serviço>): render-backend-ws não colide com o sandbox sb-render. O repositório é privado: o painel usa o token salvo em Sandboxes > Git access (todo deploy do AgentPack loga "Public clone failed, retrying with the saved git token").
Reversibilidade do volume (novo). O rollback declarado é "descartar o banco novo e reabrir a escrita no AgentPack" — mas isso não cobre o volume: uma vez que o Render escreve assets no render-prod-data, o rollback descarta o banco e não os bytes, e o AgentPack continua com os assets antigos. As duas cópias divergem e o sha256 agregado do preflight não fecha. O volume novo é descartável (o AgentPack continua sendo dono dos assets velhos até o corte, e a migração copia os bytes). Três regras no runbook: (a) antes do corte, backup ou nota de que render-prod-data 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) teste de que o script de migração, rodado duas vezes, é idempotente sobre o volume (não só sobre as linhas).
Env do render-backend-ws: a lista da §6.2, mais HOST, MCP_ALLOWED_HOSTS=render-v0-api.mp.serendiped.com (o mcpHostGuard casa request.host), CORS_ORIGIN=https://render-v0.mp.serendiped.com, TRUST_PROXY=1, APP_PUBLIC_URL=https://render-v0.mp.serendiped.com, OIDC_REDIRECT_URI=https://render-v0.mp.serendiped.com/auth/callback, OIDC_POST_LOGOUT_REDIRECT_URI=https://render-v0.mp.serendiped.com, RENDER_AGENT_TOKEN. REDIS_URL só se o painel tiver Redis.
Env do render-frontend-ws: BACKEND_UPSTREAM=minipanel-render-backend-ws:4000 (nome interno, mas nunca como default no Dockerfile) e NOMAD_UI_TOKEN se o nomad-ui voltar a ser privado.
Env do render-shot-ws: SHOT_ALLOW_ORIGINS=https://render-v0.mp.serendiped.com, SHOT_TOKEN (o mesmo do backend), NODE_ENV=production, PORT=4500.
Banco: Postgres próprio da app (projeto databases_prod, como o do AgentPack), com max_connections acima de 1000. Redis opcional — o Render só usa os contadores do rate limit, e sem REDIS_URL o plugin conta por instância.
Cadastro na Conta Nommand (ação do dono, fora do MyPanel): cliente OIDC novo, id render, redirect https://render-v0.mp.serendiped.com/auth/callback, post-logout https://render-v0.mp.serendiped.com, CONTA_PROVISIONING_SECRET novo e exclusivo (o segredo é por app: é o que identifica o app.contract.cancelled desta app), entrada no AppSwitcher com slug render, e o papel apps["render"] liberado na empresa. Sem isso a app não entra nem para a pessoa nem para o webhook.
Nenhum serviço novo nasce com autoDeploy ligado. Os 9 serviços do projeto estão com autoDeploy: true, mas o histórico mostra deploy manual (o backend do AgentPack teve 1 deploy entre 10-03 e 10-11 com 18 commits na main). Nascer desligado e nunca mais ligar: merge na main não publica, publicação é deployments_trigger, que é decisão do dono.
Registro do commit de build (novo). Com autoDeploy desligado, o painel não reconstrói quando o Dockerfile muda, e o serviço aponta para a main: se o Dockerfile tiver sido alterado sem commit, um deployments_trigger esperado constrói o commit antigo. Cada conferência pós-deploy no runbook registra o commit do build conferido (o painel mostra) junto com o hash do bundle, para que uma divergência entre "o que eu commitei" e "o que foi construído" fique visível. Mesmo padrão do AgentPack.
Alvo: backend/scripts/migrate-render-from-agentpack.ts, com --dry-run obrigatório e --apply explícito, exigência de --confirm-production para sair do dry-run, e log de cada etapa com contagem (nunca conteúdo de documento).
Ordem de execução
project_id IS NULL) + soma de rev/version;sha256 agregado de tree/payload/snapshot por tabela (a soma, não o conteúdo);project_id não existe em projects, e linhas de projeto na lixeira;RENDER_ASSETS_DIR (fora de exports/), comparado com sum(render_assets.size);task_decisions com key render-ds-upgrade-%);agentpack_render_* existem em mcp_tools, quantas chaves ativas têm render:write ou toolsets render/render-admin, quantos vínculos em skills/skill_versions/ project_skills/task_skill_links apontam para a família …100000000200..206;tools/list do /mcp do Render devolve agentpack_render_* e o catálogo do AgentPack normaliza — os dois lados concordam;openapi.json: se o AgentPack estiver com o arquivo inválido (§2.4), o api:check falha por outro motivo — corrigir antes.projects → render_projects, preservando o id, com organization_id resolvido por join com projects.organization_id e external_org_id da Conta, deleted_at e deleted_batch_id copiados. Sem isso nenhuma tela de projeto abre.render_design_systems → render_design_system_versions → render_designs → render_screens → render_revisions → render_libraries → render_ports → render_assets. Os ids são contrato: origin_id, render_ports.source_id/target_id, a URL /render/design-systems/:id, a key da decisão de upgrade (<8 primeiros do id do DS>) e os designSystemId que agentes guardaram. version/rev também: os clientes mandam If-Match. INSERT … SELECT via pg cru, com ON CONFLICT DO NOTHING para reexecução idempotente.RENDER_ASSETS_DIR/<storage_key> para o volume novo, com verificação de size e checksum linha a linha depois de copiar. exports/ não vai (TTL 24 h).render_approvals só as que ainda estão pending ou answered não consumidas (used_at IS NULL); as consumidas viram histórico em audit_events do Render.sha256 agregado de tree/payload/snapshot igual;count(*) e soma de checksum de arquivos igual;GET /api/render/export?scope=project (bundle) comparado entre o AgentPack e a app nova numa amostra de 5 projetos;RENDER_WRITES_FROZEN no withRouteAccess, respondendo 503 RENDER_FROZEN nas rotas de escrita e nas tools com render:write), repetir 3–4 e só então cortar. Sem congelar, um agente que escreve às 3h da manhã perde a tela.Preparo, com as correções da §14. O desenho anterior colocava o passo 1 (mover toolError) no "PASSO 8 do corte", que depende de P13 e P7 e não acontece antes da fase A entrar no ar. Se a fase B remover RENDER_TOOLS antes do preparo, o catálogo inteiro de tools do AgentPack quebra — inclusive as proxied e as de core — com um import não resolvido. Por isso o preparo virou os pacotes P-1 e P-2, sem dependência, e é o que entra na main primeiro.
Ordem segura, com o gatilho do dono em cada passo:
Preparo (vai para a main antes de tudo, sem mudar comportamento) — P-1 e P-2
toolError de tools/render/common.ts:28-49 → backend/src/mcp/tool-error.ts. mcp/tool-call.ts:17 e mcp/proxy-call.ts:15 importam dele para toda tool, inclusive as proxied (tool-call.ts:114,121: negação de escopo e qualquer erro). tools/render/common.ts reexporta de mcp/tool-error.ts até o Render sair, para o tools/render da fase A continuar compilando. Teste: mcp-tool-error.test.ts cassando o envelope { ok:false, error:{ code } } nas duas bordas.appBaseUrl de lib/render-view-url.ts:21-25 → módulo neutro lendo config/env.ts (task-decision.service.ts:2,284 depende). Nota: hoje o Render lê process.env direto, fora da regra do lint do AgentPack — o módulo neutro corrige isso.project_create/update/delete de tools/render/core.ts:11-28 → tools/core/projects.ts com defineTool, mantendo o envelope { ok, error } e os nomes de wire; atualizar o snapshot tests/__snapshots__/mcp-registry.test.ts.snap.serializers/render.ts sai do barril utils/serializers.ts:14.organizationId em McpServer (achado C4) e normalização do prefixo upstream em proxiedWireName (§5.2.1). Pré-requisito do corte, não do app novo.Corte (fase B)
RENDER_TOOLS sai do registro (mcp/registry.ts:19,30-35,38-40,45); toolsets render e render-admin saem de mcp/toolsets.ts:85-93; a ponte assume.app.ts:25,53-54,277-304; junto as duas chaves de plugins/rate-limit.ts:87-88 (o boot falha se a rota sumir e a chave ficar) e o :206.plugins/auth.ts:31,346-354,370-372,403-404,444,511-512: sai o resolver apv_* e a trava de escrita por id de design system (que lia render_design_systems).middleware/{actor.ts:282-285,321-322,actor.types.ts:21,authz.ts:122-128,134,137,173-176,182-190,247-252}: sai o render:write e os ramos do Render.services/project.service.ts:26,227-228,254-258,274-279: sai a contagem de telas/assets do deletePreview e a limpeza de arquivos do purge — vira a chamada DELETE /api/render/projects/:id/purge do contrato §4.2, não simplesmente "ou vira chamada à app nova".RenderMediaProvider sai do App.tsx:37,46; os 17 pontos que citam o Render (routes.tsx, paths.ts:54-113, Sidebar.tsx, Breadcrumb.tsx, os 3 barrels, pageMeta, glossary.ts, lib/agentKeys.ts, ConnectAgentModal, eslint.config.js, vitest.config.ts, nginx.conf:98-101) viram link externo ou somem. routes.tsx está em 297/300 linhas: perder o Render libera ~105 linhas de código.services/{schema-health,readiness}.service.ts e routes/health.ts:42: sai o Render e o shot.npm run docs:gen (docs/mcp/tools.md, docs/data-model.md, llms-full.txt).Limpeza de dados (por último, com backup conferido)
Project no schema.array_remove('render','render-admin') de agent_keys.toolsets e 'render:write' de agent_keys.scopes; DELETE FROM mcp_tools WHERE name LIKE 'agentpack_render_%' (o sync nunca apaga, sozinha); conferir project_mcps.enabled_tools/disabled_tools.npm run check:skills passa depois que as tools saem do registro./api/render/assets/** é um redirect de bytes de um serviço autenticado para outro, e com 307/308 o navegador repete a requisição no Render com o cookie do AgentPack — que o Render não valida, porque a sessão é do AgentPack. O resultado é 401 do Render para toda mídia. O certo é: (a) o item Render da Sidebar do AgentPack vira link externo e as telas que carregam mídia apontam para a URL do Render; e (b) se for preciso manter /api/render/assets/** no AgentPack, ele tem de ser um proxy autenticado que revalida a credencial no Render — não um redirect. audit_events fica como histórico (sem FK)./data/render-assets sai do volume agentpack-prod-data e a purga do AgentPack para de apagar esses arquivos (§4.2).Cada pacote é do tamanho de um agente. de_agentpack = caminhos de origem no origin/main 26268c0. Ordem respeitada: nenhum começa antes do que depende.
Os dois pacotes de preparo (P-1, P-2) são no AgentPack e não dependem de nada. Ver a §9.
| # | Pacote | Depende de |
|---|---|---|
| P-1 | Preparo do AgentPack: mover toolError para mcp/tool-error.ts (tool-call, proxy-call e a reexportação em tools/render/common.ts) | — |
| P-2 | Preparo do AgentPack: organizationId em McpServer (C4) + normalização do prefixo upstream em proxiedWireName | — |
| P0 | Núcleo do backend: esqueleto Fastify, config, Prisma, erros, plugins, health/metrics/llms, Dockerfiles, jest, gates, AGENTS.md | — |
| P1 | Banco do Render: schema das 8 tabelas + enums + migration inicial + render_projects + espelhos da Conta + chaves + auditoria + schema-health | P0 |
| P2 | Contratos do Render: lib/render-contracts/**, backend/schemas/render/**, os 2 geradores, segurança de conteúdo | P0, P1 |
| P3 | Identidade: OIDC (cliente render), JIT, sessões, webhook de provisionamento com suspensão por contrato, chaves apk_* (project + admin), middleware actor/authz, token de preview apv_*, contrato do claim apps | P0, P1 |
| P4 | Domínio do Render no backend: lib/render-* (incluindo render-legacy-catalog.ts), services/render-catalog, render-screen, render-system, render-bundle, render-agent-bundle.service.ts, render-tailwind, render-access, render-rest.service | P2, P3 |
| P4b | Escopo de empresa do catálogo (D3): backfill de organization_id, 3 índices parciais reescritos, visibleProjectsWhere/activeOwnerWhere por organização, GlobalWriteReach do Render para org, assinatura de readLibrary/writeLibrary | P3, P4 |
| P5 | Rotas REST do Render: routes/render.ts + render-scoped.ts (47), schemas/render.ts, serializers, lib/render-http.ts | P4b |
| P6 | Tools MCP do Render: tools/render/** (menos core.ts), toolset render/render-admin, os 11 arquivos de mcp/ que ficam, /mcp, server.json | P5 |
| P7 | Portão de upgrade (render_approvals + tela de decisão) + contrato de purga + script de sincronia de projetos + webhook do AgentPack (parte do backend) | P6 |
| P8 | Casca do frontend: Vite/TS/Tailwind/@nomad/ui, tema e paleta, TopBar Nomad, shell, RequireAuth, QueryClient, RenderMediaProvider, guards, errorReporter, nginx, Dockerfile, vite-plugins, testes de segurança | P0, P6 |
| P9 | Motor do Render no frontend: src/render/engine/**, primitives/**, contract.generated.ts + expr.generated.ts, CanvasView/CanvasFrame/RenderThumb/ScreenView/canvasManifest/formats/prefs | P8, P2 |
| P10 | Camada de dados + editor do Render no frontend: api/render*.ts, api/projectRender.ts, hooks/useRender*, render/hooks/**, types/render.ts, render/editor/** | P6, P8 |
| P11 | Telas do Render: raiz de pages/Render, Studio/**, shared/**, Import/Stacks/Tokens/Styles/Versions, projectMap/projectScreen, previews, rotas, paths.ts | P9, P10 |
| P12 | Credencial de preview (cookie apv_) + RequireAuth do preview + getToken no cliente — vem ANTES de P6 fechar | P11, P3 |
| P13 | Testes do Render (backend e frontend) + gates de cobertura e docs | P5, P6, P9, P10, P11 |
| P14 | Script de migração de dados + runbook de corte | P13, P7 |
Cinco pacotes precisam de decisão do dono antes de começarem: P1 (projeto/espelho, D1), P3 (suspensão e chaves próprias, D4), P4b (catálogo por empresa, D3), P7 (portão de upgrade, D2 reescrita) e P3 (semântica do criador desativado, D17).
| # | Risco | Gravidade | Mitigação |
|---|---|---|---|
| 1 | O /mcp inteiro do AgentPack depende de código do Render: mcp/tool-call.ts:17 e mcp/proxy-call.ts:15 importam toolError de tools/render/common.ts. Apagar a pasta sem mover a função derruba toda tool, inclusive as proxied | crítica | P-1, sem dependência, primeiro merge depois do desenho (não é mais passo do corte, que chegaria tarde) |
| 2 | 3 tools do core somem quando o registro do Render sai: project_create/update/delete moram em tools/render/core.ts:11-28 | crítica | §9 passo 3, com teste e snapshot atualizado |
| 3 | O boot do AgentPack falha se as rotas de import saírem e as chaves de plugins/rate-limit.ts:87-88 ficarem (o boot recusa bucket de rota não registrada) | alta | remover as duas chaves no mesmo commit das rotas |
| 4 | O gateway /mcp inteiro do AgentPack quebra se appBaseUrl sair com lib/render-view-url.ts (task-decision.service.ts:2) | alta | §9 passo 2 |
| 5 | A ponte de MCP não repassa a identidade: o Render vê a chave da configuração, não a do agente; escopo por projeto e teto por toolset deixam de valer por requisição | alta | aceito e documentado (§5.4); o override por vínculo cobre o caso estático |
| 6 | Tools proxied exigem write, não render:write (PROXIED_TOOL, authz.ts:169) | média | aceitar a sobreposição de escopo ou manter os dois caminhos |
| 7 | Catálogo de MCP do AgentPack é global, sem empresa (McpServer sem organizationId) | alta | P-2, antes de cadastrar o MCP do Render |
| 8 | Arquivos fora do banco: render_assets.storage_key aponta para /data/render-assets no volume agentpack-prod-data; copiar só o banco deixa os bytes em 404 | alta | §8 passo 4, com checksum por linha; e o rollback também descarta o volume (§7) |
| 9 | Perda de integridade da cascata: as 4 FKs CASCADE e o portão de lixeira viram espelho; o purge é transação + I/O de arquivo e roda por job, sem avisar ninguém | alta | contrato de purga explícito (§4.2): o AgentPack orquestra, o Render apaga só por chamada, webhook + reconciliador sinalizam |
| 10 | Catálogo global vaza entre empresas (project_id NULL visível para todas, escrita por qualquer usuário) | alta | P4b — não é adaptação mecânica: muda dado, índice, consulta e política |
| 11 | Convivência com duas fontes da verdade: enquanto o Render existir nos dois lados, dois agentes podem escrever | alta | congelamento de escrita (503 RENDER_FROZEN) antes da cópia final; janela declarada |
| 12 | autoDeploy: true nos serviços: merge na main pode virar deploy sem ninguém pedir | alta | serviços novos nascem desligados; deploy é deployments_trigger, decisão do dono; e cada conferência registra o commit do build (§7) |
| 13 | Três arquivos no teto de tamanho do lint em 100% (CanvasView.tsx 586, ClassEditor.tsx 358, StoriesStrip.tsx 335 — medido) e mais 3 entre 97% e 99% | média | manter os mesmos aliases e o mesmo eslint.config.js; as SIZE_EXCEPTIONS são medidas no app novo no primeiro build, não copiadas; a catraca pode subir com justificativa no PR, nunca sem ela; gate em P13 que falha se alguma exceção subiu sem justificativa |
| 14 | Acoplamento de layout: o backend gera 2 arquivos no frontend, o frontend importa do backend, o backend testa o motor do frontend | média | manter backend/ + frontend/ lado a lado; é o motivo de não virar workspaces |
| 15 | zod 3 no backend, zod 4 no frontend: o gerador do contrato quebra em silêncio se um lado mudar — e o typecheck do backend não aciona | média | npm run contracts:check no backend (roda os 2 geradores e falha se o diff não estiver commitado) + no prebuild do frontend; regra escrita no AGENTS.md do repo novo |
| 16 | Lock do frontend pelo npm 10.9.4: já falhou deploy em produção (2026-10-08) | média | npx npm@10.9.4 install --package-lock-only + ci --dry-run |
| 17 | @nomad/ui na main tem 1 arquivo (o commit b71f9c3 apagou 213): qualquer spec sem tag instala nada | média | pinar #v1.13.5; nunca #main |
| 18 | Captura de preview quebrada desde 7ac8e39: o shot grava sessionStorage['agentpack:auth-token'], que o frontend não lê mais, e /render/preview/** está atrás do RequireAuth | média | P12 redesenha o caminho inteiro (cookie httpOnly, §3.4); a app nova nasce sem o defeito |
| 19 | A credencial de preview vive numa página que executa conteúdo do usuário: com sessionStorage, qualquer XSS no motor lê o apv_ e ganha 5 min de leitura de qualquer tela do projeto | média | cookie httpOnly com desafio one-shot (§3.4, opção a) + teste de segurança que tenta ler a credencial de um <script> injetado |
| 20 | SHOT_SERVICE_URL sem validação: apontar para fora faz o backend mandar POST com o SHOT_TOKEN para host arbitrário; hoje o caminho nunca foi exercitado em produção (a env não existe) | média | validação no env-schema (host interno ou allowlist, sem http:// público) + teste que recusa URL pública |
| 21 | Troca de origem apaga estado local: rascunhos no IndexedDB agentpack-render/screen-drafts e preferências em localStorage | média | janela de aviso; ou ler o IndexedDB antigo uma vez no primeiro carregamento |
| 22 | Empresa fora das query keys do Render: funciona porque trocar de empresa recarrega a página pelo SSO; se o app novo trocar sem recarregar, o cache vaza | média | orgId na chave de query, como o resto do app novo |
| 23 | Estado de produção não conferido: não se sabe quantas linhas/arquivos existem, nem se o congelamento do catálogo legado rodou | média | §8 passo 1, com credencial autorizada e leitura apenas |
| 24 | RENDER_AGENT_TOKEN é uma chave de plataforma permanente: o BOOTSTRAP_AGENT alcança tudo e tem escopo de escrita | média | rotação com prazo no runbook; removível depois da primeira chave admin; segredo forte exigido no env-schema |
| 25 | A credencial de saída AgentPack→Render não tem dono: o secretHeader Bearer apk_* não estava em nenhuma lista de rotação ou revogação | média | registrada no runbook com escopo mínimo, dono da rotação e ordem de revogação (§5.4 item 7) |
| 26 | Sandbox de 2 GB (agent-package-audit) para o gate | baixa | o app é construído no SB render (20 GB); o SB pequeno só serve para leitura |
| 27 | openapi.json herdado não é JSON válido (falha em line 16801, desde bbc205c) | baixa | a app nova não tem api/generated/; o spec é gerado em build e validado contra o swagger, nunca contra arquivo commitado |
Cada uma com a recomendação do desenho. As travadas estão marcadas.
| # | Pergunta | Recomendação |
|---|---|---|
| D1 🔒 | Projetos: a app Render guarda o id do projeto do AgentPack num espelho local (render_projects, mesmo id), ou tem projetos próprios? | espelho com o mesmo id. Sem ele, nenhuma tela, mapa, biblioteca de projeto ou porte abre; com projetos próprios, nasce uma segunda noção de projeto e a migração perde os ids que os URLs e as decisões já carregam. Alimentado por migration + webhook + reconciliador, com contrato de sincronia explícito (503 REPLICATING, SLA e métrica) — §4.1 |
| D2 🔒 | Portão do upgrade: tabela própria do Render, pessoa logada, ou consulta à decisão de task do AgentPack por API? | reformulada, porque a premissa anterior estava errada. O caminho humano (REST sem decisionId) é o que existe e não precisa de tabela. O caminho do agente (tool com decisionId) é o que render_approvals substitui, com project_id NOT NULL, used_at para "uma decisão = uma transição", key e rótulos preservados. A pergunta nova: o dono aceita a tela de decisão nova na app Render? Se não, o caminho do agente volta ao AgentPack — ou a tool exige approvedByHuman e responde APPROVAL_REQUIRED para qualquer agente. §4.4 |
| D3 🔒 | Catálogo global (project_id NULL, hoje de plataforma e visível por todas as empresas): vira por empresa, ou some? | vira por empresa (organization_id). É mudança de dado, índice, consulta e política — virou o pacote P4b, com GlobalWriteReach do Render passando de platform para org (sem isso a chave admin da empresa toma 403 na própria empresa) e a assinatura de readLibrary/writeLibrary recebendo a organização ativa (35 chamadores). §4.5 |
| D4 🔒 | Chaves de agente: a app emite apk_* próprias (mesmo ciclo da Conta) ou valida as do AgentPack? | próprias. A regra do dono (contrato suspende, recontrato reativa, saída revoga só as pessoais) só é implementável em chaves que o app controla |
| D5 | Nomes das tools: manter agentpack_render_*? | manter, e agora está claro quem prefixa: o BUILTIN_TOOL_PREFIX do catálogo do Render. A revisão anterior não percebia que o proxiedWireName sempre prefixa e não normaliza, o que dobrava o nome na ponte |
| D6 | Transição: o AgentPack cadastra o MCP do Render no proxy dele? | sim, no corte (fase B), com toolPrefix: agentpack_render — mas só depois da normalização do prefixo upstream (P-2), senão os 59 nomes viram agentpack_render_agentpack_render_*. Ressalvas: escopo write em vez de render:write; a identidade do agente não atravessa; toda chamada proxied vira auditoria de escrita; X-AgentPack-Toolsets e X-AgentPack-Project precisam atravessar por header fixo no vínculo. §5.2.1 |
| D7 | Skills agentpack-render-*: ficam no AgentPack ou vão para a app? | ficam no AgentPack, sem mudança na fase 1. check:skills valida as tools citadas contra o catálogo — que, com a ponte, continua publicando agentpack_render_*. Ressalva registrada: 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 — nesse momento precisam ser reescritas ou aposentadas. A app nova publica docs/mcp/tools.md e llms-full.txt próprios |
| D8 | Nomes e domínios dos serviços: render-backend-ws/render-frontend-ws em workspace_nommand, domínios render-v0 e render-v0-api? | sim. Segue o padrão dos vizinhos (<app>-backend-ws, <app>-frontend-ws, *-v0), não colide com o sandbox sb-render, e deixa o domínio curto (render.mp.serendiped.com) para quando o v0 sair |
| D9 | shot/: entra em produção na app nova? | entra, como serviço próprio, mas é a última coisa a subir. Today não existe em produção; ligar de uma vez entrega as duas tools de captura que hoje devolvem só a URL. Deixar desligado é aceitável e mais barato (1 GB) |
| D10 | @nomad/ui: v1.13.5 (AgentPack, Motor) ou v1.13.7 (LB, Conta)? | v1.13.5. Mesmo preset de lint e tsconfig, mesmo check-lock; a diferença é um componente de teclado. Bump depois é uma linha |
| D11 | Banco: Postgres próprio da app em databases_prod? DB_CONNECTION_LIMIT 1000 também? | sim, e sim, com max_connections acima de 1000 (o padrão do Postgres é 100 e o pool do Prisma ignora o que vier na URL) |
| D12 | autoDeploy: serviços novos com ele ligado? | desligado. Merge na main não publica; publicação é deployments_trigger, que é decisão do dono |
| D13 | Migração: janela de congelamento e dados de projeto na lixeira? | congelar as escritas do AgentPack por uma janela curta e declarada (feature flag, 503 RENDER_FROZEN), repetir a cópia, e só então cortar. Projetos na lixeira são copiados com o mesmo deleted_at e só aparecem depois do restore. O rollback descarta banco e volume |
| D14 | Escopo do Render: o catálogo global legado (/api/library/:kind, /api/designs global) vai junto? | vai, mas escopado por empresa (D3, P4b). Deixar como está copia um catálogo que nenhuma empresa pode mais ler direito |
| D15 | Consertar a captura de preview entra nesta frente? | sim, e agora com o caminho completo: cookie httpOnly com desafio one-shot (§3.4 opção a), não sessionStorage — porque a página de preview executa o motor do Render e um XSS leria o token. É correção de credencial e de superfície de ataque, não só de hook |
| D16 | **Corrigir junto o AGENTPACK_CONTA_URL do frontend do AgentPack (env no painel + rebuild)? | sim, é uma env e um rebuild, e está quebrado em produção hoje ("Minha Conta" e "Gerenciar na Conta" apontam para um sandbox morto). A app nova já nasce com o build arg certo |
| D17 🔒 NOVA | Criador desativado e chave admin: no AgentPack, AgentKey.createdBy tem onDelete: SetNull e authenticateAgentKey não consulta o estado do criador — desativar o criador não derruba a chave admin da empresa. A app nova herda isso? | herda, com uma regra a mais que é nova: o criador desativado não pode mais gerenciar chaves, mas a chave admin da empresa continua valendo (é a razão de ela ser da empresa). Se o dono preferir que desativar o criador derrube a chave admin, isso é checagem nova — authenticateAgentKey passa a consultar o estado do criador — e muda o contrato de D4. §3.2 |
Leitura integral dos 4 mapas do workflow (mapa/render-backend.md 1.136 linhas, render-frontend.md 705, render-acoplamento.md 540, padroes-infra.md 586) e do diagnóstico da frente de MCP (wf-mcp/diagnostico.md), mais leitura direta do código em /workspace/.worktrees/wf-leitura (origin/main 26268c0).
Verificado no código nesta revisão (cada afirmação das §14 tem âncora de linha): backend/src/mcp/ (15 arquivos) e os imports de mcp-server.ts:63-79; backend/src/tools/render/ (13 arquivos, 1.903 linhas) e o mapa de toolsets; lib/render-legacy-catalog.ts e seus 5 importadores; services/render-agent-bundle.service.ts e seus 4 importadores; mcp/catalog.ts:73,160,292,357-365 (prefixo, colisão e proxiedWireName); mcp/toolsets.ts:80-93 (byDefault: false), middleware/authz.ts:169 (PROXIED_TOOL); render-agent-system-versions.service.ts:325-421,460-485 (o portão e seus dois ramos); routes/render.ts:259,283-294,339,971-991 (o caminho humano, o Vary e a rota de upgrade); hooks/useRenderDesignSystemVersions.ts:58-80 e UpgradeDesignSystemDialog.tsx:16; routes/render.ts + render-scoped.ts contados par a par (47); lib/render-db.ts:10,29-41,80-81, services/render-access.ts:60-90, middleware/actor.ts:228-258, plugins/auth.ts:41,106,345-353; services/project.service.ts:243-290 (purge e purgeExpired); shot/server.mjs:74-136 e frontend/nginx.conf:141-222 (incluindo a location /mcp); config/env-schema.ts:386-389,526-527, lib/render-screenshot.ts:65-89, services/readiness.service.ts:166-176; backend/knip.json, backend/coverage-floor.json (11 diretórios, src/mcp em 98); frontend/eslint.config.js:100-114 e frontend/src/test/render/ (59 arquivos, 5 de suporte); backend/tests/__snapshots__/mcp-registry.test.ts.snap (222 tools, 59 render_*); prisma/schema.prisma (McpServer sem organizationId; AgentKey com SetNull em createdBy); frontend/src/api/generated/openapi.json (json.load falha em line 16801).
Do LB (leitura) a implementação da suspensão por contrato, backend/src/services/oidc/{provisioning,jit}.ts. Painel: services_list, projects_list, domains_list do projeto workspace_nommand (leitura).
Verificado em produção, por leitura de logs e metadados: os dois serviços do AgentPack com autoDeploy: true, 23 envs no backend, SHOT_SERVICE_URL e RENDER_REQUIRE_IF_MATCH ausentes (capturas desligadas, If-Match no padrão true), domínios agentpack-v0 e agentpack-v0-api. Não verificado: o banco de produção (contagens e arquivos do Render), por estar fora da autorização desta tarefa.
Duas rondas de crítica adversarial: critica-arquitetura.json (15 problemas) e critica-paridade.json (13 problemas). Todas as 28 objeções foram aceitas e corrigidas, com uma ressalva de crédito e três onde a objeção tinha razão no fundo mas o desenho antigo já cobria o ponto (dito com todas as letras). A seção 15 lista o que mudou, para conferência.
A1 — mcp/** incompleto no P6: prompts.ts, resources.ts, registry.ts e proxy-call.ts omissos sem destino declarado. Verificado: mcp-server.ts:67 importa PROMPTS/getPrompt/completePromptArgument de ./mcp/prompts.js; :68-76 importa ./mcp/resources.js. prompts.ts importa project.service, skill-files, task.service; resources.ts importa knowledge.service, memory.service, skill.service, project.service. mcp/ tem 15 arquivos e o desenho listava 11. Aceita. §5.3 agora traz a tabela de 15 arquivos com destino explícito: os 4 saem (e por quê), os 11 ficam. mcp-server.ts é marcado adaptado, com os 5 handlers que saem (prompts/get, resources/list, resources/read, resources/templates/list, completion/complete, linhas 172-190) e os 5 que ficam. E o teste que fecha o buraco: mcp-registry.test.ts ganha a asserção de que PROMPTS e RESOURCES são listas vazias — sem ela, o SDK aceita vazio e a publicação de prompts de task passa em silêncio.
A2 — toolError estava dentro do passo do corte (P14), que depende de P13 e P7 e não acontece antes da fase A; remover RENDER_TOOLS antes quebra o catálogo inteiro de tools do AgentPack. Verificado: tool-call.ts:17, proxy-call.ts:15, e tool-call.ts:114,121 chamam toolError para a negação de escopo e qualquer erro — tool-call.ts é o despachador de toda tool. Aceita. O item saiu do P14 e virou o pacote P-1, depende_de: [], primeiro merge depois deste desenho: mcp/tool-error.ts novo, os dois importadores apontando para ele, e tools/render/common.ts reexportando até o Render sair (para o tools/render da fase A compilar). Teste mcp-tool-error.test.ts nas duas bordas.
A3 — lib/render-legacy-catalog.ts não estava em nenhum pacote. Verificado: o arquivo existe e tem 5 importadores: lib/render-db.ts:10, services/render-system/persistence.ts:11, services/render-catalog/validate.ts:15, services/render-legacy-freeze.service.ts:19, src/scripts/seed-render.ts:48. Sem ele, três comportamentos morrem sem erro: o catálogo legado global continua gravável, um projeto pode criar design system com o nome reservado e colidir com o congelado, e a herança global→projeto deixa de depreciar. Aceita. Entrou em P4 (e em §2.4 como cópia literal). E §4.5 registra que os predicados de assertLegacyCatalogWritable e isReservedDesignSystemName giram em torno de project_id IS NULL e precisam de org_id sob D3 — dependência dura da decisão de catálogo, registrada.
A4 — services/render-agent-bundle.service.ts não está na lista. Verificado: é o orquestrador único de bundle e asset, com 4 importadores (tools/render/bundles-assets.ts:38, services/render-rest.service.ts:31, services/render-bundle-artifact.service.ts:23, services/project.service.ts:26). Sem ele, P5 declara /render/import e /render/export, P6 declara render_bundle_export/import/read e as 4 tools de asset, e nenhuma das duas tem o arquivo que as implementa. Aceita. Entrou em P4 inteiro, incluindo os reexports de asset-files (linha 55: assetsDir, resolveAssetPath, makeStorageKey, sha256, decodeAssetBytes, assetBytesUrl, assetSummary, writeAssetFile, readAssetFile, removeAssetFile) — porque project.service.ts:26 e render-bundle-artifact.service.ts:23 importam assetsDir e removeAssetFile dele e os dois precisam mudar para o espelho antes do corte.
A5 — tools/render/design-systems.ts não está na lista; P6 pegava 10 dos 13 arquivos. Verificado: design-systems.ts é onde moram 18 das 59 tools — todos os ciclos de vida de design system, render_port, render_port_list, render_design_system_component_view e render_design_system_screenshot. Aceita. Entrou em P6. E o desenho agora declara a exceção explicitamente: core.ts não vai, com destino tools/core/projects.ts (§9 passo 3) e atualização do snapshot mcp-registry.test.ts.snap — para o implementador não procurar as 3 project_* no lugar errado.
A6 — P7 promete preservar um caminho que não é o que existe hoje. Verificado: em render-agent-system-versions.service.ts:390-419, assertUpgradeApproved só é chamado quando a chamada traz decisionId (:396). O frontend nunca traz: useRenderDesignSystemVersions.ts:58-80 manda o upgrade sem decisionId, com o comentário "decisionId (o usuário que chama a rota É a aprovação)"; UpgradeDesignSystemDialog.tsx:16 repete. routes/render.ts:971,988 chega com approvedByHuman e runAsRenderCaller (:339) é o transforma. DecisionReview/TaskDecisionPanel vivem em features/tasks, que não vem — não há tela de decisão de upgrade no frontend atual. Aceita, e a objeção é a mais importante das duas rondas. §4.4 foi reescrito em duas partes: (1) o caminho humano é preservado sem tabela nenhuma — mesma verificação de pins, mesmo 409, mesmo envelope; (2) o caminho do agente é o que render_approvals substitui, com o schema da tabela (incluindo project_id NOT NULL, ver A-adiante). A "tela de decisão" passa a ser explicitamente uma tela nova em pages/Render/Versions, e D2 foi reescrita para fazer essa pergunta ao dono.
A7 — a contagem de rotas REST está errada (54 em vez de 47). Verificado: parser sobre routes/render.ts + routes/render-scoped.ts, pares método+caminho, ${prefix} contado uma vez → 47. Contando os dois prefixos das rotas legadas como pares separados, não se chega a 54. Aceita. §5.1 fixa 47 com o critério de contagem escrito, traz a lista completa dos 47 pares para conferência, e P5 ganha backend/tests/render-routes.test.ts que conta sobre o registro do Fastify e falha se o número mudar sem alguém editar o design. §0 e o resumo também foram corrigidos.
A8 — a mudança de escopo do catálogo global (D3) não tem pacote de dado. Verificado: visibleProjectsWhere() (render-access.ts:68-72) monta OR:[{projectId: null}, {projectId: {in: ids}}]; activeOwnerWhere() (:80) monta OR:[{projectId: null}, {project: {deletedAt: null}}] — os dois tratam "global" como project_id nulo, sem org. Os 3 índices parciais são ON ... (X) WHERE project_id IS NULL. A escrita global é de alcance platform (actor.ts:234), que dá a qualquer usuário logado. Aceita. Virou o pacote P4b, com os 8 itens da tabela da §4.5: backfill de organization_id, os 3 índices reescritos, os dois where por organização, GlobalWriteReach do Render de platform para org, a assinatura de readLibrary/writeLibrary, o catálogo legado congelado por empresa, e o teste de duas empresas em render-isolation.test.ts.
A9 — P12 descreve a correção do credencial de preview no mecanismo errado. Verificado: o frontend atual usa createHttpClient do @nomad/ui/data (api/client.ts:19-51) e o comentário em :37-42 diz que sem getToken o pacote não injunta Authorization — ou seja, não é "voltar a ter o gancho", é configurar o gancho num cliente hoje deliberadamente sem token. E o apv_ é gravado pelo shot no sessionStorage do Chromium antes do app carregar, enquanto o app usa cookie httpOnly: a página só funciona se o RequireAuth aceitar o caminho do apv_ — e hoje ele redireciona para a Conta. Aceita, e a objeção tinha mais razão ainda do que dizia. §3.4 foi reescrito com três alternativas comparadas e escolheu a cookie httpOnly (opção a) em vez do sessionStorage da revisão 1 — porque a página de preview executa o motor do Render, ou seja, conteúdo do usuário, e um XSS leria o token do sessionStorage. Os 4 passos do caminho novo estão escritos e é P12 que os implementa. P12 também subiu na ordem: precisa estar pronto antes de P6 fechar, porque sem preview as duas tools de screenshot do registro não funcionam e o smoke de validação do P14 (3 PNGs) não tem o que comparar.
A10 — §6.2 lista SHOT_TIMEOUT_MS como env do shot/, mas ele é do backend. Verificado: env-schema.ts:527 e render-screenshot.ts:72; o shot/server.mjs não lê SHOT_TIMEOUT_MS (lê SHOT_HOST:81, SHOT_READY_TIMEOUT_MS:89, SHOT_HARD_TIMEOUT_MS:91, SHOT_MAX_QUEUE:93). Aceita. §6.2 agora separa "Só do Render" (backend: SHOT_SERVICE_URL, SHOT_TIMEOUT_MS, SHOT_TOKEN) de "shot/ (as que o shot lê)", e CHROMIUM_PATH foi incluída na lista do shot com a nota de que não precisa ir no runbook de produção, para o env-example não acusar variável faltando.
A11 — o espelho de projeto não tem dono de purge, e o purge no AgentPack não é observável por webhook. Verificado: project.service.ts:245-282 é transação (advisory lock, valida deleted_at, conta taskIds e renderAsset, apaga linhas, cascata) e depois I/O de arquivo. purgeExpired (:285) roda por job de retenção, sem avisar ninguém. Aceita. §4.2 define o contrato em 4 itens (AgentPack orquestra e chama DELETE /api/render/projects/:id/purge antes de apagar a linha local, falhando alto ou com fila de retry; o Render apaga só por chamada, nunca por job próprio; depois do corte o AgentPack para de apagar os arquivos; o que acontece se nunca chamar é sinal do reconciliador). §9 passo 10 deixou de dizer "ou vira chamada à app nova" e passou a nomear a chamada. O teste de P7 ganhou o caso.
A12 — o frontend nasce sem o proxy /mcp no nginx. Parcialmente aceito — o desenho antigo já cobria o item, faltava o teste. Verificado: §6.3 da revisão 1 já listava "proxy de /api, /auth/sso, /auth/callback e /mcp", e o nginx.conf do AgentPack já tem a location ~ ^/mcp(?:[/?]|$) (linha 193). O que realmente faltava era exigir isso como item nomeado do P8 e o teste. Aceito nessa parte. §5.3 e P8 agora nomeiam o item ("location /mcp apontando para o backend, com o mesmo tratamento de headers da rota /api, e MCP_ALLOWED_HOSTS conferindo o host") e P8 ganha um teste de segurança que falha se /mcp não estiver no proxy, com a razão explícita: a Fase A é justamente quando o agente aponta direto para o /mcp do Render. E fica declarado que o frontend precisa do build arg RENDER_MCP_URL mesmo com o proxy.
A13 — a cobertura de frontend não fecha: 59 arquivos, 1 exceção daria 58, mas 5 são de suporte e ficam sem dono. Verificado: frontend/src/test/render/ tem 59 arquivos, dos quais 5 não são de teste: desempenho.ts, qa-suporte.tsx, screen-budget-suporte.tsx, studio-suporte.tsx, template-custo-suporte.ts. Aceita. O glob passou a frontend/src/test/render/** (todos os 59), com a exceção nomeada (connect-agent-endpoint.test.tsx fica no AgentPack, por testar o ConnectAgentModal) e os 5 de suporte accompanyando os testes que os usam. hermetic-tests.test.ts entrou no gate — ele varre src/test e falha se um arquivo de teste ficar sem o import do setup, então um arquivo esquecido aparece como falha em vez de passar em silêncio.
A14 — RENDER_AGENT_TOKEN renomeado só na env, com AGENTPACK_ espalhado. Verificado: AGENTPACK_AGENT_TOKEN aparece em env-schema.ts:389, app.ts:49,193,197, middleware/actor.ts:13,144,272, plugins/auth.ts:106, plugins/swagger.ts:255. Aceita. §6.2 lista os 6 pontos e decide: renomear em tudo, com config-env.test.ts afirmando que nenhuma env AGENTPACK_ sobrevive no env-schema do app novo.
A15 — knip.json e coverage-floor.json copiados literais medem o que não existe. Verificado: coverage-floor.json tem 11 diretórios, incluindo src/tools/core (90) e src/mcp (98 — que inclui proxy-call e resources, que não vão). knip.json declara entry: ["src/scripts/*.ts", ...], o que faz o gate passar sem checar nada depois que os scripts mudarem. Aceita. §6/P13 passam a derivar em vez de copiar: manter src, src/config, src/lib, src/mcp, src/middleware, src/plugins, src/routes, src/services, src/tools/render, src/utils; remover src/tools/core; medir src/mcp no dia do primeiro build (o 98 do AgentPack inclui o proxy, que não vem). E no knip, src/scripts/*.ts sai do entry depois que os scripts da app nova estiverem listados um a um.
P1' — o prefixo de wire é duplicado na ponte: os 59 nomes mudam. Verificado: proxiedWireName (mcp/catalog.ts:357-365) monta `${prefix}_${safe}` sempre, sem normalizar; 16 + 18 = 34 caracteres, muito abaixo do teto de 64, então o nome sai literal (sem hash). O BUILTIN_TOOL_PREFIX é "agentpack_" (catalog.ts:73, aplicado em :160). Aceita — é o bloqueante mais grave dos dois lados, porque quebra a compatibilidade que D5 promete. §5.2.1 agora fixa quem aplica o prefixo em uma tabela, mostra o caminho fechado e o caminho quebrado, e traz D6 corrigida: o Render publica agentpack_render_* e a ponte é cadastrada com toolPrefix: agentpack_render, e o catálogo do AgentPack passa a normalizar o prefixo upstream (pacote P-2, pré-requisito do corte). As duas metades do documento (P6 "mesmos nomes de wire" e D6 "toolPrefix agentpack_render") só fecham com essa terceira peça. Testes: no app novo, montar o /mcp, rodar tools/list e aferir os 59 nomes; no AgentPack, teste de contrato de que, com o MCP do Render cadastrado, tools/list devolve o snapshot atual. E o preflight de P14 confere que os dois lados concordam sobre quem prefixa.
P2' — kind admin × assertGlobalWriteAllowed: a chave admin da empresa toma 403 no catálogo da própria empresa, e o vazamento continua. Verificado: canWriteOutsideProjects (actor.ts:234-236) só autoriza escrita fora de projeto com reach === "org" e chave admin; o catálogo global do Render usa reach "platform" (o próprio comentário de assertGlobalWriteAllowed em :240-243 diz que o global do Render é "platform"). assertDesignSystemWriteAllowed (plugins/auth.ts:345-353) decide por projectId === null. Aceita. A reescrita de canWriteOutsideProjects e assertDesignSystemWriteAllowed para o escopo por empresa entrou como item nomeado de P4b (item d), não ficou em "authz menos os ramos do Render". E P3/P4b ganham o caso de duas empresas: chave admin de A em catálogo sem organizationId ou de organizationId B → 403; chave admin de A em catálogo de A → 200. Sem essa reescrita, D3 não é implementável e o risco 10 fica exatamente como está.
P3' — dois códigos de projeto na mesma URL: projeto novo no AgentPack não aparece no Render e o 404 é enganoso. Verificado: as 4 relações em Project (schema.prisma:243-246) e o gate requireProject (render-db.ts:29-41) agem sobre Project; no app novo viram render_projects. O webhook é assíncrono e pode falhar, e requireProject devolve NotFoundError por padrão (render-db.ts:30), que o RequireAuth e o QueryClient transformam em erro de tela cheia. Aceita. §4.1 (nova) torna o contrato de sincronia estado de primeira classe: janela de tolerância com 503 REPLICATING em vez de 404 (5 min após o último sync bem-sucedido), SLA e métrica de idade do último sync no /health/ready (vermelho fora do SLA) e em Prometheus, e emissão síncrona do webhook na criação de projeto. Testes registrados em P7.
P4' — o passo 5 do preparo (cadastrar o MCP) não é executável hoje e cria uma credencial de saída sem dono. Verificado: o desenho reconhecia que o cadastro só funciona depois de organizationId em McpServer, e o schema.prisma confirma que a coluna não existe. Aceita. O cadastro saiu do passo 5 da §9 e virou D6, fase B; organizationId em McpServer virou o pacote P-2, anotado como pré-requisito do corte e não do app novo. E a credencial de saída (secretHeaders Bearer apk_* da app Render, escopo mínimo leitura + webhook de projeto) entrou no §5.4 item 7 e no §7, com dono da rotação e ordem de revogação (apagar o MCP no AgentPack antes de desligar o Render).
P5' — o apv_ numa página que executa conteúdo do usuário: sessionStorage é legível por qualquer script da origem. Aceita — e a objeção levou a uma decisão melhor do que a objeção pedia. §3.4 agora compara as três opções (cookie httpOnly one-shot / sessionStorage com restrição / sessão de serviço dedicada) e escolhe a primeira, que a revisão 1 não tinha considerado. O teste de segurança exigido é explícito: um <script> injetado na página de preview não consegue ler a credencial. Risco novo registrado como 19.
P6' — SHOT_SERVICE_URL sem validação de host. Verificado: readiness.service.ts:166-176 consulta GET <SHOT_SERVICE_URL>/health, e o AgentPack em produção não tem a env — o caminho nunca foi exercitado. Aceita. §3.5 (nova) define a validação no env-schema no estilo do EGRESS_ALLOWLIST (host interno ou allowlist, sem http:// público) com teste que recusa URL pública, e o runbook declara que SHOT_SERVICE_URL é opcional e que sem ele as tools devolvem só a URL. Risco 20.
P7' — a garantia de que a decisão pertence ao projeto vinha da relação com a task, e o details.decision aponta para a rota de tasks do AgentPack. Verificado: …:406 exige decision.task.projectId === opts.projectId; o link é appBaseUrl + rota de tasks (task-decision.service.ts:2,284). Aceita. §4.4 especifica project_id NOT NULL em render_approvals, a verificação passa a ser render_approvals.projectId === opts.projectId, e "uma decisão = uma transição" passa a ser garantida por used_at em UPDATE condicional. Os três testes pedidos entraram: decisão de outro projeto → 404 (sem oráculo de existência), decisão já usada → APPROVAL_REQUIRED com stale: true, agente respondendo → 403. E fica registrado que o details.decision da app nova aponta para a tela do Render, mudando o contrato de erro — as skills que leem esse link precisam ser conferidas.
P8' — o formato do claim apps não está especificado e o platform-admin não tem semântica. Aceita. §3.1 ganhou a tabela do contrato: apps: { "render": "owner" | "admin" | "member" } (ou a forma objeto), como deriva para memberships.externalAppRole e para role, o que acontece quando apps não tem a chave render, e que usuário role: "admin" de plataforma cai na organização padrão com papel owner — o app não pode assumir userRole admin sem essa semântica. Teste de contrato com payload de exemplo de cada forma, mais teste do caminho platform-admin → resolveActive.
P9' — o purge é um job de retenção que roda sozinho e o Render fica com órfãos invisíveis. Verificado: purgeExpired (project.service.ts:285) roda por job, sem avisar o Render; o CASCADE local não dispara porque o pai sumiu no outro banco; requireProject devolve 404 (invisível) e os bytes ficam no volume para sempre. Aceita. É a mesma resposta de A11: §4.2 define que o AgentPack orquestra (é dono do dado de projeto), que o Render só apaga por chamada explícita, que o Render não faz polling, e que o sinal — quando o AgentPack nunca chamar — vem do reconciliador. Teste em P7: "purga no AgentPack sem purga no Render deixa linhas, e a próxima reconciliação sinaliza".
P10' — zod 3 no backend, zod 4 no frontend: o gate do backend passa sem acionar. Aceita. §11 risco 15 ganhou a barreira real: contracts:check no backend (roda os 2 geradores e falha se o diff não estiver commitado) e no prebuild do frontend, mais a regra escrita no AGENTS.md do repo novo: "mudou um documento de contrato, rode npm run contracts:gen no backend e commite os 2 arquivos do frontend".
P11' — a reversibilidade do rollback não cobre o volume. Aceita. §7 e §8 item 8: o volume novo é declarado descartável, com as 3 regras (backup ou nota de que está vazio antes do corte; o rollback descarta ou renomeia o volume, ou reverte o diretório de assets do AgentPack ao estado pré-corte; teste de idempotência sobre o volume, não só sobre as linhas).
**P12' — os três arquivos no teto do lint, com D3/D14/D15 exigindo linhas extras, e a mitigação "não cresca" é incompatível com as decisões.* Aceita. §11 risco 13 e P9/P11: as SIZE_EXCEPTIONS são medidas no app novo no primeiro build (hoje: 586 / 358 / 335), a catraca pode subir com justificativa no mesmo PR e nunca sem ela, o AGENTS.md do repo novo diz que as exceções são medidas e não copiadas, e P13 ganha um gate que falha se alguma exceção subiu sem justificativa no histórico do commit.
P13' — o redirect de /api/render/assets/** nunca autentica. Verificado: com 307/308 o navegador repete a requisição no Render com o cookie do AgentPack, que o Render não valida (a sessão é do AgentPack) → 401 do Render para toda mídia. E é justamente por isso que existe o RenderMediaProvider/useAuthedObjectUrl, para baixar bytes com credencial da origem do Render. Aceita. §9 passo 18 deixou de ser redirect: (a) o item Render da Sidebar vira link externo e as telas que carregam mídia apontam para a URL do Render; e (b) se for preciso manter /api/render/assets/** no AgentPack, tem de ser um proxy autenticado que revalida a credencial no Render — nunca um redirect.
P14' — a herança global→projeto com catálogo por empresa muda a assinatura de 35 chamadores. Verificado: render-db.ts:80-81 — readLibrary cai na global quando não há linha local, para todo kind exceto manifest; render-db.ts:3-5 diz que 35 services/lib importam o módulo. Sem org, o filtro global {kind, projectId: null} traria a global de outra empresa. Aceita. §4.5 item de readLibrary/writeLibrary especifica a mudança de assinatura (recebem a organização ativa, do actor ou do escopo) e o filtro passa a ser {kind, projectId: null, organizationId}, com os 35 chamadores como trabalho explícito de P4b e dois testes: duas empresas com catálogo global de mesmo nome não se enxergam, e a herança global→projeto só usa a global da mesma empresa.
P15' — o default de toolset do Render e o header de toolset na ponte. Verificado: render e render-admin são byDefault: false (mcp/toolsets.ts:85-93), e buildUpstreamConfig repassa config.headers + segredos + override + x-request-id/traceparent; resolveDefaultProjectId (mcp/default-project.ts:52-60) usa o header só quando o principal alcança o projeto. Aceita. §5.2.2 define o default do Render (render e render-admin byDefault: true — o Render é o app inteiro, não um domínio dentro dele) e manda o override do vínculo AgentPack→Render repassar X-AgentPack-Toolsets e X-AgentPack-Project, senão o Render aplica o default dele e um agente que usava render por header receberia um catálogo sem as 59 tools. Teste de contrato da ponte em P6.
P16' — Vary: Host no $id reflexivo não tinha teste. Parcialmente aceito — o desenho antigo já emitia. Verificado: routes/render.ts:283-294 já mescla o Vary com o Origin do CORS. O que faltava era o teste. Aceito nessa parte. §5.1 mantém as 2 rotas na allowlist e P5 ganha o teste que confere Vary: Host na resposta, com a nota de que ele existe porque o $id é reflexivo e o cache intermediário precisa respeitar.
P17' — RENDER_AGENT_TOKEN é uma chave de plataforma permanente sem prazo. Aceita. §6.2 e §7: rotação com prazo, removível depois da primeira chave admin da primeira empresa (o bootstrap é preciso para criar a chave admin da primeira empresa), com segredo forte exigido no env-schema e teste. Risco 24.
P18' — a semântica do criador desativado na chave admin não está declarada. Verificado: AgentKey.createdBy tem onDelete: SetNull e authenticateAgentKey não checa o estado do creator — desativar o criador não derruba a chave admin. Aceita. §3.2 declara: a app nova herda (SetNull) e ganha uma regra nova, testada própria — o criador desativado não pode mais gerenciar chaves. Se o dono preferir o contrário, é checagem nova que o AgentPack não tem. Virou a pergunta D17 ao dono.
P19' — o smoke do caminho completo não está no runbook. Verificado: nginx.conf usa resolver ${NGINX_LOCAL_RESOLVERS} (o comentário do arquivo registra que lista fixa de nameserver já deu 502 em produção duas vezes) e o Dockerfile mantém BACKEND_UPSTREAM sem default, porque o AGENTS.md registra que o default apontava para o container do backend velho em silêncio. Aceita. §7 PASSO 6 ganha o smoke que prova o caminho completo: GET /health/live pelo domínio do frontend (não pelo do backend) devolve 200, e GET /api/auth/me pelo mesmo caminho devolve 401 (deny-by-default). Isso prova que o BACKEND_UPSTREAM está certo.
P20' — com autoDeploy desligado, o painel constrói o commit antigo se o Dockerfile mudou sem commit. Aceita. §7: cada conferência pós-deploy registra o commit do build (o painel mostra) junto com o hash do bundle, para que a divergência entre "o que eu commitei" e "o que foi construído" fique visível. Mesmo padrão do AgentPack.
P21' — a app nova vai ter plugins/swagger.ts; de onde vem o openapi.json? Aceita. §2.1: frontend/src/api/generated/ não existe na app nova. Se o swagger é gerado, ele é gerado em build/CI e não commitado, e o api:check valida contra o swagger gerado. A decisão fica escrita no AGENTS.md. (E o openapi.json herdado do AgentPack é inválido desde bbc205c: json.load falha em line 16801.)
| Onde | Antes | Agora |
|---|---|---|
| §0, §5.1, P5 | 54 rotas REST | 47, com critério e lista completa, e render-routes.test.ts que conta |
| §4.4, P7, D2 | portão do upgrade lia task_decisions no caminho do frontend | o caminho humano não usa tabela nenhuma; render_approvals substitui só o do agente; D2 reescrita |
| §3.4, P12, D15 | credencial de preview em sessionStorage + getToken | cookie httpOnly one-shot + RequireAuth do preview + getToken; P12 sobe antes de P6 |
| §5.2.1, §5.4, D5, D6 | prefixo agentpack_render na ponte | duplicava os 59 nomes; corrigido com normalização no catálogo do AgentPack (P-2) |
| §5.3, P6 | mcp/ com 11 arquivos listados | 15 com destino explícito; prompts/resources/registry/proxy-call saem; metrics adaptado |
| §4.5, P4b | catálogo por empresa como "mudança a fazer" | pacote próprio P4b com 8 itens e 6 pontos de código verificados |
| §4.1, P7 | espelho de projeto com webhook + reconciliador | contrato de sincronia com 503 REPLICATING, SLA e métrica |
| §4.2, §9.10 | purge "avisa e o Render apaga" | contrato de purga nomeado, com o AgentPack orquestrando |
| §3.1 | "usa o claim apps["render"]" | tabela do contrato do claim, com platform-admin |
| §3.5, §6.2 | SHOT_SERVICE_URL e SHOT_TIMEOUT_MS na tabela errada | SHOT_TIMEOUT_MS só no backend; SHOT_SERVICE_URL validado |
| §6.2 | RENDER_AGENT_TOKEN renomeado na env | 6 pontos de renome + teste de que nenhum AGENTPACK_ sobrevive |
| §2.1, §7 | volume e rollback sem cobertura | volume descartável + 3 regras de reversibilidade |
| §9.18 | redirect de /api/render/assets/** | link externo ou proxy autenticado, nunca redirect |
| §10 | 15 pacotes P0..P14 | + P-1 e P-2 (preparo do AgentPack, sem dependência) + P4b (escopo de empresa), + P12 na ordem certa |
| §12 | 16 decisões | + D17 (criador desativado) e D2, D3, D6 reescritas |
| §11 | 22 riscos | 27, incluindo a credencial de preview em página de conteúdo do usuário, o SHOT_SERVICE_URL sem validação e o bootstrap permanente |
<!-- fim do documento -->