Skip to content

ArenaProject CRD

An ArenaProject is a git repository of Arena content: the thing you run. The repository is either hosted on the workspace content volume or an external https remote. An ArenaJob’s spec.sourceRef names the project, and each run resolves the job’s spec.ref to a commit of this repository and executes exactly that commit.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ArenaProject

Scope: namespaced. Short name: ap (e.g. kubectl get ap).

FieldTypeRequiredDescription
repositoryArenaProjectRepositoryYesWhere the project’s commits live.
deployArenaProjectDeployNoWhat may be deployed from the project.

Exactly one of hosted or external must be set (enforced by CEL validation). The kind cannot change after the project is created: an update that switches hosted to external or back is refused, because an external project’s fetch would prune a hosted project’s history from the same repository. To change it, delete the project and create it again.

FieldTypeDescription
hostedHostedRepositoryA bare repository at arena/repos/<project>.git on the workspace content volume.
externalExternalRepositoryAn https git remote.

A hosted repository has one branch, main. Only the arena controller and the operator read or write it, and it is not served over the network.

FieldTypeRequiredDescription
seed.configMap.namestringYes, when seed is setA ConfigMap in the project’s namespace whose keys become the repository’s first commit on main. __ in a key stands for /, so the key prompts__greeting.yaml is the file prompts/greeting.yaml.

The controller applies a seed once, when the repository has no commits. After the first commit it ignores the seed: editing or deleting the ConfigMap does not change the repository. A repository that already has commits is never seeded, and the Seeded condition reports reason RepositoryNotEmpty. Such a project never waits for the seed’s ConfigMap: it is Ready whether or not the ConfigMap exists.

FieldTypeRequiredDefaultDescription
urlstringYesThe remote. Must start with https:// (CEL). The operator dials only this URL, never one read from a repository’s own configuration.
defaultBranchstringNomainThe branch a run or deploy uses when it names no ref.
readSecretRef.namestringNoA Secret in the project’s namespace that runs, deploys and the controller’s polls fetch the repository with. It holds username and password, or a single token, and must be labelled omnia.altairalabs.ai/credential-for: git; an unlabelled Secret is refused, so a project cannot send any other Secret to the host its url names. A public repository needs none. Your own workspace never uses it: see Workspaces.
pollIntervalstringNo5mHow often the controller fetches the remote, as a Go duration (30s, 5m, 1h). Intervals under one minute are raised to one minute.
caBundleRefCABundleRefNoPEM certificates the remote’s certificate is also trusted by, for a host behind a private certificate authority.

The controller mirrors the remote’s branches and tags into arena/repos/<project>.git on every poll, so runs and deploys of an external project read the same local object store as a hosted one. A run or deploy also fetches when it starts. A hosted repository is never polled: it changes only through Omnia.

Names one key of a ConfigMap or a Secret in the project’s namespace that holds one or more PEM certificates. Exactly one of the two must be set (CEL), and each needs both name and key. There is no namespace field: the bundle is always read from the project’s own namespace.

FieldTypeDescription
configMapKeyRef.namestringA ConfigMap in the project’s namespace. A CA certificate is not secret, so this is the usual choice.
configMapKeyRef.keystringThe key in its data (or binaryData) that holds the PEM.
secretKeyRef.namestringA Secret in the project’s namespace, for a bundle kept in one, such as a cert-manager CA Secret. Unlike readSecretRef, it needs no label: the bundle only verifies the host and is never sent to it.
secretKeyRef.keystringThe key in its data that holds the PEM, for example ca.crt.

The certificates are added to the system roots, not used in their place, so a host with a public certificate still verifies. Every dial of the remote uses them: your own clone, pull, push and branch fetches through the operator, the controller’s poll, and the fetch a run or deploy makes. The bundle is read each time the remote is dialled, so a change to the ConfigMap or Secret takes effect on the next fetch with no restart. A change to it also wakes the controller, which fetches again straight away.

A bundle that cannot be used fails the fetch, and the remote is not dialled with the system roots alone instead. That happens when the ConfigMap, Secret or key does not exist, or the value holds no PEM CERTIFICATE block that parses. The project’s Ready condition is False with reason CABundleInvalid and a message naming the object and key (never their contents), and a source-control verb answers 409 with the same message.

FieldTypeDescription
allowedRefs[]stringGlobs over full ref names (refs/heads/main, refs/tags/**) that a deploy may name. Defaults to refs/heads/<defaultBranch> and refs/tags/**. A commit SHA is allowed only when it is listed literally. External repositories only (CEL): a hosted project deploys any commit on main.

A deploy of a ref no glob matches is refused with 403, and the response lists the globs in force as allowedRefs, for example {"error": "ref may not be deployed: refs/heads/feature/tone is not one of refs/heads/main, refs/tags/**", "allowedRefs": ["refs/heads/main", "refs/tags/**"]}. An evaluation run is not limited by allowedRefs: it may use any ref the repository holds.

A glob is matched one /-separated segment at a time:

  • A segment that is exactly ** matches zero or more whole segments, so refs/tags/** matches refs/tags/v1 and refs/tags/release/v1, and refs/**/v1 matches refs/tags/v1 and refs/tags/release/v1.
  • Every other segment is a Go path.Match pattern, so *, ? and [...] match within one segment: refs/tags/* matches refs/tags/v1 and not refs/tags/release/v1.

A malformed glob (an unclosed [, a ** that is not a whole segment such as refs/a**b, or an empty segment) is refused at admission, naming the offending entry, when the chart’s webhook.enabled is on. Without the webhook it is admitted and matches nothing, so it refuses deploys rather than widening them.

A run’s or deploy’s ref is one of: a full 40-character commit SHA the repository holds, a full ref name (refs/heads/…, refs/tags/…), a branch name, or a tag name. An annotated tag resolves to the commit it tags. Abbreviated SHAs and revision expressions (main~1) are not accepted.

A ref that names nothing in the repository fails: a run started through the API is refused with 409, and an ArenaRun whose job names such a ref fails with reason RefNotFound.

Each user edits a project in their own working clone of its repository, at arena/workspaces/<project>/<user> on the workspace content volume. The dashboard and the content API name a project’s files as arena/projects/<project>/…, and every such path resolves to the caller’s own clone, which is created from the repository the first time the user opens the project. Nothing a client sends can name another user’s clone, the repository itself, or a clone’s .git directory.

Edits in a workspace are private to that user until they are committed and pushed. A run executes a commit of the repository, never a workspace, so a change reaches a run only once it is pushed.

The project editor’s source-control panel, and the operator routes behind it, offer a fixed set of git verbs on the caller’s own workspace: status, diff, log, commit, discard, pull and push, plus Sync (pull, then push) for a hosted project. Commits are authored as the signed-in user. Pull is fast-forward only: when your branch and main have both moved, Omnia does not merge; reconcile in the builder’s sandbox (which has real git over the same workspace) or in a local clone. A push to a hosted repository goes to main only, is refused if it carries a commit someone else authored, and is recorded in the operator’s audit log. Anything beyond these verbs (merge, rebase, stash, reset) happens in a sandbox or locally.

An external project’s workspace works like a desktop client’s clone:

  • Your own account on the host. Your workspace is cloned from the remote the first time you open the project, and every pull and push reaches the remote, with your own credential from the workspace’s gitHost Connection for the host of url. With no account linked, the panel says “link your <host> account” and points to Connections. The operator dials only url, never a remote or insteadOf written into the workspace’s .git/config.
  • Branches. You see the local and remote branches, create one from the current branch, and switch to any of them. A switch with uncommitted changes is refused: commit or discard them first.
  • Push sends the checked-out branch to the branch of the same name at url. A push the remote branch has moved past is refused; pull first. The host decides whether you may push, and the push is recorded in the operator’s audit log.
  • Pull requests happen on the host. For a pushed branch on GitHub or GitLab, the panel links to the host’s page for opening a pull or merge request against defaultBranch. Omnia never merges.

To create an external project from the dashboard, choose Link a repository under New project. See Work on an external repository.

A dev agent (“Try this agent”) is per project and per user, and runs the user’s workspace as it stands, uncommitted edits included.

A workspace nobody uses is removed, unless it holds work the repository does not have. Each workspace has an entry in status.workspaces, and the operator stamps its lastActivity whenever the user:

  • writes, moves, creates or deletes a file through the editor or the content API;
  • runs a source-control verb that changes the workspace (commit, discard, pull, push, Sync, create or switch a branch);
  • starts or ends a builder sandbox that mounts the workspace.

Reading files or the source-control status does not count. A stamp less than a minute old is not rewritten.

When a workspace has gone unused for the idle period (14 days by default; see below), the arena controller takes the workspace’s git index lock and checks what it holds:

The workspace holdsResult
Nothing the repository lacksThe workspace directory is deleted and its entry removed. Opening the project again clones it afresh.
Uncommitted changes, including untracked files no .gitignore coversFlagged and kept.
Commits on any local branch that its upstream does not have. A branch with no upstream counts in full.Flagged and kept.
A detached HEAD, or a directory that is not a git repositoryFlagged and kept.

A flagged workspace is never deleted. Its entry records what it holds (reason, changes, unpushedCommits), the dashboard shows it, and it stays flagged until its user uses it again, which makes it Active and restarts the idle period. When the index lock is held (a verb, a file write through the project editor, or git in the builder’s sandbox), the check is tried again five minutes later. So is a workspace that a builder sandbox mounts while its claim is live (new, Pending, Queued or Bound, and not being deleted), however old its entry’s stamp, and every workspace when the controller cannot list the namespace’s claims. A workspace that predates its entry is given one, stamped with the time it was found, and is not judged until a full idle period later.

“Upstream” is the workspace’s remote-tracking branch as of its last pull, push or Sync. The controller does not fetch, so work that was pushed from somewhere else still counts as unsaved and is kept.

Nothing scans the volume on a timer. The controller wakes when a workspace’s idle period runs out, and whenever an entry changes.

The idle period is the arena controller’s --arena-workspace-idle-after flag, set from the chart value enterprise.arena.controller.workspaceIdleAfter (a Go duration, default 336h).

After every seed, Sync or fetch, the controller exports the default branch’s tip and records it as the project’s head (status.repository.head and headContentPath). The dashboard reads what a project contains (its tools, provider groups and datasets) from the head.

Only regular files and directories are exported. Symlinks and submodules in a commit are skipped (the controller logs how many), never followed or fetched. A commit that cannot be exported at all (an entry name git would refuse, two entries at one path) leaves the project in Error, and a run of it fails with reason SnapshotUnexportable.

A run exports the commit it resolves to arena/sources/<project>/.arena/versions/<sha> on the workspace content volume and mounts that directory read-only into its workers. A snapshot is keyed by SHA and never changes. A project deleted and recreated under the same name starts with an empty repository and no snapshots.

Each time the controller exports the head, it sweeps the project’s snapshots. It keeps:

  • the head;
  • every snapshot a run of the project that has not finished pins (status.source.commit on an ArenaRun whose phase is not Succeeded, Failed or Cancelled);
  • the 10 most recently used others (a reused export counts as a use);
  • anything exported in the last hour, which may belong to a run or deploy that has not recorded it yet.

Everything else is removed, along with any export’s temporary directory older than an hour. Only directories named with a full commit SHA are touched; a symlink is never followed. A deploy compiles its snapshot into a PromptPack in the same request, so a deployed agent never depends on a snapshot being kept. A removed snapshot is only a cache of its commit: a run of that commit exports it again. A sweep that fails is logged and does not change the project’s phase.

The sweep runs whenever the controller reconciles the project: after a seed, a push to a hosted project, each poll of an external one, and on controller start.

FieldTypeDescription
phasestringPending, Ready or Error.
conditions[]ConditionSee Conditions.
observedGenerationint64Most recent generation observed by the controller.
repository.pathstringThe repository’s path relative to the workspace content subtree (arena/repos/<project>.git).
repository.defaultBranchstringThe branch a run uses when it names no ref.
repository.seedCommitstringThe commit the seed produced, when one did.
repository.headstringThe default branch’s tip when the controller last looked, which it does again after every push to a hosted main. Empty for a repository with no commits.
repository.headContentPathstringThe head’s snapshot, relative to the workspace content subtree.
repository.lastFetchTimetimeWhen an external remote was last fetched successfully.
workspaces[][]ArenaProjectWorkspaceOne entry per user’s workspace on the volume, keyed by user. See Idle workspaces.
workspaces[].userstringThe user’s pseudonym: the workspace’s directory name under arena/workspaces/<project>/. Never a name or email.
workspaces[].lastActivitytimeWhen the workspace was last used.
workspaces[].statestringActive, or Flagged: left idle with unsaved work, and kept.
workspaces[].reasonstringWhat a Flagged workspace holds, for example 3 uncommitted changes; main is 2 ahead.
workspaces[].changesint32A Flagged workspace’s uncommitted changes.
workspaces[].unpushedCommitsint32A Flagged workspace’s commits that no upstream has.
workspaces[].flaggedAttimeWhen the workspace was flagged.

The controller sets the phase on every reconcile; the diagram shows which outcomes lead from one phase to another.

stateDiagram-v2
[*] --> Pending: seed ConfigMap missing
[*] --> Ready: repository prepared
[*] --> Error: preparation failed
Pending --> Ready: ConfigMap found, repository prepared
Pending --> Error: preparation failed
Ready --> Pending: seed ConfigMap missing
Ready --> Error: fetch or export failed, or storage refused
Error --> Ready: a later reconcile prepares the repository
Error --> Pending: seed ConfigMap missing
ValueMeaning
PendingThe repository is not ready: a seed is waiting for its ConfigMap.
ReadyThe repository exists and runs can resolve refs in it. A hosted repository with no seed is Ready and empty until something commits to it.
ErrorThe repository could not be prepared: a fetch of the external remote failed (retried on the poll interval, and a hosted project’s failure every minute), its caBundleRef cannot be used, the licence does not allow the repository kind, or workspace content storage is disabled.

A run waits while its project is not Ready.

TypeReasons
ReadyReady; False with SeedConfigMapNotFound, RepositoryFailed, CABundleInvalid or ContentStorageUnavailable. A remote whose certificate does not verify is RepositoryFailed, with the TLS error (x509: certificate signed by unknown authority) in the message. A licence that does not cover an external repository’s Git source never sets it False: the project gets an UnlicensedCapability warning event and proceeds.
SeededTrue with Seeded (the message names the commit) or RepositoryNotEmpty; False with SeedConfigMapNotFound.

A hosted repository is always allowed. An external repository needs the licence’s git-source feature, which Core includes.

apiVersion: v1
kind: ConfigMap
metadata:
name: support-bundle
namespace: default
data:
config.arena.yaml: |
apiVersion: promptkit.altairalabs.ai/v1alpha1
kind: Arena
metadata:
name: support
spec:
prompt_configs:
- id: support
file: prompts/support.yaml
providers: []
scenarios:
- file: scenarios/refund.yaml
prompts__support.yaml: |
# ...
scenarios__refund.yaml: |
# ...
---
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ArenaProject
metadata:
name: support
namespace: default
spec:
repository:
hosted:
seed:
configMap:
name: support-bundle

External, with a read credential and wider deploy refs

Section titled “External, with a read credential and wider deploy refs”
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ArenaProject
metadata:
name: promptkit-examples
namespace: default
spec:
repository:
external:
url: https://github.com/AltairaLabs/PromptKit.git
defaultBranch: main
readSecretRef:
name: promptkit-read
deploy:
allowedRefs:
- refs/heads/main
- refs/heads/release-*
- refs/tags/**

A job runs a subdirectory of the repository by naming its arena file, for example arenaFile: examples/assertions-test/config.arena.yaml.

apiVersion: v1
kind: ConfigMap
metadata:
name: company-git-ca
namespace: default
data:
ca.crt: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
---
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ArenaProject
metadata:
name: support-evals
namespace: default
spec:
repository:
external:
url: https://git.internal.example.com/team/support-evals.git
caBundleRef:
configMapKeyRef:
name: company-git-ca
key: ca.crt
  • Arena Config File: Schema of the config.arena.yaml file in the repository
  • ArenaJob: Executes runs of this project (via spec.sourceRef, spec.ref and spec.arenaFile)