Skip to content

About

AI Agent skill manager and MCP server for managing and retrieving skills.

Resources

Stars

11 stars

Watchers

0 watching

Forks

Repository files navigation

🟣 Purple Skills

Catálogo aberto de skills para agentes de IA — homepage, site do catálogo, painel administrativo e dois servidores MCP.

Software livre (MIT), em containers, pensado para ser simples, bonito e pontual.


O que é

Purple Skills hospeda skills — pacotes de instruções reutilizáveis para agentes de IA, no formato SKILL.md + arquivos auxiliares. Ele entrega cinco superfícies:

Serviço O que faz Porta padrão
homepage Apresentação do projeto, estática — não fala com o banco 3004
site Página pública do usuário: skills e catálogos públicos, busca, SKILL.md renderizado, download e o mcp.json 3000
admin Painel de administração: canvas dos servidores MCP, sessões, contas e papéis 3001
mcp-public Servidor MCP para agentes descobrirem e baixarem skills 3002
mcp-admin Servidor MCP para administrar o catálogo (CRUD completo) 3003

homepage e site são páginas diferentes de propósito. A homepage explica o que é o Purple Skills e leva ao GitHub; ela não conhece instalação nenhuma e pode ir para o ar sozinha. O site é a página de quem já tem uma instalação no ar: lista as skills e os catálogos tornados públicos, ensina a configurar o mcp.json e mostra os endereços de acesso. O que é privado fica só no painel.

O banco é PostgreSQL 18 (imagem pgvector/pgvector:pg18-trixie). A busca usa full-text search nativo (tsvector + GIN) e, com a busca semântica ligada, funde o resultado com a busca vetorial do pgvector por RRF — o campo mode da resposta diz qual das duas o cliente leu.

Começando em 60 segundos

cp .env.example .env      # troque TODOS os CHANGE_ME: POSTGRES_PASSWORD,
                          # ADMIN_PASSWORD, ADMIN_SESSION_SECRET e MCP_ADMIN_TOKEN.
                          # O de sessão assina o cookie do painel — quem o deixa
                          # no placeholder entrega o painel a qualquer visitante.
                          # Gere cada um com: openssl rand -hex 32
docker compose run --rm migrate   # aplica database/schema/*.sql
docker compose up -d
docker compose run --rm seed      # opcional: skills de exemplo

Os serviços postgres, migrate e seed vêm de database/docker-compose.yml, incluído pelo compose da raiz — rode sempre a partir da raiz do repositório.

Imagens no Docker Hub

As imagens ficam no Docker Hub, na conta tmsoftbrasil, como tmsoftbrasil/purple-skills-<nome>: homepage, site, admin, mcp-public, mcp-admin, indexer e db (a do migrate e do seed) — cada uma com duas tags, latest e a da versão do package.json, e em linux/amd64 e linux/arm64. A publicação não é automática: nenhum workflow de CI tem credencial de registry. Depois de criar a tag da release, quem mantém builda e publica à mão, de uma máquina já autenticada (docker login) na conta:

./release-images.sh                        # builda e publica as 7 imagens
./release-images.sh site admin             # só as passadas por nome
PLATFORMS=linux/amd64 ./release-images.sh  # uma arquitetura só, com pressa

O script mostra versão, tags, plataformas e commit — e avisa se a árvore está suja — antes de pedir a única confirmação; a versão e o commit ficam gravados na própria imagem, em labels OCI (docker image inspect).

A latest só é movida no fim: o build publica cada imagem com a tag de versão e, depois que todas as pedidas estão no Hub, o script aponta latest para elas de uma vez, começando pela db (a do migrate). Build que falha no meio deixa latest inteira na versão anterior; aí repita o mesmo comando — o que já foi construído sai do cache —, porque rodar só as que faltam moveria a latest só delas. Se for a promoção que parar, o script imprime os comandos que faltam.

O compose usa a tag de TAG no .env (padrão latest); se a imagem não existir no Hub, ele a gera a partir do código, como o docker compose build faz sempre. A tag de versão é o caminho de volta quando uma release quebra, porque latest é sobrescrita e não deixa cópia de nada:

TAG=1.0.0-beta.20 docker compose up -d

Atrás do Traefik

Preencha os *_FQDN no .env e suba com o override:

docker compose -f docker-compose.yml -f docker-compose.traefik.yml up -d

O override assume um Traefik já rodando na rede externa traefik, com o entrypoint websecure e o certresolver le.

Ele também despublica as portas dos serviços (ports: !reset []): quem fala com os apps passa a ser só o Traefik, e é isso que impede um contêiner vizinho de chegar direto no app e declarar o X-Forwarded-For que quiser. Quem publica as portas (BIND_ADDR) e põe um proxy na frente deve nomeá-lo em TRUST_PROXY.

Depurando os MCPs com o Inspector

O MCP Inspector oficial é parte da stack e sobe junto no up -d do dia a dia, em http://localhost:6274.

Abra http://localhost:6274 e aponte para os servidores pelo nome do serviço, não por localhost: quem conecta é o backend do inspector, de dentro da rede internal.

Servidor URL Autenticação
mcp-public http://mcp-public:3002/mcp A do MCP virtual padrão: nenhuma se ele está aberto, Authorization: Bearer psv_… se exige chave
mcp-admin http://mcp-admin:3003/mcp Authorization: Bearer $MCP_ADMIN_TOKEN (obrigatório)

O inspector sobe sem autenticação própria (DANGEROUSLY_OMIT_AUTH), o que só é aceitável porque a porta fica em 127.0.0.1 — e ela não acompanha o BIND_ADDR global: publicar os outros serviços na rede não arrasta consigo uma ferramenta sem credencial que tem caminho até o mcp-admin. Para alcançar a UI de outra máquina, mude as duas metades juntas: INSPECTOR_BIND_ADDR=0.0.0.0 e INSPECTOR_OMIT_AUTH=false com INSPECTOR_API_TOKEN=$(openssl rand -hex 32). Atrás do Traefik o override não publica porta nenhuma dele nem lhe dá rota: para abrir a UI no servidor, use um túnel SSH até o IP do container na rede internal.

O Postgres é a outra exceção ao BIND_ADDR: a porta dele é publicada em POSTGRES_BIND_ADDR, com padrão 127.0.0.1, e fica no loopback mesmo com BIND_ADDR=0.0.0.0. Nenhum app usa essa porta — todos chegam ao banco por postgres:5432 na rede internal —, ela existe para o psql de quem administra a máquina, e publicá-la na rede expõe um servidor sem limite de tentativa de login nem trava de conta. Precisa mesmo do banco de outra máquina? POSTGRES_BIND_ADDR=0.0.0.0, e não com a senha do .env.example.

Desenvolvimento local

Requer Node.js 22.15+ (LTS) e um Postgres 18 acessível. Não é o 22+ de antes: a leitura de pacote importa zstdDecompressSync do node:zlib, que só existe a partir do 22.15, e um import nomeado que o módulo não exporta é erro de ligação — num 22.0–22.14 não falha o caminho .zst, falha o carregamento de @purple-skills/shared inteiro, e com ele todo serviço que o importa. As imagens não sentem: elas são node:24-alpine.

npm install
npm run build:packages     # shared, rag e db — a lista vive só no package.json
npm run migrate            # aplica database/schema/*.sql em DATABASE_URL
npm run seed               # opcional

npm run dev:homepage       # http://localhost:5175 (Vite) + estático em :3004
npm run dev:site           # http://localhost:5173 (Vite) + API em :3000
npm run dev:admin          # http://localhost:5174 (Vite) + API em :3001
npm run dev:mcp-public     # http://localhost:3002
npm run dev:mcp-admin      # http://localhost:3003

npm test                   # testes unitários (Vitest)
npm run typecheck          # TypeScript em todos os workspaces

Os testes de integração — as queries de database/, o indexador e a busca — exigem um Postgres real com a extensão vector e ficam desligados por padrão. Para rodá-los, aponte TEST_DATABASE_URL para um banco descartável — o schema é recriado do zero a cada execução:

TEST_DATABASE_URL=postgres://postgres:CHANGE_ME@127.0.0.1:5432/purple_skills_test \
  npx vitest run database/src/files.integration.test.ts

Com a variável definida, npm test roda a suíte inteira, integração incluída.

Estrutura do repositório

Monorepo com npm workspaces:

database/        camada de dados — domínio do agente dba
  schema/        nnn-nome.sql: tabelas, tipos, índices, funções e triggers
  src/           cliente, queries e tipagem Drizzle (@purple-skills/db)
  Dockerfile     imagem que aplica o schema e carrega os exemplos
  docker-compose.yml   containers postgres, migrate e seed
apps/
  homepage/      apresentação      — Express estático + React/Vite + Tailwind
  site/          página pública    — Express + React/Vite + Tailwind
  admin/         painel admin      — Express + React/Vite + Tailwind
  mcp-public/    MCP público       — @modelcontextprotocol/sdk
  mcp-admin/     MCP administrativo
  indexer/       indexador do RAG  — worker de embeddings, sem porta; parado com o driver off
packages/
  shared/        slug, rating, mime, zip, frontmatter, segredos, senha, chave de
                 API, e-mail, papéis, sessão, ícone, cabeçalhos de segurança,
                 rate limit
  rag/           drivers de embedding, chunk, hash, busca semântica e o registro
                 único de drivers, modelos e variáveis RAG_*

Cada app gera sua própria imagem Docker — são 6, e database/Dockerfile é a sétima, a do migrate/seed. Os cinco que leem o catálogo (site, admin, mcp-public, mcp-admin e indexer) falam direto com o Postgres, sem serviço de API intermediário; a homepage não abre conexão com o banco.

O banco fica em database/

Tudo o que é banco de dados — os containers postgres, migrate e seed, os arquivos de schema e a biblioteca de acesso — está confinado em database/ e é responsabilidade do agente dba. Os apps só consomem @purple-skills/db; nenhum deles escreve SQL ou abre conexão por conta própria.

O contrato completo (arquivos de schema, tabelas, variáveis de conexão, API disponível e o que cada agente pode ou não fazer) está em database/README.md; a divisão de responsabilidades entre os agentes, em AGENTS.md.

Identidade visual

O mascote é o Mago Roxo, e o roxo é a cor de tudo. Homepage, site e painel compartilham o mesmo sistema de design — tokens de cor em CSS, tema claro e escuro, tipografia Aeonik + JetBrains Mono e os diagramas em SVG da homepage.

Onde mexer em cada peça está em docs/04-design-system.md. Resumo do que importa: cada arquivo copiado tem o seu par, e nenhum é igual nos três apps. tokens.css, base.css e chrome.css são cópia byte a byte entre a homepage e o site, e só entre esses dois; markdown.css, entre o site e o painel. Ao mudar um, copie para o par no mesmo commit — o npm test reprova o par pela metade. O painel é um console com paleta e primitivos próprios (docs/10) e nunca é destino dos três primeiros: sobrescrevê-los com os do site apaga o console.

Conectando um agente ao MCP

Os dois servidores MCP expõem os três transportes do SDK TypeScript:

Rota Transporte
POST/GET/DELETE /mcp Streamable HTTP com sessão (header mcp-session-id)
POST /mcp/stateless Streamable HTTP stateless (um servidor por requisição)
GET /sse + POST /messages SSE legado

Exemplo de configuração em um cliente MCP:

{
  "mcpServers": {
    "purple-skills": {
      "type": "http",
      "url": "https://mcp.example.com/mcp"
    }
  }
}

Onde uma skill é exibida

Uma skill é flutuante: existe no catálogo, e um servidor MCP só a serve onde ela estiver vinculada a um MCP virtual. No site a regra é outra (docs/12 §7): ele lista a skill ligada que seja marcada pública, ou esteja em ao menos um MCP virtual aberto e ligado, ou participe de um catálogo público e ligado. Desvincular uma skill pública a tira dos servidores, não do site; sem nenhum dos três, ela fica visível só no painel. Cada vínculo escolhe por quais portas a skill sai naquele servidor:

Porta O que faz
skill a skill fica ao alcance das cinco ferramentas — search_skills, get_skill, get_skill_file, download_skill e a contagem de list_tags
prompt a skill entra em prompts/list com o slug como nome; prompts/get devolve o corpo do SKILL.md, sem frontmatter e sem argumentos. Na maioria dos clientes vira um slash-command
resource a skill ganha a URI skill://<slug>/SKILL.md; resources/read devolve o SKILL.md canônico (text/markdown), idêntico ao do .zip

As três portas contam acesso (view_count), no vínculo e no total da skill. Uma skill fora de uma porta responde por ela o mesmo "não encontrada" de um slug inexistente. As listas de prompts e resources são montadas por requisição, então vincular uma skill a faz aparecer sem reiniciar o servidor nem reabrir a sessão; não há listChanged, o cliente re-lista quando quiser.

O vínculo se faz pelos dois lados: na página da skill ("Publicada em") ou na do MCP virtual, e por link_skill / set_virtual_mcp_skills no mcp-admin. Publicar em um MCP virtual exige edit nele e view na skill — o dono, um admin ou quem recebeu edit no servidor. Quem edita um servidor publica nele qualquer skill que consiga ler, inclusive de outro dono; se o servidor for aberto, isso torna a skill pública. O desenho está em docs/09-mcp-padrao-e-skills-flutuantes.md.

Uma skill pode ser desligada na ficha dela: some de todo servidor e do site, direto ou por catálogo, sem perder vínculo nenhum — e volta quando for religada.

Catálogos: um grupo de skills de uma vez

Um catálogo agrupa skills e, vinculado a um MCP virtual, entrega todas as que estiverem ativas pelas portas escolhidas no vínculo — uma escolha só para o grupo, que continua valendo quando o grupo muda. Uma skill pode estar em vários catálogos; um catálogo pode estar em vários servidores.

  • Cria quem é editor ou admin, em Catálogos no painel ou por create_catalog. Quem cria é o dono; vincular a um servidor exige edit no servidor e view no catálogo — não administrar os dois. Como um catálogo público é legível por qualquer conta, quem edita um servidor seu pode puxar para dentro dele catálogo público de outro dono. Desvincular exige só o edit do servidor: tirar da lista é mexer no servidor.
  • Se uma skill do catálogo também tem vínculo direto com o mesmo servidor, o vínculo direto vale sozinho — é o jeito de restringir uma skill num servidor sem tirá-la do grupo. Sem vínculo direto, as portas são a união dos catálogos.
  • Na página do catálogo cada skill tem a participação (desmarcar tira da entrega sem remover do catálogo), e uma skill desligada aparece com alerta. O catálogo inteiro também liga e desliga.
  • No canvas o catálogo é um nó só, com o número de skills ativas que ele entrega àquele servidor. Cada catálogo tem o próprio contador de acessos.
  • Clonar um catálogo copia as propriedades e os membros, cada participação com o ligado/desligado que tinha — e não os servidores em que o original está vinculado. A cópia é de quem clonou e nasce privada (Clonagem).

O desenho está em docs/11-catalogos.md.

Ferramentas do MCP público

Ferramenta Descrição
search_skills(query?, tag?, limit?, offset?) Busca full-text nas skills vinculadas ao servidor como skill
get_skill(slug) SKILL.md completo + metadados. Conta um acesso
get_skill_file(slug, path) Lê um arquivo auxiliar da skill
download_skill(slug) Devolve a URL do pacote .zip
list_tags() Tags disponíveis, com contagem

O MCP público é o MCP virtual padrão

Não existe mais um servidor "principal" à parte: o que responde em /mcp é o MCP virtual escolhido como padrão em Configurações, no painel (ou por set_default_virtual_mcp no mcp-admin, só admin). Ele continua respondendo em /virtual/<slug>/mcp, e não tem nenhum tratamento especial — pode ser fechado, desligado ou apagado como qualquer outro. Quem decide o acesso a /mcp é ele: aberto, ou chaves psv_ emitidas na página dele. GET / anuncia qual é o padrão, e sem um em pé /mcp responde 404 dizendo a causa (nenhum escolhido, removido ou desligado).

Quem sobe de versão recebe o MCP virtual public, aberto, com toda skill pública vinculada nas mesmas superfícies de antes. MCP_PUBLIC_AUTH, MCP_PUBLIC_KEY e as chaves psp_ deixaram de existir; o mcp-public recusa subir enquanto as variáveis estiverem no .env, para quem protegia o servidor fechar o public (ou emitir chaves psv_ para ele) antes de removê-las. O desenho está em docs/09-mcp-padrao-e-skills-flutuantes.md.

MCPs virtuais: um servidor por time

Um MCP virtual é um recorte do catálogo servido pelo mesmo mcp-public em /virtual/<slug>/mcp (e /mcp/stateless, /sse + /messages), com endereço, chaves e dono próprios. Serve para um time ou projeto conectar o agente só às skills que lhe interessam — inclusive skills que não estão em nenhum servidor aberto, e que só aparecem no site se forem marcadas públicas ou participarem de um catálogo público. Um deles é o padrão, e responde também em /mcp.

  • Cria quem é editor ou admin, no painel (Servidores MCP) ou pelo MCP administrativo. Quem cria é o dono; mexem nele o dono, os administradores e quem recebeu edit (vínculos, portas e canvas) ou manage (nome, slug, aberto, chaves e concessões) no servidor. Apagar e transferir são do dono e do admin.
  • Para cada skill vinculada escolhem-se as três superfícies (ferramentas, prompt, resource) naquele servidor. Vincular é o único jeito de uma skill ser servida por um MCP; no site ela entra também marcada pública ou por um catálogo público.
  • O acesso é por chave psv_…, emitida por MCP e sem expiração; a chave de um virtual não abre outro. Um MCP pode ser marcado aberto (sem chave): aberto é público — o site o lista, com suas skills.
  • download_skill e os arquivos binários apontam para o próprio servidor, atrás da mesma chave. Cada MCP tem contadores próprios por skill; o total da skill também soma.
  • Desligar (ligado no painel) faz tudo sob /virtual/<slug> responder 404 sem apagar nada; slug inexistente também é 404, chave errada é 401.
  • Clonar um servidor copia vínculos, posições do canvas e concessões, e não as chaves psv_. A cópia nasce fechada, nunca assume o posto de padrão e exige manage no original (Clonagem).
{
  "mcpServers": {
    "time-a": {
      "type": "http",
      "url": "https://mcp.example.com/virtual/time-a/mcp",
      "headers": { "Authorization": "Bearer psv_…" }
    }
  }
}

O desenho está em docs/08-mcp-virtual.md.

Ferramentas do MCP administrativo

Exige sempre Authorization: Bearer <credencial>, que pode ser o MCP_ADMIN_TOKEN (papel admin, ator token-global na auditoria) ou uma chave psk_… emitida por um usuário no painel — nesse caso valem o papel, o acesso por objeto e o nome do dono: a chave vê e faz exatamente o que a conta faria no painel (ver Contas, papéis e acesso).

Ferramenta Descrição
list_skills(query?, tag?, limit?, offset?, scope?) Lista o que a credencial enxerga, inclusive skills sem vínculo, cada uma com o dono, o acesso e mcps; scope = mine / shared / public
get_skill(slug) / get_file(slug, path) Leitura
create_skill(name, description?, icon?, skill_md_content, tags?, slug?, mcps?, is_public?) Cria a skill (quem cria é o dono) e o SKILL.md na mesma transação, já publicada nos MCPs de mcps (só os que a credencial edita). skill_md_content é só o corpo. Sem mcps nenhum MCP a serve; com is_public: true ela aparece no site mesmo assim
edit_skill(slug, {name?, description?, icon?, tags?, new_slug?, is_active?, is_public?}) Edita metadados — é por aqui que muda o frontmatter. Nome, descrição, ícone e tags exigem edit; slug, is_active e is_public, manage
link_skill(skill, mcp, asSkill, asPrompt, asResource) / unlink_skill(skill, mcp) Publica e despublica pelo lado da skill: edit no MCP virtual e view na skill
set_file(slug, path, content) Cria ou sobrescreve um arquivo. Em SKILL.md, grava só o corpo
set_files_bulk(slug, zip_base64, replace?, confirm_deletions?) Importa uma árvore inteira de um .zip, preservando os caminhos (só a pasta raiz única que contém o SKILL.md — o formato do pacote baixado — é desembrulhada; uma subpasta enviada sozinha fica como veio) — por padrão o zip é o estado completo (omitidos são removidos, SKILL.md preservado). Se algo sairia, a chamada é recusada com a lista: repita com confirm_deletions igual ao número exato de arquivos a remover, ou com replace: false para só acrescentar e sobrescrever
delete_file(slug, path) Remove um arquivo (bloqueado para SKILL.md)
delete_skill(slug, confirm) Remove a skill (exige confirm: true; só o dono ou um admin)
share_skill(slug, username, level) / unshare_skill(slug, username) / transfer_skill(slug, username) Concede (view, edit, manage), revoga e transfere o dono; o mesmo para *_catalog e *_mcp. A conta é o username, nunca o e-mail
clone_skill(slug, name?, new_slug?) / clone_catalog(slug, name?, new_slug?) / clone_virtual_mcp(slug, name?, new_slug?) Copia o objeto; quem clona vira o dono e a cópia nasce fechada (nunca pública nem aberta). A skill leva arquivos e tags e nasce flutuante; o catálogo leva os membros com a participação de cada um; o servidor leva vínculos, canvas e concessões, sem as chaves psv_. Sem name fica o nome do original, sem new_slug o desempate automático (-2, -3…); new_slug já ocupado é 409. Exige edit no original e o papel de criar — no servidor, manage
list_tags() / get_stats() Navegação e métricas
list_virtual_mcps(scope?) / get_virtual_mcp(slug) / create_virtual_mcp(…) / update_virtual_mcp(…) / delete_virtual_mcp(slug, confirm) MCPs virtuais — alcance pelo acesso por objeto
set_virtual_mcp_skills(slug, [{slug, asSkill, asPrompt, asResource}]) Substitui a lista inteira de skills do MCP virtual
list_virtual_mcp_keys(slug) / create_virtual_mcp_key(slug, name) / revoke_virtual_mcp_key(slug, key_id) Chaves psv_ do MCP virtual
get_default_virtual_mcp() / set_default_virtual_mcp(slug | null) Qual MCP virtual responde em /mcp; escolher é só admin
list_catalogs() / get_catalog(slug) / create_catalog(…) / update_catalog(…) / delete_catalog(slug, confirm) Catálogos — alcance pelo acesso por objeto, como os MCPs virtuais
set_catalog_skills(slug, [{slug, isActive?}]) Substitui a lista inteira de skills do catálogo; isActive: false mantém sem entregar
set_virtual_mcp_catalogs(slug, [{slug, asSkill, asPrompt, asResource}]) Substitui a lista de catálogos do MCP virtual; exige edit no MCP e view em cada catálogo (os que saem da lista, só o edit do MCP)

Busca semântica (opcional)

Além da busca full-text de sempre, o catálogo pode buscar por significado: "como padronizar mensagem de commit" acha conventional-commits mesmo sem a palavra "padronizar" no texto. As duas pernas são fundidas por RRF, e a resposta traz mode — text ou hybrid — para o cliente saber o que leu.

Ela vem desligada. Sem RAG_DRIVER, sem chave, sem a migration 020 ou com o provedor fora do ar, tudo responde exatamente como antes, em modo text — e isso não é erro.

Três provedores, um modelo por vez:

RAG_DRIVER Modelos Dimensões
google gemini-embedding-2 3072
openai text-embedding-3-small, text-embedding-3-large 1536 / 3072
voyage voyage-4-lite, voyage-4, voyage-4-large 1024

Para ligar:

docker compose run --rm migrate   # a 020 e a 025 criam as tabelas rag_*
docker compose up -d              # o indexador sobe junto com os demais serviços

O indexador faz parte do up -d comum e fica parado enquanto o driver for off: só publica o próprio estado e não fala com provedor nenhum. A trava do envio é, portanto, uma só — o driver escolhido (no painel, ou semeado por RAG_DRIVER no primeiro boot), com a chave dele no .env — e ligá-la vale para o acervo inteiro, inclusive as skills privadas, a partir do ciclo seguinte. Quem prefere que subir o indexador seja um gesto à parte descomenta profiles: [rag] no serviço indexer do docker-compose.yml e passa a subi-lo com docker compose --profile rag up -d indexer.

Depois, no painel, em Configurações → Busca semântica, escolha o driver e o modelo. O .env só semeia esses dois valores no primeiro boot: dali em diante quem manda é o painel, e mudar a variável vira só um aviso no log. As chaves, essas sim, são do .env — e dá para deixar mais de uma configurada, o que permite trocar de provedor pelo painel sem recriar container nenhum.

Trocar de driver ou de modelo cria outro espaço de embedding: o acervo é reindexado nele e os vetores do anterior ficam onde estão, prontos para quando alguém voltar atrás. Nada é apagado numa troca, e o botão "Reindexar" não gasta embedding com texto que não mudou.

Aviso, e é só do Google. No nível gratuito da Gemini API o conteúdo enviado — inclusive o de skills privadas — é usado para melhorar produtos, e revisores humanos podem lê-lo. Para que não seja, gere a chave num projeto com faturamento ativo. No Espaço Econômico Europeu, na Suíça e no Reino Unido, só o nível pago é permitido para quem oferece o serviço a usuários dessas regiões. OpenAI e Voyage não treinam sobre o tráfego da API.

O desenho está em docs/14-rag.md.

API REST pública

A API do site é aberta (CORS * por padrão; feche com SITE_CORS_ORIGIN) e serve como alternativa ao MCP. Todas as rotas são GET, e todas mostram só o que está público — skill vinculada a MCP virtual aberto, skill marcada pública ou skill de catálogo público:

GET  /api/meta                                  nome, tagline e endereços da instalação
GET  /api/mcps                                  MCPs virtuais abertos, com endereço
GET  /api/catalogs                              catálogos públicos
GET  /api/catalogs/:slug                        detalhe + os membros ativos do catálogo
GET  /api/skills?q=&tag=&sort=&limit=&offset=   lista/busca
                                                a resposta traz `mode`: text ou hybrid
GET  /api/skills/:slug                          detalhe + corpo do SKILL.md (conta acesso)
GET  /api/tags                                  tags com contagem
GET  /api/skills/:slug/files/<caminho>          arquivo avulso
GET  /skills/:slug/files/<caminho>              o mesmo, sem o prefixo /api
GET  /skills/:slug/download                     pacote .zip        (conta download)
GET  /skills/:slug/download.skill               o mesmo ZIP, extensão do formato
                                                aberto de Agent Skills
GET  /api/skills/:slug/download                 aliases com /api dos dois
GET  /api/skills/:slug/download.skill           downloads; contam igual
GET  /healthz                                   saúde do serviço

Fora de /healthz e de /assets/, tudo aqui entra no limite por IP de SITE_RATE_LIMIT_MAX (240 por minuto; 0 desliga).

Metadados e o SKILL.md

Slug, nome, descrição e tags são campos da skill, não texto do arquivo. O frontmatter é gerado a partir deles toda vez que o SKILL.md é entregue — no download .zip, na leitura crua do arquivo e nas ferramentas MCP:

---
name: revisao-de-codigo          # o slug: nome oficial da skill
description: Revisa PRs pequenos: foco em risco, teste e legibilidade.
metadata:
  title: Revisão de Código       # nome de exibição no catálogo
  tags: code-review, qa
---

O que fica gravado é só o corpo do prompt: qualquer frontmatter enviado no conteúdo é descartado na escrita, e o formulário do painel é a fonte da verdade. Renomear a skill atualiza o arquivo sozinho, sem reescrever o texto.

Quarentena: aprovar antes de publicar

Um pacote que veio de fora não precisa virar skill na hora. Ao importar um pacote no painel — .zip, .skill, .tar, .tar.gz/.tgz, .gz ou .tar.zst/.tzst/.zst, reconhecidos pela assinatura do arquivo e não pela extensão —, quem importa escolhe o destino:

  • Direto para produção — o comportamento de sempre: a skill nasce com nome, slug e tags do SKILL.md, e com os servidores que a tela oferecer.
  • Para a quarentena — os arquivos ficam guardados como chegaram, esperando aprovação. Nada é publicado, indexado pela busca semântica nem aparece no site; nenhum agente alcança o conteúdo. Dá para escolher já aqui o destino: os catálogos em que a skill vai entrar e os servidores MCP em que ela vai ser publicada quando for aprovada.

Um pacote pode trazer várias skills. Quem decide qual caso é o seu é a raiz do pacote: com um SKILL.md nela, o pacote é uma skill só e tudo o mais é conteúdo dela — inclusive o SKILL.md de exemplo que um template guarda em references/. Sem SKILL.md na raiz, skill passa a ser cada diretório que tenha um, levando junto os arquivos dele e das subpastas dele; diretório sem SKILL.md, e fora de um que tenha, é ignorado por inteiro. Baixar o .zip do repositório obra/superpowers no GitHub e importá-lo põe todas as skills dele na fila, uma por diretório, ignorando .github/, docs/ e o resto — e "ignorado" é literal: o que está fora do diretório de uma skill não entra em envio nenhum, nem mesmo quando o pacote traz uma skill só. Um pacote com duas ou mais skills vai sempre para a quarentena: pedir produção é recusado, com a contagem do que foi encontrado e o apontamento para a fila. .rar, .7z, .xz e .bz2 não são suportados, e a recusa diz isso com todas as letras em vez de reclamar de arquivo inválido.

A quarentena é deliberadamente simples: um envio não tem slug, tag, ícone nem vínculo, e o SKILL.md fica com o frontmatter dentro dele — é o arquivo cru que se lê e se edita, até alguém aprovar. Dois envios podem ter o mesmo nome; o que os distingue é o identificador e a data.

Aprovar cria a skill no acervo, com quem aprovou como dono, já no destino do envio — dentro dos catálogos escolhidos, e com isso em todo servidor vinculado a eles, e publicada nos servidores escolhidos —, e tira o envio da fila. Sem destino ela nasce sem servidor nem catálogo, e publicar fica para depois. O destino se edita na ficha do envio até a aprovação; quem escolhe e quem aprova precisam editar cada catálogo e servidor dele, e quem aprova sem esse acesso tem a aprovação recusada, sem nada criado. Quem submeteu fica registrado na trilha de auditoria. Quem pode aprovar é escolha da instalação, em Configurações → Quarentena:

Opção Quem aprova
admin só administradores
admin+owner (padrão) administradores e quem submeteu o envio
admin+editor administradores e editores

Revisar e corrigir os arquivos de um envio é de quem o enxerga — o dono, os administradores e os editores —, independente de quem aprova. Aprovar exige também o papel de criar no acervo (editor para cima): aprovar é criar uma skill. Criar uma skill pelo formulário continua indo direto para produção: o portão existe para o pacote de terceiro.

Dentro da edição de uma skill já cadastrada entra só arquivo de texto, um a um. Imagens e outros binários chegam pelo pacote, na importação — e trocar a árvore de uma skill que já existe deixou de ter caminho no painel; quem precisa disso usa o set_files_bulk do MCP administrativo. O desenho está em docs/15-quarentena.md.

Clonagem: uma cópia fechada

Skill, catálogo e MCP virtual têm um botão "Clonar" — na ficha e na linha da lista —, e as tools clone_skill, clone_catalog e clone_virtual_mcp no MCP administrativo. O diálogo vem com nome e slug preenchidos e editáveis; confirmar cria a cópia e abre a ficha dela.

Tipo O que a cópia leva
Skill Propriedades, arquivos e tags. Nasce flutuante: sem servidor, sem catálogo
Catálogo Propriedades e os membros, cada participação com o ligado/desligado que tinha
Servidor MCP Propriedades, vínculos de skill e de catálogo (as três portas e as posições do canvas) e as concessões

Três regras valem para os três:

  • A cópia nasce fechada. Nunca pública, nunca aberta, mesmo que o original seja — expor é um ato à parte, com o aviso de sempre. Ligado/desligado (is_active), esse sim, é copiado.
  • Quem clona é o dono, e os contadores nascem em zero. O dono do original não ganha acesso à cópia: dono não é uma concessão, então não há o que copiar.
  • Chave psv_ não é copiada. O segredo nunca esteve no banco (só o hash), então uma chave duplicada não abriria nada. A cópia nasce sem chave.

Clonar exige edit no original e o papel de criar (editor ou admin); no servidor MCP o mínimo é manage, porque a cópia leva a lista de concessões e ler essa lista já é poder de manage. Não há clonagem profunda: clonar um servidor não duplica as skills dentro dele. O desenho está em docs/16-clonagem.md.

Contadores e ranking

Cada skill tem view_count e download_count. O ranking do site é a soma simples dos dois (ORDER BY view_count + download_count DESC), calculada em tempo de query.

O incremento é atômico e acontece em qualquer superfície de acesso — página do site, API REST e, no MCP, get_skill, resources/read e prompts/get. Acesso direto a arquivos auxiliares não conta; apenas o SKILL.md e o download do pacote.

Cada uma dessas leituras também deixa um registro (skill_accesses): quem leu — a chave psv_ do servidor, a conta por trás de uma chave psk_ do MCP administrativo, ou ninguém —, por qual servidor e catálogo, de que IP e com que cliente. O painel mostra os últimos na guia Auditoria da skill e do catálogo, para quem os administra (docs/13-fichas-e-acessos.md). O registro nunca é apagado.

Contas, papéis e acesso

O painel usa contas. Numa instalação nova a tabela nasce vazia: a ADMIN_PASSWORD ainda entra sozinha e o painel oferece criar o primeiro administrador. A partir da primeira conta, ela fica inerte — o acesso passa a ser sempre por conta e senha.

Cada conta tem um perfil: o nome de exibição, uma foto e — se a pessoa quiser — descrição, site e links. O bloco público é opt-in: desligado, nada dele sai do painel; ligado, ele ganha página própria em /u/<usuário> no site, com as skills e os catálogos que a pessoa já tinha tornado públicos. Quem escreve é o dono da conta; um administrador só pode limpar um perfil, o que fica na trilha de auditoria. Detalhes em docs/20-perfil.md.

Cada conta tem um usuário (@fulano) e um e-mail, e os dois papéis são diferentes. O usuário é o identificador público: é por ele que a conta aparece como dona de uma skill, na lista de quem tem acesso, na trilha de auditoria, na busca de "compartilhar com…" e na ficha pública do site. O e-mail é privado — só a própria pessoa e os administradores o veem — e serve a três coisas: entrar, recuperar a senha e casar a conta com uma identidade do SSO. No login, um campo só aceita os dois: com @ é lido como e-mail, sem @ como usuário. O desenho inteiro está em docs/19-username.md.

Ação admin editor membro
Ver tudo ✅ ❌ ❌
Criar skill, catálogo e servidor MCP (e virar dono) ✅ ✅ ❌
Editar / administrar o que é seu ou lhe foi concedido ✅ ✅ ✅
Apagar e transferir o que é seu ✅ ✅ ✅
Gerenciar contas e papéis ✅ ❌ ❌
Ver a trilha de auditoria ✅ ❌ ❌
Emitir chaves de API para si ✅ ✅ ✅

O papel decide só quem cria e quem administra a instalação. O escopo é por objeto (docs/12-acesso-granular.md):

  • Skills, catálogos e servidores MCP têm dono. Quem cria é o dono e faz tudo, inclusive apagar e transferir. Admin é dono de tudo.

  • Concessões por objeto, cumulativas: view (ler; num servidor ou catálogo, ler também as skills dentro), edit (conteúdo da skill, membros do catálogo, vínculos e canvas do servidor) e manage (slug, estado, público/aberto, chaves e as próprias concessões). Quem tem manage concede a outros; quem tem view numa skill pode vinculá-la aos servidores e catálogos que edita.

  • Público: uma skill ou um catálogo marcado como público é legível por qualquer conta e pelo site, sem concessão. Um servidor aberto (is_open) já é público por definição. Um catálogo público ou um servidor aberto expõe o que está dentro, mesmo skills privadas — o painel avisa.

  • Quem não é admin só lista o que é seu, o que lhe foi concedido e o que é público; as listas do painel têm o filtro Meus / Compartilhados comigo / Públicos.

  • Chaves de API (psk_<prefixo>_<segredo>) substituem o token global ao configurar um agente: carregam o papel do dono e aparecem na auditoria com o nome dele. O texto completo é mostrado uma única vez, na emissão.

  • Revogação é individual: trocar senha, mudar papel ou desativar a conta invalida na hora todos os cookies e sessões daquela pessoa. Contas não são apagadas — são desativadas, para a auditoria continuar fazendo sentido.

  • Login por SSO (OIDC) é opcional. Ligue com OIDC_ISSUER + OIDC_CLIENT_ID + OIDC_CLIENT_SECRET e registre no provedor o redirect_uri <ADMIN_PUBLIC_URL>/api/auth/oidc/callback. Defina OIDC_ALLOWED_DOMAINS: com a lista vazia o SSO recusa todo login, até o de quem já tem conta e já entrava por ele — é de propósito, porque sem allowlist qualquer conta do provedor entraria como membro e leria tudo o que é público. Enquanto ela não estiver preenchida, só o login local funciona.

  • Redefinição de senha por e-mail exige SMTP_URL + SMTP_FROM. Sem SMTP o painel continua completo: o administrador gera uma senha temporária, e a pessoa é obrigada a trocá-la no primeiro acesso.

  • Rate limiting no login em duas camadas: janela em memória por IP e trava da conta (LOGIN_MAX_ATTEMPTS, LOGIN_LOCK_SECONDS).

  • Linha de comando no servidor (purple-admin), para quando ninguém consegue entrar no painel — o único admin esqueceu a senha e não há SMTP, a instalação ficou sem administrador, a conta está travada. Roda dentro do container do painel e passa pelas mesmas regras e pela mesma trilha de auditoria (ator cli):

    docker compose exec admin purple-admin help
    docker compose exec admin purple-admin user passwd admin@empresa.com      # sorteia uma temporária
    docker compose exec admin purple-admin user add --username ana --email ana@empresa.com --role editor
    docker compose exec admin purple-admin user role ana admin

    Também há user list, show, disable, enable, unlock e logout. Para informar a senha sem deixá-la no histórico, use --password-stdin (com exec -T). Referência completa em docs/21-cli-admin.md.

O desenho completo, com as decisões e o que ficou de fora, está em docs/05-accounts-and-roles.md.

Mudança de comportamento na série beta (acesso granular). O papel leitor virou membro e deixou de ver o acervo inteiro: um membro (e um editor) vê só o que é seu, o que lhe foi concedido e o que é público ou está em servidor aberto. Skills existentes ganham como dono quem as criou; nada nasce público. Depois de atualizar, marque como públicas as skills que devem continuar visíveis a todos, ou conceda acesso na seção Acesso de cada uma. Sessões abertas com o papel antigo pedem novo login.

Mudança de comportamento na série beta. A ADMIN_PASSWORD deixa de logar assim que existir a primeira conta. Instalações existentes sobem sem intervenção — quem não passar pelo setup continua entrando com ela indefinidamente.

Configuração

As variáveis estão documentadas em .env.example — ele é a lista, e quem passa a ler uma variável nova a acrescenta lá no mesmo commit. Os segredos marcados com / _FILE na tabela aceitam <NOME> ou <NOME>_FILE; os do banco (POSTGRES_PASSWORD, DATABASE_URL, PGPASSWORD) e o INSPECTOR_API_TOKEN só aceitam o valor direto:

Variável Obrigatória Descrição
DATABASE_URL sim* Conexão com o Postgres (senha percent-encodada). *Alternativa sem escape: PGHOST/PGUSER/PGPASSWORD/PGDATABASE — é o que o compose usa
ADMIN_PASSWORD / _FILE sim (admin) Senha de bootstrap: cria o primeiro administrador e depois fica inerte
ADMIN_DOCS_URL, ADMIN_SUPPORT_URL, ADMIN_CHAT_URL não Links externos da sidebar do painel; vazio some do menu (a documentação aponta para este README por padrão)
ADMIN_BRAND_NAME, ADMIN_BRAND_ICON_URL não Marca do painel: nome e ícone da sidebar, do login e da aba. O nome cai em SITE_NAME; o ícone aceita URL http(s) ou caminho do painel, e valor inválido derruba o boot
MCP_SESSION_ONLINE_WINDOW_MS não Janela em que um cliente do MCP público conta como online no painel (padrão 2 min); agrupa as requisições stateless de um mesmo cliente numa sessão
MCP_MAX_SESSIONS, MCP_MAX_SESSIONS_PER_IDENTITY não Sessões MCP em memória: teto do processo (padrão 500) e teto por credencial. Deixe o segundo vazio — o padrão dele é um décimo do primeiro (mínimo 10), e um número fixo desfaz essa relação. Estourar o teto da credencial fecha a sessão mais parada dela, e o cliente reabre. Num MCP virtual aberto todos os clientes sem chave são a mesma credencial e dividem esse teto (50 no padrão): para dar mais vagas a eles, suba o primeiro — o transporte sem sessão (/mcp/stateless) não ocupa vaga
SITE_RATE_LIMIT_MAX, MCP_RATE_LIMIT_MAX não Requisições por minuto por IP no site (240) e no MCP público (600); 0 desliga, para quem já limita no proxy. Quem chega com chave psv_ não gasta a cota do endereço
MCP_MAX_BATCH não Mensagens aceitas num lote JSON-RPC (um POST cujo corpo é um array) no MCP público (padrão 20; 1 recusa todo lote de mais de uma mensagem). Acima disso a resposta é 400, e cada mensagem do lote gasta uma marca de MCP_RATE_LIMIT_MAX. Vale mesmo com o limite de taxa em 0: o proxy também conta requisição, não mensagem
MCP_MAX_STATELESS_SESSIONS não Identidades stateless contabilizadas ao mesmo tempo (padrão 5000, mínimo 1 — aqui 0 não desliga, derruba o boot); atingido o teto, a requisição é atendida sem virar linha em mcp_sessions
MCP_PUBLIC_JSON_LIMIT, MCP_ADMIN_JSON_LIMIT não Teto do corpo JSON de cada MCP (1mb no público, 48mb no admin, de propósito). São duas para um valor só não levar o teto do admin à superfície anônima; MCP_JSON_LIMIT, o nome antigo, ainda vale como queda do admin e não chega mais ao público. Formato 64mb, 512kb ou inteiro de bytes — fora disso o serviço não sobe
MCP_MAX_FILE_TEXT_BYTES, MCP_ADMIN_MAX_FILE_TEXT_BYTES não Teto do texto que as leituras MCP devolvem dentro da resposta (4 MiB), nos dois MCPs; a segunda vale só para o admin e, vazia, acompanha a primeira. No público (get_skill, get_skill_file, prompts/get, resources/read) a resposta acima do teto é a URL de download — em prompts/get e resources/read, um erro que traz a URL, ou só o tamanho quando a skill não está nas ferramentas daquele servidor. No admin (get_skill, get_file), que não tem rota de download, get_file recusa com tamanho e tipo e get_skill omite o corpo e informa o tamanho
SITE_CORS_ORIGIN não Origens aceitas na API pública do site (padrão *); feche numa instalação que só deve ser lida de dentro. O cartão "API REST pública" da home acompanha ("CORS aberto" ou "CORS restrito"); a lista de origens não é publicada
TRUST_PROXY não Em quais proxies confiar no X-Forwarded-*. Vazio: um salto, e só quando quem abre a conexão é loopback ou faixa privada. Dois proxies internos em cadeia precisam declarar o valor
MCP_PUBLIC_SERVER_NAME, MCP_ADMIN_SERVER_NAME não Nome de cada MCP no handshake (purple-skills e purple-skills-admin); são duas para os dois servidores não aparecerem com o mesmo nome no mcp.json de quem conecta
TAG, APP_VERSION não Tag das imagens que o compose usa (padrão latest) e versão que os MCPs anunciam no handshake; preencha as duas com o mesmo valor
APP_MEM_LIMIT, HOMEPAGE_MEM_LIMIT não Teto de memória dos containers de aplicação (1536m) e da homepage (256m). Subiu ZIP_MAX_UNCOMPRESSED_BYTES? Suba o primeiro na mesma conta
POSTGRES_MEM_LIMIT, DB_JOB_MEM_LIMIT não Teto de memória do container do banco (2048m) e dos passos migrate/seed (512m), em database/docker-compose.yml. O do banco cobre as áreas compartilhadas, os 50 backends que o pool dos apps abre (DB_POOL_MAX × 5) e três autovacuum: subiu um desses, suba o teto — apertado demais, o OOM killer derruba o postmaster
POSTGRES_BIND_ADDR não Endereço em que a porta do Postgres é publicada (padrão 127.0.0.1). Não acompanha o BIND_ADDR: nenhum app usa essa porta — todos falam com o banco pela rede interna —, e publicá-la expõe um servidor sem limite de tentativa de login nem trava de conta
DB_POOL_MAX não Conexões simultâneas no pool de cada app (padrão 10)
ADMIN_SESSION_SECRET / _FILE recomendada Chave do cookie de sessão (derivada da senha com scrypt se ausente). Gere com openssl rand -hex 32
MCP_ADMIN_TOKEN / _FILE sim (mcp-admin) Bearer token administrativo
SITE_BASE_URL recomendada Base da URL da página de uma skill pública, devolvida pelo MCP
MCP_PUBLIC_URL, MCP_ADMIN_URL, ADMIN_URL não Endereços mostrados na seção "Endereços de acesso" do site; vazio = o cartão some. MCP_PUBLIC_URL é a base, sem /mcp — o site acrescenta o sufixo ao mostrar o MCP público, e a mesma base monta os MCPs virtuais no painel e as URLs de download do mcp-public. MCP_ADMIN_URL e ADMIN_URL são o contrário: o endereço completo, mostrado como está — a do MCP administrativo já vem com o caminho do transporte (https://mcp-admin.example.com/mcp), porque é ela que vai para o mcp.json copiável do site; só com o host, o agente recebe 404
ADMIN_PUBLIC_URL recomendada (SSO e SMTP) Base do redirect_uri do OIDC e do link de redefinição de senha. Sem ela o link só é montado para pedido vindo de rede interna; numa instalação exposta, POST /api/password-reset/request responde 503 e a redefinição passa a ser feita por um administrador. Também é a origem do próprio painel na checagem anti-CSRF das escritas, que confere nome e porta: necessária quando o proxy publica o painel numa porta que não repassa no Host
OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET / _FILE não Ligam o login por SSO (os três juntos)
OIDC_ALLOWED_DOMAINS sim, com SSO Domínios de e-mail autorizados; vazia recusa todo login por SSO, inclusive de contas que já existem e já estão vinculadas
SMTP_URL / _FILE, SMTP_FROM não Ligam a redefinição de senha por e-mail
LOGIN_MAX_ATTEMPTS, LOGIN_LOCK_SECONDS não Trava da conta após tentativas erradas (padrão: 8 / 900s)
RAG_DRIVER, RAG_MODEL não Ligam a busca semântica: google, openai, voyage ou off. Lidas só pelo admin, que as semeia no banco no primeiro boot — depois quem manda é o painel. Sem RAG_MODEL, vale o padrão do driver
RAG_GOOGLE_API_KEY, RAG_OPENAI_API_KEY, RAG_VOYAGE_API_KEY / _FILE sim, com o driver ligado A chave de cada provedor. Vão para o indexer, o mcp-public e o site; nunca para o painel. Mais de uma configurada permite trocar de driver sem recriar container
RAG_GOOGLE_BASE_URL, RAG_OPENAI_BASE_URL, RAG_VOYAGE_BASE_URL não URL base de cada provedor, já com a versão da API. Usada como está
RAG_QUERY_TIMEOUT_MS, RAG_INDEX_INTERVAL_SECONDS, RAG_INDEX_TIMEOUT_MS não Prazo do embedding da consulta (padrão 2000 ms), intervalo do indexador (padrão 30 s) e prazo de cada chamada ao provedor durante a indexação, tentativas incluídas (padrão 120000 ms) — nenhuma chamada ao provedor fica sem prazo

Só o .env.example é versionado, e com CHANGE_ME no lugar de cada segredo — o CI reprova qualquer outro .env* que entre no índice. Como o placeholder é público, as credenciais que autenticam alguém (ADMIN_PASSWORD, ADMIN_SESSION_SECRET, MCP_ADMIN_TOKEN) são recusadas no boot enquanto estiverem com ele: o serviço não sobe, com a mensagem dizendo o comando que gera um valor bom. Os arquivos preenchidos (.env, .env-builder, run-builder.sh, docker-compose-builder.yml) são do ambiente de testes do mantenedor, ficam fora do Git e da imagem, e não fazem parte do projeto publicado. Gere seus próprios segredos: nenhum valor deste repositório — inclusive os que o commit inicial publicou antes de virarem placeholders — deve ser reaproveitado. Ver docs/02-architecture-decisions.md §7.5.

Limitações assumidas no v1

Documentadas em docs/02-architecture-decisions.md:

  • Contadores sem deduplicação — infláveis por refresh-spam.
  • Sem limite por skill (soma dos arquivos). Existem tetos por requisição: upload de 64 MB (ADMIN_MAX_UPLOAD_BYTES), 256 MB descomprimidos por pacote (ZIP_MAX_UNCOMPRESSED_BYTES — orçamento único, descontado por cada camada de compressão aberta, o que deixa um .tar.gz com ~metade desse valor de conteúdo útil, contra o valor inteiro no .zip) e 32 MB de base64 no set_files_bulk (MCP_MAX_ZIP_BASE64). O teto de entradas deixou de ser um só: 512 entradas por requisição (ZIP_MAX_ENTRIES) — a importação passou a usar BUNDLE_MAX_ENTRIES (20 000) para o pacote inteiro e ZIP_MAX_ENTRIES (512) por skill, com BUNDLE_MAX_SKILLS (200) limitando quantas skills traz o pacote cuja raiz não é skill, que é o único que vira várias; o set_files_bulk, que não passa por esse caminho, continua nos 512. A tabela inteira, com o que acontece ao estourar cada um, está na §10 de docs/15-quarentena.md.
  • Sem versionamento de arquivos (apenas um log de auditoria, sem restore).
  • audit_log sem política de retenção.
  • Busca semântica sem índice vetorial. A perna vetorial é varredura exata: o HNSW do pgvector para em 2000 dimensões e o modelo do Google tem 3072. Ligar a busca vale para o acervo inteiro (não há recorte de escopo), e não há como apagar os vetores de um espaço — o texto que fica órfão, esse sim, é coletado pelo indexador no fim de cada ciclo, com os vetores dele (migration 025). Ver a §12.5 de docs/02-architecture-decisions.md.
  • Com SSO ligado, a vinculação a uma conta local é sempre pelo e-mail: confie no provedor que você configurar e restrinja OIDC_ALLOWED_DOMAINS.
  • Um MCP virtual aberto é público: o site o lista, com suas skills, sem confirmação. Endereço obscuro nunca foi proteção; quem não quer aparecer fecha o servidor e emite chaves.

Licença

MIT.

About

AI Agent skill manager and MCP server for managing and retrieving skills.

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages