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.
API Version
Section titled “API Version”apiVersion: omnia.altairalabs.ai/v1alpha1kind: ArenaProjectScope: namespaced. Short name: ap (e.g. kubectl get ap).
Spec Fields
Section titled “Spec Fields”| Field | Type | Required | Description |
|---|---|---|---|
repository | ArenaProjectRepository | Yes | Where the project’s commits live. |
deploy | ArenaProjectDeploy | No | What may be deployed from the project. |
repository
Section titled “repository”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.
| Field | Type | Description |
|---|---|---|
hosted | HostedRepository | A bare repository at arena/repos/<project>.git on the workspace content volume. |
external | ExternalRepository | An https git remote. |
repository.hosted
Section titled “repository.hosted”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.
| Field | Type | Required | Description |
|---|---|---|---|
seed.configMap.name | string | Yes, when seed is set | A 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.
repository.external
Section titled “repository.external”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | The remote. Must start with https:// (CEL). The operator dials only this URL, never one read from a repository’s own configuration. | |
defaultBranch | string | No | main | The branch a run or deploy uses when it names no ref. |
readSecretRef.name | string | No | A 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. | |
pollInterval | string | No | 5m | How often the controller fetches the remote, as a Go duration (30s, 5m, 1h). Intervals under one minute are raised to one minute. |
caBundleRef | CABundleRef | No | PEM 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.
repository.external.caBundleRef
Section titled “repository.external.caBundleRef”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.
| Field | Type | Description |
|---|---|---|
configMapKeyRef.name | string | A ConfigMap in the project’s namespace. A CA certificate is not secret, so this is the usual choice. |
configMapKeyRef.key | string | The key in its data (or binaryData) that holds the PEM. |
secretKeyRef.name | string | A 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.key | string | The 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.
deploy
Section titled “deploy”| Field | Type | Description |
|---|---|---|
allowedRefs | []string | Globs 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, sorefs/tags/**matchesrefs/tags/v1andrefs/tags/release/v1, andrefs/**/v1matchesrefs/tags/v1andrefs/tags/release/v1. - Every other segment is a Go
path.Matchpattern, so*,?and[...]match within one segment:refs/tags/*matchesrefs/tags/v1and notrefs/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.
Workspaces
Section titled “Workspaces”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
gitHostConnection for the host ofurl. With no account linked, the panel says “link your<host>account” and points to Connections. The operator dials onlyurl, never a remote orinsteadOfwritten 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.
Idle workspaces
Section titled “Idle workspaces”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 holds | Result |
|---|---|
| Nothing the repository lacks | The workspace directory is deleted and its entry removed. Opening the project again clones it afresh. |
Uncommitted changes, including untracked files no .gitignore covers | Flagged 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 repository | Flagged 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).
Snapshots
Section titled “Snapshots”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.
Retention
Section titled “Retention”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.commiton an ArenaRun whose phase is notSucceeded,FailedorCancelled); - 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.
Status Fields
Section titled “Status Fields”| Field | Type | Description |
|---|---|---|
phase | string | Pending, Ready or Error. |
conditions | []Condition | See Conditions. |
observedGeneration | int64 | Most recent generation observed by the controller. |
repository.path | string | The repository’s path relative to the workspace content subtree (arena/repos/<project>.git). |
repository.defaultBranch | string | The branch a run uses when it names no ref. |
repository.seedCommit | string | The commit the seed produced, when one did. |
repository.head | string | The 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.headContentPath | string | The head’s snapshot, relative to the workspace content subtree. |
repository.lastFetchTime | time | When an external remote was last fetched successfully. |
workspaces[] | []ArenaProjectWorkspace | One entry per user’s workspace on the volume, keyed by user. See Idle workspaces. |
workspaces[].user | string | The user’s pseudonym: the workspace’s directory name under arena/workspaces/<project>/. Never a name or email. |
workspaces[].lastActivity | time | When the workspace was last used. |
workspaces[].state | string | Active, or Flagged: left idle with unsaved work, and kept. |
workspaces[].reason | string | What a Flagged workspace holds, for example 3 uncommitted changes; main is 2 ahead. |
workspaces[].changes | int32 | A Flagged workspace’s uncommitted changes. |
workspaces[].unpushedCommits | int32 | A Flagged workspace’s commits that no upstream has. |
workspaces[].flaggedAt | time | When 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| Value | Meaning |
|---|---|
Pending | The repository is not ready: a seed is waiting for its ConfigMap. |
Ready | The repository exists and runs can resolve refs in it. A hosted repository with no seed is Ready and empty until something commits to it. |
Error | The 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.
Conditions
Section titled “Conditions”| Type | Reasons |
|---|---|
Ready | Ready; 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. |
Seeded | True with Seeded (the message names the commit) or RepositoryNotEmpty; False with SeedConfigMapNotFound. |
Licensing
Section titled “Licensing”A hosted repository is always allowed. An external repository needs the licence’s git-source feature, which Core includes.
Examples
Section titled “Examples”Hosted, seeded from a ConfigMap
Section titled “Hosted, seeded from a ConfigMap”apiVersion: v1kind: ConfigMapmetadata: name: support-bundle namespace: defaultdata: 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/v1alpha1kind: ArenaProjectmetadata: name: support namespace: defaultspec: repository: hosted: seed: configMap: name: support-bundleExternal, with a read credential and wider deploy refs
Section titled “External, with a read credential and wider deploy refs”apiVersion: omnia.altairalabs.ai/v1alpha1kind: ArenaProjectmetadata: name: promptkit-examples namespace: defaultspec: 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.
External, on a host behind a private CA
Section titled “External, on a host behind a private CA”apiVersion: v1kind: ConfigMapmetadata: name: company-git-ca namespace: defaultdata: ca.crt: | -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE--------apiVersion: omnia.altairalabs.ai/v1alpha1kind: ArenaProjectmetadata: name: support-evals namespace: defaultspec: repository: external: url: https://git.internal.example.com/team/support-evals.git caBundleRef: configMapKeyRef: name: company-git-ca key: ca.crtRelated Resources
Section titled “Related Resources”- Arena Config File: Schema of the
config.arena.yamlfile in the repository - ArenaJob: Executes runs of this project (via
spec.sourceRef,spec.refandspec.arenaFile)