Xeonr Developer Docs

Environments

The image a workload runs — registered from an existing repo, or built from a spec and content-addressed.

An environment is deps and toolchain: an immutable, digest-pinned OCI image plus the runtime knobs a workload needs. Jobs, sandboxes and app versions all reference one by environment_id.

Environments are namespace-scoped. Two namespaces registering the same image get two environment records.

Registered (static) environments

Register an image that already exists. This is the path for pipeline runner images, codegen plugins, and any toolchain you build in CI already.

const environment = await cp.environments.register(
  "registry.prod.wtf/xeonr/my-app/runner:1.42",
  { label: "runner 1.42", defaultArgv: ["/usr/local/bin/runner"] },
);

register always creates, which is deliberate — two records for one image is a legitimate thing to want. For an image CI rebuilds, use cp.environments.ensure(ref): it looks before registering, caches the result, and single-flights concurrent callers, so two dispatches racing after a deploy do not each mint one. It requires a digest-pinned ref, because a cache keyed on a moving tag keeps running the old image.

A tag is resolved to a digest at registration, using the platform's own pull credentials plus any image_pull_secrets the namespace declares — and the digest is what pods run. A tag that later moves does not change what an existing environment runs. You may also pass repo@sha256:... directly.

Registration is idempotent: registering the same image in the same namespace returns the existing record.

FieldPurpose
imagerepo:tag (resolved) or repo@sha256:...
default_argvUsed when a run omits argv; otherwise the image entrypoint
runtimeGVISOR (default), RUNC, KATA
securityRelax uid / rootfs — see below
parameters / outputsDeclared input and output schema for the environment
labelDisplay name

Private registries

Pull credentials are per-namespace, not global: a namespace declares the names of dockerconfigjson secrets held by the platform, and the controller copies only those into that namespace's runner namespace.

await cp.raw.namespace.updateNamespace({
  namespace: "my-app",
  imagePullSecrets: ["my-app-registry"],
});

Built environments

BuildEnvironment builds an image from a structured spec and pushes it to registry.containers.xeonr.io. It is a server stream: log frames while it builds, then a terminal done or failure.

const built = await cp.environments.build({
  label: "python + deps",
  baseImage: "docker.io/library/python@sha256:...",
  steps: [["pip", "install", "requests", "pandas"]],
  cacheInputs: { "requirements.txt": lockfileBytes },
}, {
  onLog: (line) => process.stdout.write(line),
});

console.log(built.environmentId);

A build that fails throws with the builder's own message — it is the author's problem, not an outage.

Content addressing. The spec hashes to env_key = sha256(base digest ‖ canonical steps ‖ cache-input hashes). An identical spec is an identical key, so a repeat build is a cache hit that returns in milliseconds instead of rebuilding. This is why cache_inputs matters: feed it your lockfiles so a dependency change invalidates the image and a code change doesn't.

Base images must end up digest-pinned. A tag is resolved for you when tag resolution is configured; otherwise pass a digest. The pin is not convenience — the base digest is hashed into env_key, so an unpinned base would mean one key naming two different images.

shared_cache (cross-namespace dedupe) defaults to false and should stay there unless you have a reason: a shared cache across tenants is a supply-chain channel.

Multi-stage builds from a workspace

For building your own source, set stages and a context_snapshot_id — an immutable workspace snapshot that becomes the build context. The last stage is the runtime image; earlier stages are COPY --from sources.

import { copy, run } from "@xeonr/containers";

await cp.environments.build({
  contextSnapshot: snapshot.id,
  stages: [
    {
      name: "build",
      baseImage: "docker.io/library/golang@sha256:...",
      workdir: "/src",
      ops: [
        copy({ src: ["go.mod", "go.sum"], dst: "." }),
        run("go", "mod", "download"),
        copy({ src: ["."], dst: "." }),
        run("go", "build", "-o", "/out/app", "./cmd/app"),
      ],
    },
    {
      baseImage: "gcr.io/distroless/static@sha256:...",
      ops: [copy({ from: "build", src: ["/out/app"], dst: "/app" })],
      entrypoint: ["/app"],
    },
  ],
});

ops is a single ordered list of copy and run, deliberately: interleaving "copy the manifest → install deps → copy the source → build" is what keeps the dependency install a cache hit when only application code changed. That ordering is unexpressible if all copies must precede all runs.

The platform authors the Dockerfile — callers never submit Dockerfile text, and RUN is always exec-form, never shell-joined. Builds run on the shared BuildKit daemon, confined by a NetworkPolicy that permits outbound HTTP(S) only and no in-cluster access. Because the builder is shared, it is for platform and preset-shaped installs; a namespace's own hostile-by-assumption install argv belongs in a sandbox, not the builder.

Extending a built environment

Environment.build_spec persists the recipe verbatim, so a built environment can be shown, cloned and edited, or rebuilt with its own image as the new base. Registered static environments have no build_spec — its presence is what marks an environment as built.

Relaxing the default posture

The default is locked down: uid 1000, read-only root filesystem, all capabilities dropped, seccomp on. ContainerSecurity relaxes exactly two things:

security: { runAsRoot: true, writableRootfs: true }
FieldEffect
run_as_rootRun as uid 0. Only under GVISOR or KATA — rejected on RUNC, where root sits on the shared host kernel
writable_rootfsMount / read-write. Ephemeral; only /workspace persists

Capabilities stay dropped and seccomp stays on regardless.

Lifecycle

CallNotes
GetEnvironment / ListEnvironmentsNamespace-filtered, paginated
PinEnvironmentProtects from garbage collection
DeleteEnvironmentRemoves the record

On this page