CORTEX · doc

Storage Service

armazenamento de objetos

8 regras · v0.5.0 · rascunho · owner: plataforma · 21 KB · storage-service · sumário em JSON

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.md Documentos 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 é:

  1. O produtor pede ao storage-service para guardar um objeto, mandando o metadado (tipo, tamanho declarado, dono, contexto) — não o byte.
  2. O storage-service decide a object_key (§2), grava o metadado como pending (§3) e devolve uma autorização de upload assinada com policy, na qual o bucket impõe, antes do byte subir: a object_key, o content-type e 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 tripla tenant × context × content_type (§7).
  3. 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õe presign_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: o content-type da 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) pelo storage-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_upload autorizaçã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_get leitura por URL assinada (§5)
    head_object (existe? tamanho?) confirmação do commit pending → committed (§3)
    delete_object remover 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. O institutional-service é o dono do dado (executor burro, regra no institutional — ECOSYSTEM §2).
  • O storage-service manté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 status segue sendo pending/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 (via delete_object, §6) e marca o metadado invalid. 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. Objeto unverified ou invalid tem GET negado, mesma postura do pending.
  • 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 no committed e 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; o delete_object em si já está disponível.
  • Regra recíproca no institutional-service (dona da política de upload — §7). Como o institutional-service cria, 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).