Skip to content

Open questions

Postponed decisions with the reasoning preserved, so they are not re-argued from scratch — and known limitations that still have no solution.


The question. When an implementation offers more than the contract describes, what happens?

Discarded: extra-args. v0 had a free-form bag alongside the arguments. It is not typed, not validated, does not appear in the contract — swapping implementation can break the pipeline silently. It loses substitutability and verifiability at once.

Chosen direction: an extension publishable by third parties, which is, precisely, multiple inheritance limited to one level:

apiVersion: oren.sh/v1
kind: TaskExtension
metadata:
name: signing
namespace: security-co # another organisation, asking nothing of the contract's owner
spec:
extends: techlite/build-docker-image@^1.0.0
inputs:
signingKey: { type: string, secret: true }
outputs:
signature: { type: string }

Two restrictions make the difference:

  • Depth 1. An extension cannot be extended.
  • Additions only. It removes no output and restricts no input, so every implementation keeps satisfying the base contract.

With that, a task takes N extensions instead of generating 2ⁿ contracts, and org B extends org A’s contract without permission.

The argument that does NOT decide it. Substitutability (“everything true of the child is true of the parent”) holds in both models. Under unrestricted inheritance, an implementation of the child satisfies the parent; with an extension, the implementation is of the base contract and substitutability is trivial. What decides is governance: an open registry requires decentralised extension, and limited depth avoids the combinatorial explosion.

Why postpone. The eleven contracts written from a real pipeline needed no extension at all — the whole of extra-args dissolved into decomposition and first-class inputs. Building the mechanism before a real case is guessing at its shape.

Trigger to revisit. A case that the “if I swap implementation and this value disappears, does the output change?” test does not classify.


The itch. A release pipeline has five steps: analyse, version, build, publish, tag. Repeated across several projects, it invites grouping.

Two forms were considered and rejected.

The implementation runs Oren inside itself. That brings dagger-in-dagger back: it would require engine/docker, critical privilege — exactly what was eliminated from the whole image chain.

A declarative composed task — a contract that groups others. The problem is subtler and more serious: it hides exactly what the project exists to make visible. A release-node would declare “I need gcpCredential, gitToken and a mutable directory”, and which part needs what disappears. Consent becomes all-or-nothing, cache becomes all-or-nothing, effects becomes the worst case of every step, and a failure reports “the composition failed”.

The chosen direction: a pipeline template. Reusable steps that the CLI expands at resolution time, rather than encapsulating at run time:

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

That becomes four real steps before any execution. From there everything works as it already works — granular consent, per-step cache, per-step effects, per-step visible privilege, and the failure points at the real step.

The distinction that decides it. Composed, the abstraction lives at run time and erases information. Expanded, it lives only in the writing, and the result is identical to having typed the steps by hand.

When a composed task would be legitimate. If the grouping is a real abstraction rather than shorthand — something like deploy-blue-green, which conceptually is one operation. Then opacity is the product, and declaring the union of dependencies is the price accepted for it.

The test: does the user think of this as one thing, or as N things they always do together? If the second, it is a template.

Trigger to revisit. The same set of steps repeated in three or more projects — then the template is worth it. An operation nobody thinks of as a sequence — then the composed task is worth discussing.

The type exists in the registry, but the execution engine gives every container network access and exposes no per-run control. A worker that omits network still reaches the internet.

It is the only gap in the core — every other dependency is granted by materialising something, and omitting them really does break the worker. The registry marks it enforced: false, so that the difference between declared and guaranteed lives in the data and not only in the documentation.

Until that changes, network serves for the portal to display and compare, not as a guarantee.


The spec allows pointing at an OCI image directly, with the contract read from the sh.oren.* labels. The CLI still refuses, with a message explaining why.

It is what would make the image self-describing and allow validating without a portal. It depends on settling the label convention.


--resume exists, and in CI it depends on the platform cache. See Running in CI.

What remains open — the cache inside the trust boundary. The restored state decides which steps are skipped and with what output, and a CI project’s cache is usually writable by any job in it. The recorded revision stops resuming across commits, but does not stop another job from writing the cache for this commit.

Closing that requires signing the state — a signing key in CI, and whoever holds it. That is another project, and it is why --resume is optional and the generated file carries the warning.

Recorded restrictions: steps producing a secret output are not resumable, because the value is not persisted. And steps that write to the project ROOT block the cache, because caching the root would restore .oren/registry/ and .oren/consent.json from another run.


jq required in native mode. When an input comes from another step, the job uses jq to compose input.json. The worker image needs it. In oren mode this does not apply: the CLI composes it.

Validation per platform. GitLab has its output checked against the official schema, and Cloud Build in --mode oren ran end to end on a real project — build, push and deploy. GitHub Actions and Cloud Build’s --mode native still have no real run.

Running for real found three defects no amount of reading documentation would have caught: a mutable directory that did not exist in a clean copy, a refspec under detached HEAD, and git credentials inherited from the CI beating the worker’s own. That is the reason not to write a generator for a platform nobody will exercise.

properties is not supported in native mode. Property files are resolved before the run and do not exist on the runner. In oren mode they work: the CLI reads them, inside the job.


Jenkins, Azure DevOps and Bitbucket have no generator, and the 2026-08-10 decision was to put it on hold.

oren run already works on all three: in oren mode the file is thin — a job running the Oren image with a Docker daemon, secrets as variables and the repository on disk.

What argues against writing generators: each one is permanent debt, and without someone actually running it, it is a promise that breaks on someone else’s machine.

Azure DevOps is the easiest candidate: YAML shaped like GitHub’s, native Cache@2 (so --resume would work).

Bitbucket has the best conceptual fit — it already has per-branch sections, which is exactly the branches → propertyFile map. But its Docker service gets 1 GB by default with a ceiling around 3 GB, and increasing the step size does not increase the service’s memory. Whether the engine fits there is a question to measure before any code.

Jenkins is the most expensive: Groovy instead of YAML, no single format, and no native cache between builds.

The highest-return path is documenting the contract a CI has to satisfy, and letting whoever uses it write the few lines for their platform.


Decided base. A model close to npm’s: users, organisations, and members with roles. A namespace belongs to the organisation.

Suggested adjustment: everything is an organisation. Creating an account automatically creates an organisation of the same name. It avoids two kinds of owner — user and org — with distinct rules, which is where npm accumulated complexity. A solo user publishes without thinking about orgs; when they need a team, they create another and the model is the same.

Roles.

Role May
viewer see the organisation’s private content
publisher publish new versions and deprecate
admin manage members, approve implementations, transfer the namespace

publisher carries permanent weight. A published version is immutable and there is no deletion — granting the role is granting irreversible power over the namespace. It is worth the interface saying so when inviting, and perhaps an extra confirmation on the first publication of a namespace, which is when the name gets set in stone.

Deprecating belongs to publisher. With no deletion, deprecation is the only correction mechanism; whoever publishes needs to be able to correct without going through an admin.

viewer only makes sense with private content. In an open catalogue everything is public. That is not a problem — it means the role exists for the paid plan.

A namespace needs an explicit owner and a transfer route. Since nothing is deletable, an abandoned namespace stays abandoned forever — the problem npm harvested with orphaned packages. Having a named owner (not merely “whoever has the admin role”) and a transfer path is cheap now and expensive later.

Approving implementations belongs to admin. The list of what the organisation trusts is governance, not publishing.

A possible refinement, not now. Separating “publish a contract” from “publish an implementation” into distinct roles — changing a contract affects third parties, publishing an implementation affects fewer. Defensible, but optimisation without a real case.

The product decision. The portal is self-hostable and free; charging is for use of the public instance. Consistent with the rest: self-hosting gives control, the public instance gives the network — and charging for the network is more honest than withholding functionality.

The implication that is still unresolved. A task is referenced as techlite/analyze-commits@^1.0.0 — namespace, name and range, with no origin. In a single installation that is enough. With several instances, techlite/build-docker-image could be one contract in one and another elsewhere, and the same oren.yaml would produce different builds depending on who runs it.

Likely direction, following npm’s path. Separate the three things:

  • The reference stays namespace/name@range — readable and portable.
  • The origin is environment configuration, not pipeline configuration (the analogue of .npmrc).
  • The lockfile records where each task actually came from.

The lockfile already has the right place for it: today it stores task as namespace/name@version, and would also store the resolved origin. A project switching instances sees the diff.

What NOT to do. Putting the host in the oren.yaml reference (portal.acme.com/techlite/build-docker-image). That ties the pipeline to an instance and breaks the portability that name references exist to give.

Accepted consequence. Each self-host is an island: reuse between organisations only happens within one instance. Federation between instances would be the alternative, and it is much more machinery. It is the same trade-off already accepted for private contracts — you trade network for control.

Trigger. Before the portal accepts third-party publishing. After that, changing the reference shape breaks everyone’s oren.yaml.

An idea on record: the portal exposes an MCP that returns an organisation’s contracts and implementations, for a model to assemble the oren.yaml. Scope defined: read-only, local execution — the model suggests, the user runs.

What already favours it: contracts are JSON Schema, which is the format models consume for tool use; validate is a deterministic checker that runs in a second; and consent stays with the user, in their terminal.

A caution on record: contract descriptions come from third-party organisations. They are data, not instructions.


The idea. The CLI reports progress to the portal, and you follow pipelines in one place — instead of opening GitLab’s interface for some and Cloud Build’s for others.

Why it is not just another dashboard. A CI shows “job passed in 4s”. Oren knows which task, which implementation, which digest ran, which privilege was granted, what came from the cache and what actually ran. No CI has that information because none knows what a task is — and it is exactly what you want to look at when something goes wrong. It is a natural consequence of the pipeline not belonging to the platform: if it runs in three places, looking at three interfaces is the price this view eliminates.

The obvious problem. It requires network access in CI.

The bigger, less obvious problem. Exactly what leaves the machine. Outputs contain user data — image names, versions, paths, counts. “Not secret” does not mean “may be exfiltrated”. A CI tool that phones home by default is blocked in many organisations, and rightly so.

A design that solves both:

  • Metadata, never content. Step, duration, result, task and version, digest, granted privilege, cache hit. Never input or output values.
  • Non-blocking. A telemetry failure does not fail the pipeline. A portal that is down does not stop a build.
  • Explicit opt-in. Never the default.
  • Self-hostable. An organisation that wants the view without sending anything outside points at its own collector.

What already exists. .oren/runs/<id>/ records a manifest and per-step status, with exactly those fields. What is missing is the transport and the decision of what leaves — not the data model.

Trigger. After the portal, and after the catalogue view is standing. Before that there is nowhere to display it.

The node-build and npm-publish tasks exist and were tested against a local registry. What is missing is pointing at npmjs with a real credential and having Oren publish Oren.

Chicken and egg solved: the first publication of each package is manual; from the second on, the pipeline takes over.

A person has the right to delete their data, and an organisation to close its account. That needs to exist. What is not decided is what happens to what has already been published.

The natural proposal — marking the artefacts as orphans and removing them after a grace period — collides head-on with once public, always public. Removing is the same break as making it private, only delayed: a third party’s pipeline works today and fails in ninety days, without anyone having touched it.

The precedent is well known. In 2016 an eleven-line package was removed from npm and took down builds worldwide; the policy changed shortly after, and today npm, crates.io and Maven Central do not remove published versions. None of them treats this as an implementation detail — it is a registry’s central promise.

Separate two things the proposal merges:

Personal data is deletable, and should be. Name, e-mail, handle, tokens, session history. That is what GDPR and the Brazilian LGPD reach, and there is no reason to retain it.

A published artefact stays, with authorship anonymised. The documento is not personal data: it is a contract published for third-party consumption, and whoever consumes it has a legitimate expectation that it keeps existing. publicada_por becomes null; the content remains.

That satisfies the right to erasure without transferring the cost to whoever chose nothing.

What survives of the original proposal, and is good

Section titled “What survives of the original proposal, and is good”

Not listing it. An orphaned artefact leaving the listing and the search — appearing only behind an explicit filter — discourages new adoption without breaking existing adoption. It is the difference between “we do not recommend it” and “it no longer exists”, and only the second breaks somebody.

Mandatory deprecation. On closing, everything the organisation published is marked deprecated, with a reason. Whoever resolves the version gets the warning, and whoever is choosing a task sees that it no longer has an owner.

Transfer. An organisation that is closing can pass its contracts to another that takes them on. It is the best outcome, and worth offering before the other options.

  • Is the orphan organisation real (a row in organizacao) or a state of the contract? A real organisation keeps the namespace alive — and the namespace is what appears in the references, so it needs to keep resolving.
  • Can a released namespace be registered by someone else? Almost certainly not: whoever took acme would be able to publish new versions of acme/build, and pipelines using ^1.0.0 would receive them. That is dependency hijacking, and npm has already lived it.
  • Retention of personal data in backups, and the real deadline for it to leave them.

Billing: the starter, and why the webhook waits

Section titled “Billing: the starter, and why the webhook waits”

Billing is not in the community jar. It is a separate Spring starter (portal-cobranca) that the public portal adds as a dependency.

The practical reason comes before the commercial one: a quota rule in open code is a removable rule. In the community jar it would protect nothing — it would only document what someone needs to delete. And a self-hosted portal has nobody to charge: the purchase already happened, by contract, before the install.

What stayed on each side:

  • communitygrupo, grupo_admin, organizacao.grupo_id, the count of private artefacts, and the LimiteDePublicacao port whose default does nothing. Not “unlimited”: the concept of a limit does not exist there;
  • starterassinatura, evento_de_cobranca, the plans, and the implementation that enforces them.

The group did NOT go with it, even though it exists because of billing: organizacao.grupo_id is mandatory, and grouping teams is useful in a portal that charges nobody.

What is missing, and why it is parked. The Paddle webhook receiver does not exist. It is the easy part — the schema already has the key that makes handling idempotent and the timestamp that orders out-of-order events — but it cannot be exercised without an account.

And the account depends on the portal being live: Paddle is a merchant of record, so it reviews the website before accepting a seller. With no site, no published pricing and no terms, the application does not pass. Building the webhook first would mean writing against documentation with no way to run it — exactly what produced the unvalidated CI generators.

Decided on 2026-08-11: portal first, webhook after.