SDK — Publishing

Publishing a biome to the Xema Store is a controlled lifecycle transition, not a npm publish. Every transition is a capability call, every submission runs in the store-review execution environment before approval, and the published version is immutable.

This page covers the publisher-side flow: from a working biome in the org-installed lifecycle to a store-approved listing other orgs can find in the catalogue and install through biome lifecycle.


The four publishing transitions

From → ToCapabilityWho calls it
org-installed → review-requiredbiome:promote@1Biome owner (org admin)
review-required → store-submittedbiome:submit-to-store@1 (≡ store:biome.submit@1)Biome owner
store-submitted → store-approvedbiome:approve-in-store@1 (≡ store:biome.approve@1)Store reviewer
store-approved → archivedbiome:archive@1 (≡ store:biome.archive@1)Biome owner or reviewer

Every transition is mediated by xema-capability-router and stamped to audit-log-api. The biome owner cannot self-approve — biome:approve-in-store@1 requires a reviewer subject distinct from the submitting subject.


Step 1 — promote out of org-installed

When you are ready to start the publishing flow, promote the installation to review-required:

The xema CLI has no promote verb; invoke the biome:promote@1 capability with xema run, passing the installation as input:

bash
1xema run biome:promote@1 --input @promote.json

The promotion call freezes the current draft into a published version (semver-tagged) and writes the manifest + bundle + permission digest into the review-required state. From this point the biome runs in sandbox + store-review and is no longer reachable from production org subjects.


Step 2 — submit to the Store

Once the review-required state holds the version you want to publish, submit:

bash
1xema biome submit <installation-ref> --as <listing-id>

Submission writes a StoreListingVersion row with lifecycle store-submitted via POST /listings/:id/versions on xema-store-api — a version is always addressed under its listing, never through a separate top-level submission route. The submission carries:

  • the immutable manifest;
  • the bundle, as a signed OCI artifact (see Bundle format below);
  • the computed permission digest (capabilities + risk tier + data-access summary + diff against the previously approved version, if any);
  • the publisher's signing identity.

From submission onward, the version runs only in store-review — no production data, no production credentials, no external network beyond what the environment allows.


Step 3 — review

The Store reviewer interacts with the submission through three endpoints on xema-store-api:

EndpointPurpose
POST /listings/:id/versions/:version/commentStructured comment, recorded in the ReviewLog
POST /listings/:id/versions/:version/approveFlip to store-approved; the version becomes installable by any org
POST /listings/:id/versions/:version/rejectTerminal rejection; the publisher must submit a new version

GET /listings/:id/versions/:version/review-log is the audit trail. Comments are not free-form admin notes — every comment is structurally addressable so the publisher can read and respond.

Approval is the publisher's signal that the version is ready for general install. There is no "soft-launch" or "preview" intermediate state; the version is either store-submitted, store-approved, archived, or rejected.


Step 4 — installing

The Store does not install. Once approved, the version appears in the catalogue and an org installs it through biome lifecycle: the host plans the install, the admin reviews the plan (permission digest, dependencies), and the host applies exactly what was planned.


Bundle format — OCI artifacts

Biomes ship as OCI artifacts, not npm packages and not bare .tgz files. The same container-registry substrate that holds Docker images stores biome artifacts; the same cosign toolchain that signs container images signs biomes.

Layer layout — one layer per file

A biome bundle is a single OCI artifact whose artifactType is:

application/vnd.xema.biome.manifest.v1+json

Every file in the biome tree is pushed as its own layer. Each layer carries an org.opencontainers.image.title annotation holding that file's biome-root-relative POSIX path — xema-biome.json, skills/code-review/SKILL.md, api/acme-api/dist/main.js, and so on.

That annotation is load-bearing, not decoration. biome-fetcher-api unpacks a bundle by running oras pull --output <dir> <ref>, and oras materialises each layer at <dir>/<org.opencontainers.image.title> — the annotation is the only thing that reconstructs the directory tree. A layer without it is silently skipped by oras (Skipped pulling layers without file name in "org.opencontainers.image.title"), so an artifact built from opaque tarball layers pulls successfully, writes nothing, and installs an empty bundle. The fetcher then re-tars the reconstructed tree and requires a top-level xema-biome.json in it.

Two annotations are stamped on the artifact itself: org.opencontainers.image.title = the biome id, org.opencontainers.image.version = the manifest version.

What ships

Everything under the biome root except node_modules/ and .git/. There is no include-list and no per-directory layer split — manifest, source, compiled output, contributions, skills and assets all travel as ordinary files. Layer order is the sorted relative path, so the same tree produces the same layer sequence on every publish.

Registry authorization and repository namespaces

Pushing and pulling is authorized by the Xema container registry service, which implements the OCI Distribution token flow. It issues tokens and records an audit entry for each decision; it never carries image or artifact bytes. Your client talks to the registry directly, and the registry checks the token on every request.

  1. The client calls the registry and is told to get a token from the Xema token endpoint, GET /v2/token, with the repository and actions it wants (pull, push, delete).
  2. The token endpoint verifies your Xema access token and returns a signed bearer token valid for 5 minutes, scoped to the access it granted. A request that asks for nothing is a login probe and gets an empty-access token.
  3. The client presents the bearer token to the registry for each pull or push, and asks again after it expires.

The repository path decides who may use it:

Repository pathWho can pullWho can push
<org-slug>/<name>Callers whose organization is that orgCallers whose organization is that org
system-shareable/<name>Anyone, including anonymous callersXema platform only
system/<name>Xema platform onlyXema platform only

Org repositories always have the org slug as their first segment; a single-segment name is rejected, so one org cannot reach another's namespace. The token endpoint returns exactly the actions it granted. It never silently trims a request: if nothing asked for is granted, the call is refused. When you publish to the Xema registry, put your org slug in the --registry value (for example <host>/<org-slug>), so the reference <registry>/<biome-id>:<tag> falls under your org namespace.

The same service also bakes custom workspace images for an org. An org admin creates a variant with a slug, a runtime kind, a base image and a manifest, and the service builds it asynchronously. A variant moves through draft, building, ready, failed and archived; re-creating an identical ready variant changes nothing, a failed or stale bake can be re-run, and archiving removes a variant from selection while keeping its history. One variant per runtime kind can be the org default. Baking and editing variants needs the workspace:image.bake@1 capability, listing needs workspace:image.use@1, and archiving needs workspace:image.publish@1.

Publishing tool

xema biome publish <path>, from the @xemahq/xema CLI. It resolves the target and the file set, runs oras push, then cosign sign — every prerequisite is fail-fast, nothing is skipped silently.

bash
1xema biome publish ./ --registry ghcr.io/acme --token-env GHCR_TOKEN
FlagEffect
--registry <host>Target OCI registry host, optionally with a namespace path. Overrides a configured oci source.
--source <name>Name of a configured oci source in xema.config.yaml to take the registry + token from. Required to disambiguate when several oci sources exist.
--token-env <name>Env-var NAME holding the registry token (used with --registry).
--username <user>Registry username paired with the token. Default: token.
--tag <tag>Tag to publish under. Default: the manifest version.
--config <path>Explicit xema.config.yaml path, instead of the standard search order.
--dry-runPrint the resolved plan and the exact oras / cosign commands without touching the registry.

The published reference is <registry>/<biome-id>:<tag>. The registry token is always referenced by env-var name — never inlined in config, never placed on the command line. The CLI resolves the name and hands the value to oras over --password-stdin. With no --token-env and no authTokenEnv on the configured oci source, oras uses ambient credentials; run oras login first.

Signing — keyed or keyless cosign

The publisher signs with a key (COSIGN_KEY) or keylessly (--keyless, Fulcio/OIDC from the CI's ambient token).

Keyless needs the run's own signer stated explicitly: --certificate-identity https://github.com/<job_workflow_ref> (the workflow file that actually runs, which is the reusable workflow when there is one) and --certificate-oidc-issuer https://token.actions.githubusercontent.com. Both flags are required with --keyless and refused without it.

A publish owes its own signer's signature and attestation. When the tag already holds the same content, the publisher checks with cosign verify / cosign verify-attestation against this run's signer: a signature or attestation by another identity (say, a develop-branch run before the main-branch run) does not count. Whatever is owed is added on the existing digest, without pushing again. In key mode that check needs COSIGN_KEY whenever the tag exists, dry runs included. Cosign must be logged in to the registry.

COSIGN_KEY holds the cosign private-key ref — the same env var the distribution tooling uses. It is read from the environment and never passed on argv. If COSIGN_KEY is unset, the publish fails before pushing anything; there is no unsigned mode and no override flag.

bash
1COSIGN_KEY=cosign.key xema biome publish ./ --registry ghcr.io/acme

The command run is cosign sign --yes --key <ref> <ociRef> — a registry signature on the pushed ref, which is what the fetcher's cosign verify looks for. A detached cosign sign-blob signature would not be found.

Verification is the broader of the two: BIOME_TRUSTED_PUBLISHERS accepts keyless certificate identities or a key:<path> public key, so an artifact signed keylessly by any CI pipeline, the Xema CLI's --keyless included, installs when its identity is allowlisted.

Provenance — SLSA v1.0

biome-fetcher-api requires a SLSA v1.0 in-toto attestation on the artifact by default: BIOME_REQUIRE_PROVENANCE defaults to "true". It is verified with cosign verify-attestation --type https://slsa.dev/provenance/v1 against the same trust anchors as the signature, and an artifact without one is refused with BIOME_PROVENANCE_MISSING / 403.

xema biome publish attaches it for you, as the third and final step of the publish — there is no separate command and no flag to skip it. It generates the predicate, writes it to a private temp file, and runs:

bash
1cosign attest --yes --key "$COSIGN_KEY" \
2  --type https://slsa.dev/provenance/v1 \
3  ghcr.io/acme/<biome-id>:<tag> \
4  --predicate <temp>/slsa-provenance.json

The predicate is SLSA v1.0. buildDefinition.resolvedDependencies carries one entry per published file with its sha256 — one per pushed layer, so the attestation covers the complete build input rather than a subset. runDetails.builder.id is resolved in this order: XEMA_BIOME_BUILDER_ID if set, else the GitHub Actions GITHUB_WORKFLOW_REF, else local-developer://<user>@<host>. There is no unset case, so provenance is never silently omitted.

A deployment that sets BIOME_REQUIRE_PROVENANCE=false accepts a signature-only artifact — the signature is still mandatory.

Install-time enforcement

biome-fetcher-api verifies signature + provenance before any bytes are unpacked. Failure modes:

VerdictError codeHTTP status
Signature missingBIOME_SIGNATURE_MISSING403
Signature invalidBIOME_SIGNATURE_INVALID403
Identity not in BIOME_TRUSTED_PUBLISHERSBIOME_SIGNATURE_IDENTITY_NOT_TRUSTED403
SLSA provenance missingBIOME_PROVENANCE_MISSING403
SLSA provenance invalidBIOME_PROVENANCE_INVALID403
cosign binary absent in containerBIOME_COSIGN_UNAVAILABLE500

See the biome supply chain page for the trusted-publisher allowlist runbook and the fetcher's trust configuration.


What does not change a published version

A published version is immutable. The following actions do not edit the version:

  • Comment on a submission (writes a ReviewLog row, not a manifest change).
  • Update the listing's branding metadata (PATCH /listings/:id updates listing-level fields, not version-level).
  • Archive the listing (hides from new installs but keeps existing installs functional; their lockfile pins the version).

To ship a fix, publish a new version. The user-controlled versioning model is the same one that governs every other Xema object — there is no "edit in place" mode for published biomes.


Lockfile interaction

When a version is approved, xema-store-api derives a canonical lockfile entry for the bundle. Org-side installs reuse the canonical lockfile fragment in their own composite lockfile, so a biome that is store-approved at version 1.4.2 resolves to byte-identical pinned dependencies in every org that installs it.

This is what makes "the same biome in two orgs" mean exactly the same thing — versioning + lockfile + capability digest.



Previous: ← Storage Next: UI I contribute →