Pular para o conteúdo

Questões em aberto

Decisões adiadas com o raciocínio preservado, para não serem re-discutidas do zero — e limitações conhecidas que ainda não têm solução.


A pergunta. Quando uma implementação oferece mais do que o contrato descreve, o que acontece?

Descartado: extra-args. A v0 tinha uma bag livre ao lado dos argumentos. Não é tipada, não é validada, não aparece no contrato — trocar de implementação pode quebrar o pipeline em silêncio. Perde substituibilidade e verificabilidade de uma vez.

Direção escolhida: extension publicável por terceiros, que é, com precisão, herança múltipla limitada a um nível:

apiVersion: oren.sh/v1
kind: TaskExtension
metadata:
name: signing
namespace: security-co # outra organização, sem pedir nada à dona do contrato
spec:
extends: techlite/build-docker-image@^1.0.0
inputs:
signingKey: { type: string, secret: true }
outputs:
signature: { type: string }

Duas restrições fazem a diferença:

  • Profundidade 1. Uma extension não pode ser estendida.
  • Só adiciona. Não remove output nem restringe input, então toda implementação continua satisfazendo o contrato base.

Com isso, uma task recebe N extensions em vez de gerar 2ⁿ contratos, e a org B estende o contrato da org A sem permissão.

O argumento que NÃO decide. Substituibilidade (“tudo que vale para a filha vale para o pai”) vale nos dois modelos. Numa herança irrestrita, uma implementação da filha satisfaz o pai; com extension, a implementação é do contrato base e a substituibilidade é trivial. Quem decide é a governança: registry aberto exige extensão descentralizada, e profundidade limitada evita a explosão combinatória.

Por que adiar. Os onze contratos escritos a partir de um pipeline real não precisaram de nenhuma extension — o extra-args inteiro dissolveu em decomposição e inputs de primeira classe. Construir o mecanismo antes de um caso real é adivinhar a forma dele.

Gatilho para retomar. Um caso que o teste do “se eu trocar de implementação e esse valor sumir, o output muda?” não classifique.


O incômodo. Um pipeline de release tem cinco steps: analisar, versionar, construir, publicar, taggear. Repetido em vários projetos, dá vontade de agrupar.

Duas formas foram consideradas e rejeitadas.

A implementação roda o Oren por dentro. Traz o dagger-in-dagger de volta: exigiria engine/docker, privilégio crítico — exatamente o que foi eliminado de toda a cadeia de imagem.

Task composta declarativa — um contrato que agrupa outros. O problema é mais sutil e mais grave: ela esconde justamente o que o projeto existe para tornar visível. Uma release-node declararia “preciso de gcpCredential, gitToken e um diretório mutável”, e some qual parte precisa de quê. O consentimento vira tudo-ou-nada, o cache vira tudo-ou-nada, effects vira o pior caso de todos os passos, e uma falha reporta “a composição falhou”.

A direção escolhida: template de pipeline. Steps reutilizáveis que a CLI expande na resolução, em vez de encapsular em runtime:

steps:
- use: oren/release-node@^1.0.0
with:
imageName: acme/api

Isso vira quatro steps de verdade antes de qualquer execução. A partir daí tudo funciona como já funciona — consent granular, cache por step, effects por step, privilégio visível por step, e a falha aponta o step real.

A distinção que decide. Composta, a abstração vive em runtime e apaga informação. Expandida, vive só na escrita, e o resultado é idêntico a ter digitado os steps à mão.

Quando a composta seria legítima. Se o agrupamento for uma abstração real, e não encurtamento — algo como deploy-blue-green, que conceitualmente é uma operação. Aí a opacidade é o produto, e declarar a união das dependências é o preço aceito por ela.

O teste: o usuário pensa nisso como uma coisa, ou como N coisas que ele sempre faz juntas? Se é a segunda, é template.

Gatilho para retomar. O mesmo conjunto de steps repetido em três ou mais projetos — aí vale o template. Uma operação que ninguém pensa como sequência — aí vale discutir a composta.

Hoje. O oren.yaml referencia tasks por nome e versão — é o que torna o pipeline portável, já que um caminho de disco não sobrevive a outra máquina nem a um runner de CI. O registry local é a camada que resolve nome → documento, e sem portal ele é a fonte: oren add copia os arquivos para lá, e o oren.lock é derivado deles. Por isso precisa ser commitado — apagá-lo quebra o projeto mesmo com o lockfile intacto.

Com o portal, a direção inverte. O paralelo com npm deixa claro:

npm Oren hoje Oren com portal
package.json oren.yaml oren.yaml
package-lock.json oren.lock (derivado) oren.lock (fonte da verdade)
node_modules/ .oren/registry/ (fonte) .oren/registry/ (cache derivado)

oren install passará a buscar os contratos do portal e materializá-los, e o registry vira cache: gitignorado, descartável, reconstruível — como node_modules.

O caso que sobrevive à inversão. Tasks locais do projeto, que não vale a pena publicar, continuam sendo fonte e não cache. Elas provavelmente precisam de um lugar separado do cache — senão um install que limpe o cache apagaria código sem cópia em lugar nenhum.

Desenho ainda não decidido. Uma possibilidade é .oren/cache/ (derivado, gitignorado) ao lado de tasks/ na raiz do projeto (fonte, commitado). O resolver já trata as duas origens da mesma forma, então a separação é de convenção e ciclo de vida, não de mecanismo.

Gatilho. O primeiro oren install que busque do portal. Antes disso a inversão não tem como acontecer.

O tipo existe no registry, mas o motor de execução dá rede a todo container e não expõe controle por execução. Um worker que omita network continua alcançando a internet.

É a única lacuna do núcleo — todas as outras dependências são concedidas materializando algo, e omiti-las realmente quebra o worker. O registry marca com enforced: false, para que a diferença entre declarado e garantido fique no dado e não só na documentação.

Enquanto isso não mudar, network serve para o portal exibir e comparar, não como garantia.


O spec prevê apontar uma imagem OCI diretamente, com o contrato lido dos labels sh.oren.*. A CLI ainda recusa com mensagem explicando.

É o que tornaria a imagem auto-descritiva e permitiria validar sem portal. Depende de definir a convenção dos labels.


--resume existe, e no CI depende do cache da plataforma. Ver Rodando no CI.

O que continua em aberto — o cache dentro da fronteira de confiança. O estado restaurado decide que steps são pulados e com que saída, e o cache de um projeto de CI costuma ser gravável por qualquer job dele. A revisão gravada impede atravessar commits, mas não impede que outro job escreva no cache deste commit.

Fechar isso pede assinar o estado — chave de assinatura no CI, e quem a guarda. É outro projeto, e por isso --resume é opcional e o arquivo gerado sai com o aviso.

Restrições registradas: steps que produzem output secreto não são retomáveis, porque o valor não é persistido. E steps que escrevem na RAIZ do projeto impedem o cache, porque cachear a raiz restauraria .oren/registry/ e .oren/consent.json de outra execução.


jq obrigatório no modo nativo. Quando um input vem de outro step, o job usa jq para compor o input.json. A imagem do worker precisa tê-lo. No modo oren isso não se aplica: quem compõe é a CLI.

Validação por plataforma. O GitLab tem a saída conferida contra o schema oficial, e o Cloud Build em --mode oren rodou de ponta a ponta num projeto real — build, publicação e deploy. GitHub Actions e o --mode nativo do Cloud Build continuam sem execução real.

Rodar de verdade encontrou três defeitos que nenhuma leitura de documentação teria achado: diretório mutável que não existia na cópia limpa, refspec em checkout destacado, e credencial de git herdada do CI vencendo a do worker. É a razão de não escrever gerador para plataforma que ninguém vai exercitar.

properties não é suportado no modo nativo. Arquivos de propriedades são resolvidos antes da execução e não existem no runner. No modo oren eles funcionam: quem lê é a CLI, dentro do job.


Jenkins, Azure DevOps e Bitbucket não têm gerador, e a decisão de 2026-08-10 foi deixar em espera.

O oren run já funciona nos três: no modo oren o arquivo é fino — um job rodando a imagem do Oren com daemon Docker, segredos como variáveis e o repositório em disco.

O que pesa contra escrever geradores: cada um é dívida permanente, e sem alguém que execute de verdade ele é uma promessa que quebra na casa de outra pessoa.

Azure DevOps é o candidato mais fácil: YAML de forma parecida com o GitHub, Cache@2 nativo (então --resume funcionaria).

Bitbucket tem o melhor encaixe conceitual — ele já tem seções por branch, que é o mapa branches → propertyFile. Mas o serviço Docker dele tem 1 GB por padrão e teto de ~3 GB, e aumentar o tamanho do step não aumenta a memória do serviço. Se o motor de execução cabe ali é pergunta a medir antes de qualquer código.

Jenkins é o mais caro: Groovy em vez de YAML, sem formato único, e sem cache nativo entre builds.

O caminho de maior retorno é documentar o contrato que um CI precisa cumprir, e deixar quem usa escrever as poucas linhas da plataforma dele.


Não existe. O registry local (.oren/registry/) tem a mesma interface que o cliente do portal vai expor, então trocar a origem não deve mexer no resolver nem no executor.

Decisões de produto já tomadas: registry aberto entre organizações, contratos oficiais curados pela org oren, implementações oficiais marcadas como referência e sem peso no ranking, plano pago para implementação privada com os benefícios do portal (resolução por versão, auto-wiring, auditoria, catálogo interno).

Em aberto: política de curadoria do namespace oren, e o desenho da lista de implementações aprovadas por organização — que precisa aprovar versão, não nome, e funcionar offline pelo lockfile.


Base decidida. Modelo próximo ao do npm: usuários, organizações, e membros com papéis. Namespace pertence à organização.

Ajuste sugerido: tudo é organização. Criar conta cria uma organização homônima automaticamente. Evita dois tipos de dono — usuário e org — com regras distintas, que é onde o npm acumulou complexidade. Um usuário solo publica sem pensar em org; quando precisar de time, cria outra e o modelo é o mesmo.

Papéis.

Papel Pode
viewer ver conteúdo privado da organização
publisher publicar versões novas e depreciar
admin gerenciar membros, aprovar implementações, transferir namespace

publisher tem peso permanente. Versão publicada é imutável e não existe deleção — conceder o papel é conceder poder irreversível sobre o namespace. Vale a interface dizer isso ao convidar, e talvez confirmação extra na primeira publicação de um namespace, que é quando o nome fica cravado.

Depreciar é do publisher. Sem deleção, depreciação é o único mecanismo de correção; quem publica precisa poder corrigir sem passar por admin.

viewer só faz sentido com conteúdo privado. No catálogo aberto tudo é público. Não é problema — significa que o papel existe para o plano pago.

Namespace precisa de owner explícito e rota de transferência. Como nada é deletável, um namespace abandonado fica abandonado para sempre — o problema que o npm colheu com pacotes órfãos. Ter um dono nominal (não apenas “quem tem role admin”) e um caminho de transferência é barato agora e caro depois.

Aprovar implementações é de admin. A lista do que a organização confia é governança, não publicação.

Refinamento possível, não agora. Separar “publicar contrato” de “publicar implementação” em papéis distintos — mudar um contrato afeta terceiros, publicar uma implementação afeta menos. Defensável, mas é otimização sem caso real.

Gatilho. Antes do primeiro oren publish. A sequência recomendada é começar pelo catálogo somente-leitura, que exercita a API inteira sem exigir autenticação nem permissões — a parte cara vem quando o modelo de leitura já estiver provado.

A decisão de produto. O portal é self-hostable e gratuito; a cobrança é pelo uso da instância pública. Coerente com o resto: self-host dá controle, a instância pública dá a rede — e cobrar pela rede é mais honesto que reter funcionalidade.

A implicação que ainda não está resolvida. Uma task é referenciada como techlite/analyze-commits@^1.0.0 — namespace, nome e range, sem origem. Numa instalação só isso basta. Com várias instâncias, techlite/build-docker-image pode ser um contrato numa e outro noutra, e o mesmo oren.yaml produziria builds diferentes conforme quem executa.

Direção provável, pelo caminho do npm. Separar as três coisas:

  • Referência continua namespace/nome@range — legível e portável.
  • Origem é configuração do ambiente, não do pipeline (o análogo do .npmrc).
  • Lockfile registra de onde cada task veio de fato.

O lockfile já tem o lugar certo para isso: hoje guarda task como namespace/nome@versão, e passaria a guardar também a origem resolvida. Um projeto que troque de instância vê o diff.

O que NÃO fazer. Colocar o host na referência do oren.yaml (portal.acme.com/techlite/build-docker-image). Isso amarra o pipeline a uma instância e quebra a portabilidade que a referência por nome existe para dar.

Consequência aceita. Cada self-host é uma ilha: o reaproveitamento entre organizações só acontece dentro de uma instância. Federação entre instâncias seria a alternativa, e é bem mais máquina. É o mesmo trade-off já aceito para contrato privado — você troca rede por controle.

Gatilho. Antes do portal aceitar publicação de terceiros. Depois disso, mudar a forma da referência quebra oren.yaml de todo mundo.

Ideia registrada: o portal expõe um MCP que devolve contratos e implementações de uma organização, para um modelo montar o oren.yaml. Recorte definido: somente leitura, execução local — o modelo sugere, o usuário executa.

O que já favorece isso: os contratos são JSON Schema, que é o formato que modelos consomem para tool use; o validate é um verificador determinístico que roda em um segundo; e o consentimento continua sendo do usuário, no terminal dele.

Cuidado registrado: descrições de contratos vêm de organizações terceiras. São dados, não instruções.


A ideia. A CLI reporta o andamento ao portal, e você acompanha pipelines num lugar só — em vez de abrir a interface do GitLab para uns e do Cloud Build para outros.

Por que não é só mais um dashboard. Um CI mostra “job passou em 4s”. O Oren sabe qual task, qual implementação, qual digest executou, qual privilégio foi concedido, o que veio do cache e o que executou de fato. Nenhum CI tem essa informação porque nenhum sabe o que é uma task — e é exatamente o que se quer olhar quando algo dá errado. É consequência natural de o pipeline não pertencer à plataforma: se ele roda em três lugares, olhar três interfaces é o preço que essa visão elimina.

O problema óbvio. Exige rede no CI.

O problema maior, e menos óbvio. O que exatamente sai da máquina. Outputs contêm dados do usuário — nomes de imagem, versões, caminhos, contagens. “Não é secret” não significa “pode ser exfiltrado”. Uma ferramenta de CI que liga para casa por padrão é barrada em muita organização, e com razão.

Desenho que resolve os dois:

  • Metadados, nunca conteúdo. Step, duração, resultado, task e versão, digest, privilégio concedido, cache hit. Jamais valores de input ou output.
  • Não-bloqueante. Falha de telemetria não falha pipeline. Portal fora do ar não impede um build.
  • Opt-in explícito. Nunca padrão.
  • Self-hostable. Uma organização que queira a visão sem mandar nada para fora aponta para o coletor dela.

O que já existe. .oren/runs/<id>/ grava manifest e status por step, com exatamente esses campos. Falta o transporte e a decisão de o quê sai — não o modelo de dados.

Gatilho. Depois do portal, e depois de a visão de catálogo estar de pé. Antes disso não há onde exibir.

As tasks node-build e npm-publish existem e foram testadas contra um registry local. Falta apontar para o npmjs com credencial real e fazer o Oren publicar o Oren.

Ovo e galinha resolvido: a primeira publicação de cada pacote é manual; da segunda em diante o pipeline se encarrega.

Uma pessoa tem direito a apagar seus dados, e uma organização a encerrar a conta. Isso precisa existir. O que ainda não está decidido é o que acontece com o que já foi publicado.

A proposta natural — marcar os artefatos como órfãos e removê-los depois de um prazo — colide de frente com uma vez público, sempre público. Remover é a mesma quebra que tornar privado, só com um atraso: o pipeline de terceiros funciona hoje e falha daqui a noventa dias, sem ninguém ter tocado nele.

O precedente é conhecido. Em 2016 um pacote de onze linhas foi removido do npm e derrubou builds no mundo inteiro; a política mudou logo depois, e hoje npm, crates.io e Maven Central não removem versões publicadas. Nenhum deles trata isso como detalhe de implementação — é a promessa central de um registry.

Separar duas coisas que a proposta junta:

Dado pessoal é apagável, e deve ser. Nome, e-mail, apelido, tokens, histórico de sessão. Isso é o que a LGPD e o GDPR alcançam, e não há motivo para reter.

Artefato publicado permanece, com a autoria anonimizada. O documento não é dado pessoal: é um contrato publicado para consumo de terceiros, e quem o consome tem uma expectativa legítima de que continue existindo. publicada_por vira nulo; o conteúdo fica.

Isso satisfaz o direito ao apagamento sem transferir o custo para quem não escolheu nada.

Não listar. Um artefato órfão sair da listagem e da busca — aparecendo só com um filtro explícito — desincentiva adoção nova sem quebrar adoção existente. É a diferença entre “não recomendamos” e “não existe mais”, e só a segunda quebra alguém.

Depreciação obrigatória. Ao encerrar, tudo que a organização publicou é marcado como depreciado, com motivo. Quem resolver a versão recebe o aviso, e quem estiver escolhendo uma task vê que ela não tem mais dono.

Transferência. Uma organização que encerra pode passar seus contratos a outra que os assuma. É o melhor desfecho, e vale oferecer antes das outras opções.

  • A organização de órfãos é real (uma linha em organizacao) ou é um estado do contrato? Uma organização de verdade mantém o namespace vivo — e o namespace é o que aparece nas referências, então ele precisa continuar resolvendo.
  • Um namespace liberado pode ser registrado por outra pessoa? Quase certamente não: quem pegasse acme passaria a poder publicar versões novas de acme/build, e pipelines que usam ^1.0.0 as receberiam. É sequestro de dependência, e o npm já viveu isso.
  • Retenção de dado pessoal em backup, e o prazo real para eles saírem de lá.

A cobrança não está no jar comunitário. Ela é um starter Spring separado (portal-cobranca) que o portal público acrescenta como dependência.

A razão prática vem antes da comercial: regra de cota em código aberto é regra removível. No jar comunitário ela não protegeria nada — só documentaria o que alguém precisaria apagar. E o auto-hospedado não tem a quem cobrar: a compra já aconteceu, por contrato, antes da instalação.

O que ficou em cada lado:

  • comunitáriogrupo, grupo_admin, organizacao.grupo_id, a contagem de artefatos privados, e a porta LimiteDePublicacao cujo padrão não faz nada. Não é “ilimitado”: o conceito de limite não existe ali;
  • starterassinatura, evento_de_cobranca, os planos, e a implementação que limita.

O grupo NÃO foi junto, embora exista por causa da cobrança: organizacao.grupo_id é obrigatório, e agrupar equipes é útil num portal que não cobra de ninguém.

O que falta, e por que está parado. O recebedor de webhook do Paddle não existe. Ele é a parte fácil — o esquema já tem a chave que torna o processamento idempotente e o carimbo que ordena eventos fora de ordem —, mas não pode ser exercitado sem uma conta.

E a conta depende do portal estar no ar: o Paddle é merchant of record, então ele revisa o site antes de aceitar um vendedor. Sem site, sem preço publicado e sem termos, a inscrição não passa. Construir o webhook antes seria escrever contra documentação sem poder rodar — que é exatamente o que produziu os geradores de CI não validados.

Decidido em 2026-08-11: portal primeiro, webhook depois.