Desenho da app Render

O documento completo do desenho, renderizado do markdown. Arquitetura, stack, autenticacao, banco, rotas, tools, deploy, migracao, riscos e as 28 criticas respondidas.

Fonte: /workspace/.sbcache/wf-render/desenho/desenho.md · revisao final 2026-10-13

← Voltar ao relatorio

App Render — desenho da app separada do AgentPack

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.

0. O pedido em uma tela

O que entra na app nova43% 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 AgentPackprojetos, decisões humanas de task, chaves apk_*, o núcleo Fastify/Prisma/MCP, o catálogo de skills, a auditoria
O que NÃO vainada 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
Compatibilidademesmos 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 virada4 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.

1. Arquitetura

                          ┌─────────────── 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

2. Estrutura do repo e stack

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.

2.1 Raiz

CaminhoAçãoObservação
AGENTS.md + CLAUDE.mdcopiar e adaptarsandbox 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.mdescreverapp nova, não "hub de contexto"
CHANGELOG.mdcopiar, começa em 0.1.0Keep a Changelog 1.1.0
docs/copiar o esqueleto, sem o do RenderADRs 010/011/012/013 + 0014 (versionamento de design system, é do Render)
llms.txt, llms-full.txtsymlink para backend/public/gerados por npm run docs:gen
server.jsoncopiar e trocar host/descriçãosó 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 existeo 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.jsoncopiarsem workspaces, um lock por pacote
docker-compose.test.ymlcopiarname: render-test, banco render_test, portas próprias
HANDOVER.md, .validation-evidence/não copiarhistórico do AgentPack

2.2 Backend — mesma versão do AgentPack

PeçaVersão
Node^24.11.0, imagem node:24.21.0-alpine3.24@sha256:ebfe2f90…
Fastify5.12.5
Prisma CLI e client6.19.3 (iguais)
zod3.25.76 (o frontend está no 4; o contrato do Render é gerado do zod 3)
fastify-type-provider-zod4.0.2
MCP SDK1.30.1 (servidor do /mcp)
tailwindcss 3.4.19 + postcss 8.5.28só do Render: compilação de props.cls no servidor
jsdom26.1.0 — só services/render-bundle/asset-files.ts (SVG passivo)
jsonwebtokentoken de preview apv_* e sessão
Jest 30.4.2 / @swc/jest / TS 5.9.3idem

2.3 Frontend — mesma versão do AgentPack

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

(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.

2.4 O que é copiado igual, o que é adaptado, o que não vai

ClasseO 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ânicabackend/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ãomcp/** (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 vaitudo 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.

3. Autenticação, chaves e o contrato entre apps

3.1 Pessoas: SSO pela Conta

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 ContaO 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 rendersem 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 contaresolveActive 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.

3.2 Máquinas: chaves próprias 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:

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).

3.3 Regra da Conta, implementada já na app nova

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 ContaO que a app nova faz
org.upsertedespelha nome/slug; se a empresa está suspensa, marca resumeRequestedAt — não reativa
member.upsertedespelha a pessoa e o papel do dono; nada de acesso
app_access.grantedcria o vínculo da pessoa (JIT); nunca reativa empresa suspensa
member.removed / app_access.revokedremove 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.cancelledsuspende: suspendedAt, suspendedReason, derruba as sessões, e o middleware de agente passa a recusar as chaves apk_* da empresa. Nada é apagado
org.deletedterminal: 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.

3.4 Token de preview apv_* — caminho de credencial

Muda 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çãoCredencial na página de previewVeredito
(a) cookie httpOnly de escopo curto, emitido pelo backend para o Chromium, SameSite=Strict, desafio one-shotnada legível por scriptescolhida — mesmo caminho do OIDC, já implementado no app
(b) sessionStorage com getToken no cliente HTTPlegí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 minnada legível por script, mas cria sessão de verdade por capturaaceitável, mais caro; fica como plano B se o desafio one-shot atrapalhar o Chromium

O caminho (a) em 4 passos, todos testáveis:

  1. O backend emite o apv_ com audiência e rótulo próprios e o devolve num campo que o shot grava no cookie de preview do Chromium.
  2. O shot grava cookie render:preview=<apv_>; HttpOnly; SameSite=Strict; Path=/render/preview antes do primeiro load da página.
  3. RequireAuth ganha exceção para /render/preview/**: aceita o apv_ (via cookie httpOnly, enviado pelo Chromium automaticamente) e não redireciona para a Conta.
  4. 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.

3.5 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).

4. Empresa e projeto

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:

  1. 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.
  2. O isolamento por empresa (actor.projectIds, render-access.ts:52) precisa do vínculo projeto→empresa sem ida ao AgentPack.
  3. O espelho também é o que permite o Render recusar projeto na lixeira sem conhecer o AgentPack.

4.1 O contrato de sincronia, como estado de primeira classe (novo)

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:

4.2 Contrato de purga (novo — o desenho anterior estava incompleto)

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:

  1. O AgentPack orquestra (é dono do dado de projeto): o job de retenção chama 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.
  2. O Render apaga as linhas do espelho e os arquivos no seu próprio RENDER_ASSETS_DIR, e só por essa chamada explícita: nunca por job próprio (ele não consulta o AgentPack).
  3. Depois do corte, o AgentPack para de apagar os arquivos do Render (agentpack-prod-data é dele, render-prod-data é do Render).
  4. O que acontece se o AgentPack nunca chamar: o Render não faz polling. O sinal é do reconciliador (que detecta 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.

4.3 O que fica no AgentPack

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).

4.4 Portão do upgrade de design system — corrigido

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")
}

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.

4.5 Catálogo global: por empresa — e o que isso mexe

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:

OndeHojeO 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 parciaisON (base_key/kind/name) WHERE project_id IS NULLON (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 adminGlobalWriteReach 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 === nulldecide por organização, não por nulidade de projeto
lib/render-db.ts:80-81 herança global→projetoreadLibrary cai na global quando não há linha localfiltro {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 / isReservedDesignSystemNamegiram em torno de project_id IS NULLprecisam de org_id no predicado
lib/render-catalog/validate.ts:15 INHERITANCE_DEPRECATIONdeprecia a herança global→projetoidem, 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.

5. Compatibilidade: mesma URL, mesmo nome

5.1 REST — 47 endpoints, não 54

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.

5.2 Tools MCP — 59, e quem aplica o prefixo

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):

ArquivoToolsO que é
tools/render/design-systems.ts18ciclo 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.ts17telas, nós, histórico, mapa e preview de projeto
tools/render/catalog.ts15componentes, estilos e tokens (os 9 render_component_*/_style_*/_token_* entram por targetTool/styleTool/tokenTool, não por renderTool)
tools/render/bundles-assets.ts7import/export de bundle e CRUD de assets
tools/render/extract.ts1extração
tools/render/help.ts1render_help
tools/render/core.ts0 (as 3 project_*)não vai — ver §9 passo 3
total59igual 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.

5.2.1 O prefixo é aplicado uma vez só — e isso decide D5/D6

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.

CaminhoQuem prefixaO 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: renderrender_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:

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.

5.2.2 Toolsets e headers na ponte

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:

  1. Default de toolset do Render. No AgentPack, 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.
  2. 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.
  3. 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.

5.3 /mcp próprio

Sim, 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):

ArquivoDestinoPor quê
mcp/registry.tssai — vira o registro só do Renderhoje é o registro do app inteiro (core + Render + GitHub)
mcp/prompts.tssaiimporta project.service, skill-files, task.service: prompts executar-task e handover, que são de task
mcp/resources.tssaiimporta knowledge.service, memory.service, skill.service, project.service: resources skill://, agentpack://knowledge, agentpack://memory
mcp/proxy-call.tssaio 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_*.

5.4 A ponte de MCP, com as ressalvas fechadas

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:

  1. Enquanto o registro built-in do AgentPack ainda tiver 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.
  2. Escopo: toda tool proxied recebe scope: "write" (PROXIED_TOOL, middleware/authz.ts:169), não render:write. Quem usa a ponte precisa de write na chave do AgentPack.
  3. Identidade: 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.
  4. Auditoria: toda tool proxied gera linha em audit_events como escrita (mcp/tool-call.ts:88), mesmo as de leitura. Mais registro, nunca menos.
  5. Sessão: o /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.
  6. Segurança do catálogo (achado 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.
  7. A credencial de saída (novo). O cadastro cria no AgentPack uma credencial de serviço para o Render: secretHeaders com Authorization: Bearer apk_* da app Render, de escopo mínimo (leitura
    • webhook de projeto). Ela precisa estar registrada em três lugares que o desenho anterior não

tinha: 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:

Recomendação final: ponte no corte (Fase B), /mcp direto como alvo. Quem avalia a chamada é o Render; a ponte só entrega.

6. Banco, envs, imagens

6.1 Schema

Uma migration inicial só do Render (squash das 7 do AgentPack), com:

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.

6.2 Envs

GrupoVariáveis
Só do RenderRENDER_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 AgentPackNODE_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_*
SSOOIDC_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.

6.3 Imagens

7. Topologia de deploy no MyPanel (runbook, não executado)

Projeto: workspace_nommand (947JSvgPNhtoIM97ko6ie), ao lado dos outros 9.

ServiçoOrigemPortaMemóriaVolumeDomínio
render-backend-ws (NOVO)PedroPaduelo/render, branch main, buildPath /backend, Dockerfile4000 (não publicada)1024 MBrender-prod-data → /datarender-v0-api.mp.serendiped.com → 4000, SSL
render-frontend-ws (NOVO)idem, buildPath /frontend8080 (não publicada)256 MB—render-v0.mp.serendiped.com → 8080, SSL
render-shot-ws (NOVO, opcional)idem, buildPath /shot45001024 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.

8. Migração dos dados de produção (desenho do script, não roda)

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

  1. Preflight (só leitura, precisa de credencial autorizada):
    • contagem por tabela + globais (project_id IS NULL) + soma de rev/version;
    • sha256 agregado de tree/payload/snapshot por tabela (a soma, não o conteúdo);
    • linhas cujo project_id não existe em projects, e linhas de projeto na lixeira;
    • tamanho e contagem de arquivos em RENDER_ASSETS_DIR (fora de exports/), comparado com sum(render_assets.size);
    • decisões de upgrade abertas (task_decisions com key render-ds-upgrade-%);
    • quantas linhas 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;
    • conferência de quem prefixa (novo): tools/list do /mcp do Render devolve agentpack_render_* e o catálogo do AgentPack normaliza — os dois lados concordam;
    • checagem de drift do openapi.json: se o AgentPack estiver com o arquivo inválido (§2.4), o api:check falha por outro motivo — corrigir antes.
  2. Copia dos projetos: 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.
  3. Copia das linhas do Render, preservando id, versão e datas, nesta ordem (as FKs internas): 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.
  4. Arquivos: cópia de 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).
  5. Decisões de upgrade pendentes: copiar para 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.
  6. Validação (o script se recusa a seguir se qualquer uma falhar):
    • contagem por tabela igual entre origem e destino;
    • 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;
    • abrir 3 telas reais pelo frontend novo e comparar o PNG do shot com o do AgentPack.
  7. Janela de congelamento: com escrita congelada no AgentPack (feature flag 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.
  8. Rollback: o script é só-insert e roda contra um banco novo; o rollback é descartar o banco novo e o volume novo (§7) e reabrir a escrita no AgentPack. Nada no AgentPack é alterado até a limpeza (passo 10 da §9).

9. Matar o Render no AgentPack (plano futuro, fora do escopo agora)

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

  1. 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.
  2. 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.
  3. 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.
  4. serializers/render.ts sai do barril utils/serializers.ts:14.
  5. 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)

  1. 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.
  2. Rotas do Render saem de 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.
  3. 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).
  4. 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.
  5. 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".
  6. Frontend: 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.
  7. services/{schema-health,readiness}.service.ts e routes/health.ts:42: sai o Render e o shot.
  8. As 22 suites do núcleo que citam o Render (lista no mapa de acoplamento §7) e as 59 linhas do snapshot.
  9. Docs: npm run docs:gen (docs/mcp/tools.md, docs/data-model.md, llms-full.txt).

Limpeza de dados (por último, com backup conferido)

  1. Migration que derruba as 8 tabelas, os 3 enums e as relações de Project no schema.
  2. 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.
  3. As 7 skills: aposentar (as que o AgentPack reescreve para o MCP novo) — a chave é que npm run check:skills passa depois que as tools saem do registro.
  4. Mídia: não é redirect (correção da §14). /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).
  5. /data/render-assets sai do volume agentpack-prod-data e a purga do AgentPack para de apagar esses arquivos (§4.2).

10. Pacotes de trabalho

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.

#PacoteDepende de
P-1Preparo do AgentPack: mover toolError para mcp/tool-error.ts (tool-call, proxy-call e a reexportação em tools/render/common.ts)—
P-2Preparo do AgentPack: organizationId em McpServer (C4) + normalização do prefixo upstream em proxiedWireName—
P0Núcleo do backend: esqueleto Fastify, config, Prisma, erros, plugins, health/metrics/llms, Dockerfiles, jest, gates, AGENTS.md—
P1Banco do Render: schema das 8 tabelas + enums + migration inicial + render_projects + espelhos da Conta + chaves + auditoria + schema-healthP0
P2Contratos do Render: lib/render-contracts/**, backend/schemas/render/**, os 2 geradores, segurança de conteúdoP0, P1
P3Identidade: 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 appsP0, P1
P4Domí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.serviceP2, P3
P4bEscopo 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/writeLibraryP3, P4
P5Rotas REST do Render: routes/render.ts + render-scoped.ts (47), schemas/render.ts, serializers, lib/render-http.tsP4b
P6Tools MCP do Render: tools/render/** (menos core.ts), toolset render/render-admin, os 11 arquivos de mcp/ que ficam, /mcp, server.jsonP5
P7Portão de upgrade (render_approvals + tela de decisão) + contrato de purga + script de sincronia de projetos + webhook do AgentPack (parte do backend)P6
P8Casca 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çaP0, P6
P9Motor do Render no frontend: src/render/engine/**, primitives/**, contract.generated.ts + expr.generated.ts, CanvasView/CanvasFrame/RenderThumb/ScreenView/canvasManifest/formats/prefsP8, P2
P10Camada de dados + editor do Render no frontend: api/render*.ts, api/projectRender.ts, hooks/useRender*, render/hooks/**, types/render.ts, render/editor/**P6, P8
P11Telas do Render: raiz de pages/Render, Studio/**, shared/**, Import/Stacks/Tokens/Styles/Versions, projectMap/projectScreen, previews, rotas, paths.tsP9, P10
P12Credencial de preview (cookie apv_) + RequireAuth do preview + getToken no cliente — vem ANTES de P6 fecharP11, P3
P13Testes do Render (backend e frontend) + gates de cobertura e docsP5, P6, P9, P10, P11
P14Script de migração de dados + runbook de corteP13, 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).

11. Riscos

#RiscoGravidadeMitigação
1O /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 proxiedcríticaP-1, sem dependência, primeiro merge depois do desenho (não é mais passo do corte, que chegaria tarde)
23 tools do core somem quando o registro do Render sai: project_create/update/delete moram em tools/render/core.ts:11-28crítica§9 passo 3, com teste e snapshot atualizado
3O 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)altaremover as duas chaves no mesmo commit das rotas
4O gateway /mcp inteiro do AgentPack quebra se appBaseUrl sair com lib/render-view-url.ts (task-decision.service.ts:2)alta§9 passo 2
5A 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çãoaltaaceito e documentado (§5.4); o override por vínculo cobre o caso estático
6Tools proxied exigem write, não render:write (PROXIED_TOOL, authz.ts:169)médiaaceitar a sobreposição de escopo ou manter os dois caminhos
7Catálogo de MCP do AgentPack é global, sem empresa (McpServer sem organizationId)altaP-2, antes de cadastrar o MCP do Render
8Arquivos 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 404alta§8 passo 4, com checksum por linha; e o rollback também descarta o volume (§7)
9Perda 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émaltacontrato de purga explícito (§4.2): o AgentPack orquestra, o Render apaga só por chamada, webhook + reconciliador sinalizam
10Catálogo global vaza entre empresas (project_id NULL visível para todas, escrita por qualquer usuário)altaP4b — não é adaptação mecânica: muda dado, índice, consulta e política
11Convivência com duas fontes da verdade: enquanto o Render existir nos dois lados, dois agentes podem escreveraltacongelamento de escrita (503 RENDER_FROZEN) antes da cópia final; janela declarada
12autoDeploy: true nos serviços: merge na main pode virar deploy sem ninguém pediraltaserviços novos nascem desligados; deploy é deployments_trigger, decisão do dono; e cada conferência registra o commit do build (§7)
13Trê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édiamanter 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
14Acoplamento de layout: o backend gera 2 arquivos no frontend, o frontend importa do backend, o backend testa o motor do frontendmédiamanter backend/ + frontend/ lado a lado; é o motivo de não virar workspaces
15zod 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 acionamédianpm 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
16Lock do frontend pelo npm 10.9.4: já falhou deploy em produção (2026-10-08)médianpx 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 nadamédiapinar #v1.13.5; nunca #main
18Captura 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 RequireAuthmédiaP12 redesenha o caminho inteiro (cookie httpOnly, §3.4); a app nova nasce sem o defeito
19A 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 projetomédiacookie httpOnly com desafio one-shot (§3.4, opção a) + teste de segurança que tenta ler a credencial de um <script> injetado
20SHOT_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édiavalidação no env-schema (host interno ou allowlist, sem http:// público) + teste que recusa URL pública
21Troca de origem apaga estado local: rascunhos no IndexedDB agentpack-render/screen-drafts e preferências em localStoragemédiajanela de aviso; ou ler o IndexedDB antigo uma vez no primeiro carregamento
22Empresa 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 vazamédiaorgId na chave de query, como o resto do app novo
23Estado de produção não conferido: não se sabe quantas linhas/arquivos existem, nem se o congelamento do catálogo legado rodoumédia§8 passo 1, com credencial autorizada e leitura apenas
24RENDER_AGENT_TOKEN é uma chave de plataforma permanente: o BOOTSTRAP_AGENT alcança tudo e tem escopo de escritamédiarotação com prazo no runbook; removível depois da primeira chave admin; segredo forte exigido no env-schema
25A 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çãomédiaregistrada no runbook com escopo mínimo, dono da rotação e ordem de revogação (§5.4 item 7)
26Sandbox de 2 GB (agent-package-audit) para o gatebaixao app é construído no SB render (20 GB); o SB pequeno só serve para leitura
27openapi.json herdado não é JSON válido (falha em line 16801, desde bbc205c)baixaa app nova não tem api/generated/; o spec é gerado em build e validado contra o swagger, nunca contra arquivo commitado

12. Decisões do dono

Cada uma com a recomendação do desenho. As travadas estão marcadas.

#PerguntaRecomendaçã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
D5Nomes 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
D6Transiçã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
D7Skills 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
D8Nomes 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
D9shot/: 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
D11Banco: 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)
D12autoDeploy: serviços novos com ele ligado?desligado. Merge na main não publica; publicação é deployments_trigger, que é decisão do dono
D13Migraçã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
D14Escopo 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
D15Consertar 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 🔒 NOVACriador 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

13. Como este desenho foi feito

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.


14. Críticas e respostas

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.

14.1 Crítica de arquitetura

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.

14.2 Crítica de paridade (regras entre apps)

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.)

15. O que mudou da revisão 1 para esta

OndeAntesAgora
§0, §5.1, P554 rotas REST47, com critério e lista completa, e render-routes.test.ts que conta
§4.4, P7, D2portão do upgrade lia task_decisions no caminho do frontendo caminho humano não usa tabela nenhuma; render_approvals substitui só o do agente; D2 reescrita
§3.4, P12, D15credencial de preview em sessionStorage + getTokencookie httpOnly one-shot + RequireAuth do preview + getToken; P12 sobe antes de P6
§5.2.1, §5.4, D5, D6prefixo agentpack_render na ponteduplicava os 59 nomes; corrigido com normalização no catálogo do AgentPack (P-2)
§5.3, P6mcp/ com 11 arquivos listados15 com destino explícito; prompts/resources/registry/proxy-call saem; metrics adaptado
§4.5, P4bcatálogo por empresa como "mudança a fazer"pacote próprio P4b com 8 itens e 6 pontos de código verificados
§4.1, P7espelho de projeto com webhook + reconciliadorcontrato de sincronia com 503 REPLICATING, SLA e métrica
§4.2, §9.10purge "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.2SHOT_SERVICE_URL e SHOT_TIMEOUT_MS na tabela erradaSHOT_TIMEOUT_MS só no backend; SHOT_SERVICE_URL validado
§6.2RENDER_AGENT_TOKEN renomeado na env6 pontos de renome + teste de que nenhum AGENTPACK_ sobrevive
§2.1, §7volume e rollback sem coberturavolume descartável + 3 regras de reversibilidade
§9.18redirect de /api/render/assets/**link externo ou proxy autenticado, nunca redirect
§1015 pacotes P0..P14+ P-1 e P-2 (preparo do AgentPack, sem dependência) + P4b (escopo de empresa), + P12 na ordem certa
§1216 decisões+ D17 (criador desativado) e D2, D3, D6 reescritas
§1122 riscos27, 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 -->