CORTEX — storage-service (armazenamento de objetos)
Versão: 0.5.0 · Data: 2026-06-18 · Status: Rascunho · Owner: Plataforma Caminho canônico:
cortex-docs/services/STORAGE_SERVICE.mdDocumentos irmãos:cortex-docs/ECOSYSTEM.md(mapa do ecossistema) ·cortex-docs/standards/UNISCALE_ENGINEERING_STANDARDS.md(padrões técnicos)
Este documento define as regras do storage-service — o dono do controle de objetos binários (arquivos) do CORTEX. Rascunho pendente de revisão.
O storage-service segue o mesmo princípio dos demais executores do ecossistema (ECOSYSTEM §2): aluga o músculo, mantém o juízo. O bucket (S3/MinIO) é capacidade bruta alugada (buy) — guarda byte, e byte é byte. O storage-service é a casca de governança (make): é dono da chave, do metadado, da permissão de acesso e do fato de que um objeto foi guardado. Ele nunca carrega o byte — autoriza e registra; quem carrega o peso é o bucket.
Tem banco próprio (metadado dos objetos; UNISCALE §banco-por-serviço). Não tem banco vetorial. O conteúdo dos arquivos alimenta o RAG por outro caminho (ver §4 e RAG_AND_INGESTION), não aqui.
1. Upload por URL assinada, sem proxy de byte (Obrigatório)
O produtor (ex.: institutional-service) não envia o arquivo para dentro do storage-service. O fluxo é:
- O produtor pede ao
storage-servicepara guardar um objeto, mandando o metadado (tipo, tamanho declarado, dono, contexto) — não o byte. - O
storage-servicedecide aobject_key(§2), grava o metadado comopending(§3) e devolve uma autorização de upload assinada com policy, na qual o bucket impõe, antes do byte subir: aobject_key, ocontent-typee o intervalo de tamanho (content-length-range, com teto), tudo expirando num TTL curto. Esses critérios (teto, content-type, TTLs) não são globais nem fixados em deploy — vêm da política de upload resolvida pela triplatenant × context × content_type(§7). - O produtor faz o upload direto no bucket com essa autorização. O byte nunca transita pelo serviço. Upload que furar a policy (acima do teto de tamanho, content-type errado) é rejeitado pelo próprio bucket — o byte não é aceito.
O storage-service não faz proxy do byte — nem na entrada. Proxyar o byte tornaria o serviço gargalo e consumidor de memória para arquivo grande, pagaria banda dobrada (entra no serviço, sai pro bucket) e não acrescentaria governança — toda a governança é exercida pela autorização assinada, sem tocar no conteúdo.
Nota técnica (mecanismo, Obrigatório seguir): uma URL assinada de PUT simples (S3/MinIO) não impõe tamanho máximo — trava só chave, TTL e, com esforço, content-type; quem chega com 10 GB sobe os 10 GB. Para o bucket honrar os critérios do arquivo antes do upload, usa-se POST assinado com policy (condições
content-length-range+content-type), que o bucket rejeita se violado. Por isso a porta (§6) expõepresign_upload(a operação que carrega a policy), não um PUT cru. Driver que não suporte policy de upload não atende o requisito de governança desta regra. (Atenção: ocontent-typeda policy é o declarado — o bucket confere a string, não os bytes; a verdade do tipo é validada de forma assíncrona na §8.)
2. object_key opaca e não-enumerável (Obrigatório)
A object_key (caminho do objeto no bucket) é decidida pelo storage-service, nunca pelo produtor, e é opaca, aleatória e não-enumerável (UUIDv7; UNISCALE §types/identidade). É proibido derivar a chave de dado de negócio adivinhável (CPF, e-mail, id sequencial, nome de arquivo do usuário) — chave previsível permite varredura do bucket. O nome original do arquivo, quando relevante, vive como metadado, não como chave.
3. Ciclo de vida do objeto: pending → committed (Obrigatório)
Todo objeto tem estado de metadado explícito:
pending— criado no instante em que a autorização de upload é emitida (§1). O byte ainda pode não existir no bucket.committed— só após confirmação de que o byte chegou, por dois caminhos complementares: o primário é a notificação do bucket (evento de objeto criado), de baixa latência; a rede de segurança é a verificação ativa (HEAD) pelostorage-service, que reconcilia objetos cuja notificação se perdeu. Tanto a notificação quanto a verificação ativa conferem existência e tamanho real — defesa em profundidade sobre a policy de upload (§1).
Um objeto pending é incompleto: não é servido para leitura como definitivo e não dispara efeito a jusante (§4). Objetos que ficam pending além do TTL são lixo de upload abandonado e podem ser coletados — a coleta apaga apenas o metadado (o registro pending); o storage-service não apaga o byte no bucket (a porta §6 não expõe operação de delete). Byte órfão eventual (upload que furou a janela e nunca commitou) é responsabilidade da lifecycle policy do bucket (configuração de ops), não do serviço. A transição para committed é a única fonte de verdade de que o arquivo existe — nunca assuma que o upload teve sucesso só porque a URL foi emitida.
4. Fato de "objeto guardado" publicado na fila (Obrigatório)
Somente após committed (§3), o storage-service publica um fato ("objeto guardado") no exchange topic (ECOSYSTEM §5). Quem se interessa consome de forma independente — o storage-service não sabe quem ouve e não chama o consumidor direto. É assim que o conteúdo chega à vetorização: o worker de ingestão do domínio interessado consome o fato, puxa o objeto (URL assinada de GET, §5) e seu pipeline o vetoriza via embedding-service (o storage-service não fala com o embedding-service nem escreve no Qdrant — ECOSYSTEM §4, RAG_AND_INGESTION).
O fato carrega object_key + metadado, nunca o byte. Publicar antes do committed propagaria um arquivo que pode não existir.
5. Leitura por URL assinada, sem proxy de byte (Obrigatório)
A leitura é simétrica à escrita: o consumidor pede ao storage-service uma URL assinada de GET (escopada à chave, com TTL curto), e o bucket serve o byte direto. O storage-service não faz proxy de leitura e o bucket é privado — não há acesso público ao objeto; todo acesso passa por uma URL assinada de vida curta emitida pelo serviço, que é onde mora a decisão de quem pode ler o quê. A leitura também é gateada pela validação de conteúdo (§8): por padrão (estrito), o GET só é liberado para objeto valid — objeto não-validado ou inválido tem leitura negada, como o pending.
6. Porta de armazenamento com drivers plugáveis (Obrigatório)
O desacoplamento do fornecedor de bucket é requisito de design, não detalhe (aplicação da regra de plataforma UNISCALE §29.5). O núcleo do serviço fala com uma porta de armazenamento (interface), nunca com o SDK de um fornecedor diretamente. Os drivers são plugáveis, selecionados por variável de ambiente (STORAGE_DRIVER):
-
Começa com MinIO, usando o SDK oficial
minio— explicitamente MinIO, não um cliente AWS (boto3) apontado para o MinIO. -
Driver de AWS S3, depois Google, entram somando um adapter, sem tocar no núcleo.
-
A porta expõe o mínimo que as regras deste documento exigem, e nada de tipo/conceito específico de fornecedor vaza para o núcleo:
Operação da porta Serve à regra presign_uploadautorização de upload assinada com policy ( content-length-range+content-type) — realizada como POST assinado no S3/MinIO, não um PUT cru (§1)presign_getleitura por URL assinada (§5) head_object(existe? tamanho?)confirmação do commit pending → committed(§3)delete_objectremover o byte no bucket — quarentena de conteúdo inválido (§8) e erasure/LGPD (direito ao esquecimento)
A porta cresce só quando uma regra deste documento passa a exigir — a §8 (quarentena) exige delete_object, então a porta passa de 3 a 4 operações; o erasure por LGPD reusa a mesma. boto3 (ou qualquer SDK de fornecedor) amarrado ao núcleo é proibido — o núcleo conhece a porta, os drivers conhecem o fornecedor.
7. Política de upload replicada e versionada (Obrigatório)
Os critérios que a autorização de upload impõe (§1) — teto de tamanho, content-types aceitos e TTLs (upload, GET, pending) — não são globais nem fixados em deploy. Vêm de uma política de upload endereçada pela tripla:
tenant_id(dono/chamador) ×context×content_type
A presença de uma política para essa tripla é a permissão de upload daquele tenant naquele contexto e tipo. Sem política aplicável (ou desabilitada) → o upload é negado (default seguro; nunca um teto global implícito).
Fonte da verdade e réplica
- A política é criada e editada no
institutional-service, por usuário autorizado lá — ajuste sem deploy. Oinstitutional-serviceé o dono do dado (executor burro, regra no institutional — ECOSYSTEM §2). - O
storage-servicemantém uma réplica local versionada da política no seu próprio banco. Não lê o banco do institucional nem faz chamada síncrona no caminho do upload (banco-por-serviço; performance e desacoplamento). - A propagação institucional → storage é por fato/evento (mesma mecânica de fila do §4), não por banco compartilhado. Cada mudança publica uma nova versão; a réplica aplica o upsert idempotente pela versão.
Aplicação
No instante em que a autorização de upload é emitida (§1), o storage-service resolve a política pela tripla e injeta na POST-policy assinada o content-length-range (teto), o content-type permitido e os TTLs (upload §1, GET §5, pending §3). O bucket segue sendo quem rejeita o byte que furar — a política só alimenta os critérios.
Conteúdo da política (escopo mínimo coerente)
Carrega apenas o necessário para um upload seguro e escalável; nada além agora (antivírus, quota, checksum, etc. não entram — extensões futuras são campos novos, sem quebrar o contrato):
| Campo | Função |
|---|---|
tenant_id, context, content_type |
chave da política (a permissão) |
max_size_bytes |
teto do content-length-range |
ttl_upload_seconds |
validade da autorização de upload (§1) |
ttl_get_seconds |
validade da URL assinada de leitura (§5) |
ttl_pending_seconds |
janela antes do GC do pending (§3) |
enabled |
liga/desliga sem apagar a linha |
require_valid_before_read |
postura de leitura por contexto (§8): exige valid antes do GET; ausente ⇒ estrito |
version |
versão vinda do institucional (ordena o upsert) |
created_by, updated_by, timestamps |
autoria/versão (UNISCALE §23.6 + colunas base) |
Versão e auditoria
Toda linha de política carrega quem criou e quem atualizou e a versão de origem. A réplica preserva o rastro de versão para auditoria; o histórico completo de edição é responsabilidade do dono (institutional-service).
Relaciona-se com: §1 storage-presigned-upload (ponto de aplicação), §4 storage-stored-event (mecânica de fato/réplica), §6 storage-driver-port (o driver tem de suportar POST-policy), uniscale.columns.authorship, banco-por-serviço.
8. Validação de conteúdo assíncrona e quarentena (Obrigatório)
O content-type que o bucket impõe na subida (§1) é o valor declarado no formulário assinado — o S3/MinIO confere a string, não os bytes. Um cliente pode declarar application/pdf e subir um .html: a policy passa e o objeto fica gravado com o tipo errado. Nenhum S3 inspeciona conteúdo. A garantia real do bucket é tamanho (content-length-range, medido nos bytes); tipo é declaração.
Por isso a verdade do conteúdo (os bytes batem com o tipo declarado? o arquivo é seguro?) é verificada fora do caminho de upload, de forma assíncrona.
Gatilho e fronteira
- Um validador (worker) consome o fato
storage.object.stored(§4) e processa o objeto depois do commit — nunca no caminho síncrono de upload/leitura. - O validador pode ler o byte (range para sniff, ou o objeto inteiro para scan). Isso não viola o "sem proxy" de §1/§5: aquilo veda proxiar o byte na requisição do cliente; aqui é um consumidor interno em background, não a chamada do usuário.
O que valida (escopo mínimo coerente)
| Verificação | Nível |
|---|---|
| Tipo real × declarado (assinatura / magic bytes dos primeiros bytes) | Obrigatório |
| Antivírus / scan de segurança | Opcional e plugável (driver por env, como §6; não obrigatório no MVP) |
Nada além disso agora (sem parsing profundo, OCR, etc.) — extensões futuras entram como verificações plugáveis, sem quebrar o contrato.
Veredito e quarentena
- O objeto carrega um estado de validação ortogonal ao ciclo de vida da §3 (o
statussegue sendopending/committed):validation ∈ {unverified, valid, invalid}. São dois campos de estado único para concerns distintos — não fere o "estado em campo único" do UNISCALE §29.4. invalid(tipo não bate ou scan reprovou) → quarentena: o serviço apaga o byte no bucket (viadelete_object, §6) e marca o metadadoinvalid. Quarentena é apagar o byte (mais simples e seguro), não mover para área separada.- O validador publica um fato (
storage.object.validated/storage.object.rejected) para o produtor/consumidores reagirem (avisar o usuário, reprocessar).
Postura de leitura
- Default estrito: a leitura (§5) só é liberada após
valid— fecha a janela em que um arquivo malicioso seria baixável, ao custo da latência até validar. Objetounverifiedouinvalidtem GET negado, mesma postura dopending. - A postura pode ser relaxada por contexto via a política de upload (§7, campo
require_valid_before_read): contextos não-sensíveis podem liberar nocommittede contar com a invalidação posterior. Ausência ⇒ estrito (default seguro).
Relaciona-se com: §1 storage-presigned-upload (limite do que o bucket garante), §3 storage-object-lifecycle (estado ortogonal), §4 storage-stored-event (gatilho), §5 storage-read-presigned (gating da leitura), §6 storage-driver-port (delete_object), §7 storage-upload-policy (postura por contexto).
Fronteiras a definir (não inventar)
Itens reconhecidos mas ainda não regrados. O construtor que precisar deles para e pede a regra — não improvisa.
- Fluxo de erasure (direito ao esquecimento — LGPD). O mecanismo de apagar byte já existe: a porta expõe
delete_object(§6), trazida pela §8. O que falta é o fluxo/regra de erasure — quem solicita, com que base legal, prazo de expurgo, auditoria do que foi apagado — e o documento técnico de LGPD ("o que fazer e quando"), ainda pendente. Quem for implementar erasure para e pede a regra do fluxo; odelete_objectem si já está disponível. - Regra recíproca no
institutional-service(dona da política de upload — §7). Como oinstitutional-servicecria, edita e publica a política, o doc dele precisa de uma regra-irmã: autorização do editor (quem pode alterar política, por tenant/contexto) e publicação do fato "política de upload alterada" (versão nova, mesma mecânica de fila do §4). Enquanto não escrita lá, o produtor da política para e pede a regra — não improvisa o contrato do fato.
CHANGELOG
| Versão | Data | Resumo |
|---|---|---|
| 0.5.0 | 2026-06-18 | Adiciona §8 — validação de conteúdo assíncrona e quarentena (Obrigatório, ecosystem.services.storage-content-validation): o content-type da policy (§1) é declarado (o bucket confere a string, não os bytes), então a verdade do conteúdo é validada fora do caminho de upload por um worker que consome storage.object.stored (§4) — sniff de tipo real (magic bytes) obrigatório, antivírus opcional/plugável. Estado de validação ortogonal ao ciclo §3 (validation ∈ {unverified, valid, invalid}); invalid → quarentena = apaga o byte e nega leitura. Postura de leitura estrita por default (GET só após valid), relaxável por contexto via §7 (campo require_valid_before_read). §6 ganha 4ª operação delete_object (serve à quarentena §8 e ao erasure/LGPD), o que destrava a fronteira de erasure — o mecanismo de apagar byte agora existe; falta só o fluxo/regra de erasure. §5 passa a gatear a leitura pela validação. MINOR. |
| 0.4.0 | 2026-06-17 | Adiciona §7 — política de upload replicada e versionada (Obrigatório, ecosystem.services.storage-upload-policy): os critérios do upload (teto de tamanho, content-types, TTLs) deixam de ser globais e passam a vir de uma política endereçada pela tripla tenant × context × content_type, criada/editada no institutional-service (dono) e replicada versionada no banco do storage por fato/evento (não banco compartilhado, sem chamada síncrona no caminho do upload), com upsert idempotente pela versão. Deny-by-default: sem política aplicável → upload negado (nunca teto global implícito). §1 passa a apontar a §7 como fonte dos critérios. Nova fronteira: regra recíproca no institutional-service (autorização do editor + fato "política de upload alterada"). MINOR. |
| 0.3.1 | 2026-06-16 | Reconciliação com o primeiro build real (storage-service MVP, E2E 9/9). Editorial: (a) §3 troca as 2 menções narrativas a "PUT" por "autorização de upload"/"upload" (o mecanismo já é POST-com-policy desde 0.3.0); (b) §3 separa os dois caminhos de commit — primário (notificação do bucket) + rede de segurança (reconciliação ativa por HEAD); (c) §3 afirma que a coleta de pending vencido apaga só metadado, byte órfão é lifecycle policy do bucket; (d) nova seção Fronteiras a definir registrando que apagar byte (erasure/LGPD) exige estender a §6 primeiro (4ª operação da porta). Sem regra nova nem mudança de comportamento. PATCH. |
| 0.3.0 | 2026-06-16 | Correção técnica do mecanismo de upload (§1, §6). Uma URL assinada de PUT (S3/MinIO) não impõe tamanho máximo antes do upload — só chave/TTL/content-type. Para o bucket honrar content-length-range + content-type antes do byte subir, o upload passa a ser POST assinado com policy; o bucket rejeita o que furar. Operação da porta renomeada presign_put → presign_upload (vendor-neutra, realizada como POST-policy). §3 reforça head_object conferindo tamanho real no commit (defesa em profundidade). Slug ecosystem.services.storage-presigned-upload preservado (não nomeava "put"). MINOR. |
| 0.2.0 | 2026-06-15 | Adiciona §6 — porta de armazenamento com drivers plugáveis (Obrigatório): núcleo fala com uma interface, fornecedor de bucket atrás de driver selecionado por STORAGE_DRIVER; começa com MinIO via SDK oficial minio (não cliente AWS), AWS S3 e Google entram como adapters sem tocar no núcleo; porta expõe o mínimo (presign_put §1, presign_get §5, head_object §3); boto3 amarrado ao núcleo proibido. Aplica a regra de plataforma UNISCALE §29.5 (stack + desacoplamento de fornecedor). MINOR. |
| 0.1.0 | 2026-06-15 | Rascunho inicial do storage-service. Fixa o serviço como dono do controle de objetos binários (make: chave, metadado, permissão, fato) sobre um bucket alugado (buy): upload por URL assinada sem proxy de byte (§1), object_key opaca/não-enumerável UUIDv7 (§2), ciclo pending→committed com committed como única verdade de existência (§3), fato de objeto guardado publicado na fila só após commit, carregando chave+metadado e nunca o byte, alimentando a vetorização por consumo (§4), e leitura por URL assinada sobre bucket privado (§5). Primeiras regras @rule do namespace ecosystem (área services). Pendente de revisão e de inclusão do serviço no catálogo do ECOSYSTEM (§4/§8). |