Workspace content layout
The workspace content volume holds one directory tree per workspace namespace. This page lists every path in that tree, the component that writes and reads it, the mount modes that decide how pods address it, and the retention rules that apply to it. For the reasons behind the layout, see Workspace content layout.
The operator mounts the whole volume at --workspace-content-path (default /workspace-content) and addresses every workspace as:
/workspace-content/{workspace}/{namespace}/All paths below are relative to {workspace}/{namespace}/.
Mount modes
Section titled “Mount modes”The operator and the arena controller take --workspace-content-scoped, set by the chart value workspaceContent.scoped (default false). Both must agree.
| Mode | Chart value | Volume root of a workspace namespace’s PVC | subPath a pod uses for {rel} |
|---|---|---|---|
| Shared (share-root) | workspaceContent.scoped: false | the whole share | {workspace}/{namespace}/{rel} |
| Scoped | workspaceContent.scoped: true | the {workspace}/{namespace} subtree | {rel} |
A recorded contentPath (for example ArenaRun.status.source.contentPath) is relative to {workspace}/{namespace} in both modes. The subPath a pod mounts, and SandboxClaim.status.volumeSubPath, carry the {workspace}/{namespace} prefix in shared mode only.
A pod mounts the workspace’s own PVC, workspace-{namespace}-content.
{workspace}/{namespace}/├── arena/│ ├── repos/{project}.git/ bare repository│ ├── workspaces/{project}/{user}/ a user's working clone│ ├── sources/{project}/.arena/versions/{sha}/ project snapshot│ ├── template-sources/{source}/.arena/versions/{version}/│ └── template-indexes/{source}.json├── skills/{source}/.arena/versions/{version}/├── manifests/{promptpack}.json├── sandboxes/{claim}/ scratch sandbox filesystem└── sessions/{runtime}/{key}/ ├── session.json └── handles/{sandbox}/{instance}/ session sandbox filesystem| Path | Contents | Written by | Read by | Authoritative |
|---|---|---|---|---|
arena/repos/{project}.git/ | Bare git repository of an ArenaProject, plus an omnia-owner file recording the project’s UID | Arena controller (seed, fetch of an external remote); operator content API (push and sync on a hosted project) | Operator, Arena controller | Hosted project: yes. External project: no, the remote is |
arena/workspaces/{project}/{user}/ | One user’s working clone; {user} is the pseudonym (vuid) from the caller’s verified token | Operator content API on the user’s behalf; a builder sandbox bound to it; the Arena controller deletes it when idle | Operator content API; builder sandbox (read-write); dev agent runtime (read-only) | Until pushed |
arena/sources/{project}/.arena/versions/{sha}/ | One commit of the project, exported; {sha} is the full 40-character commit SHA | Arena controller (project head, run snapshot); operator (run start, deploy) | Arena worker pods (read-only); content API (read-only) | No |
arena/template-sources/{source}/.arena/versions/{version}/ | Synced ArenaTemplateSource content, with .arena/HEAD naming the served version | Arena controller | Arena controller, content API | No |
arena/template-indexes/{source}.json | Generated index of a template source’s templates | Arena controller | Operator content API | No |
skills/{source}/.arena/versions/{version}/ | Synced SkillSource content, with .arena/HEAD naming the served version; spec.targetPath overrides skills/{source} | Operator | Agent runtime containers | No |
manifests/{promptpack}.json | Skill manifest of a PromptPack, named after the PromptPack object | Operator (PromptPack reconciler) | Agent runtime containers | No |
sandboxes/{claim}/ | Filesystem of a scratch sandbox | Sandbox pod; the operator removes it once no SandboxClaim of that name exists | Sandbox pod | No |
sessions/{runtime}/{key}/ | session.json (the operator’s record of the session) and handles/{sandbox}/{instance}/, a sandbox’s /workspace for the session; {key} is the hex of the first 16 bytes of the SHA-256 of the session ID | Operator (session.json); sandbox pods (handle directories) | Sandbox pods | No |
Synced sources and sandbox sync-back share one writer, which stores {targetPath}/.arena/versions/{version}/ and {targetPath}/.arena/HEAD. {version} is the first 12 characters of the content SHA-256.
Content API path rules
Section titled “Content API path rules”The operator’s content API serves a workspace’s content to the dashboard. It resolves paths as follows.
| Request path | Result |
|---|---|
arena/projects/{project}/… | Alias for the caller’s own arena/workspaces/{project}/{user}/…. The clone is created on first use. Requires a verified user |
arena/projects | Refused (403) |
Anything under arena/workspaces/, arena/repos/, sandboxes/ or sessions/ | Refused (403) |
.git inside a project path | Refused (403); also omitted from listings |
Writes under arena/sources/ | Refused (403) |
PUT | Replaces the file atomically; an existing file keeps its permission bits |
PUT onto a symlink | Refused (400) |
PUT with neither If-Match nor If-None-Match | Writes unconditionally |
Conditional writes use the file’s SHA-256 hex digest.
| Mechanism | Behaviour |
|---|---|
ETag: "{hash}" header and hash field | Returned on file reads and PUT responses |
?hashes=1 on a directory | Adds hash to each file entry |
If-Match: "{hash}" on PUT | Writes only if the current hash matches |
If-None-Match: * on PUT | Writes only if the file does not exist |
| Precondition failure | 412 with {"error": "precondition failed", "currentHash": "{hash}"} |
Every write (PUT, DELETE and a move) holds a per-path lock across operator replicas, so a precondition holds whichever replica serves the request.
| Situation | Behaviour |
|---|---|
| Another write holds the path for more than about 2 seconds | 503 with Retry-After: 2 |
A write into a clone whose git index stays locked for about 2 seconds (a source-control verb, or git in the builder’s sandbox) | 409 workspace busy |
| The clone was removed as idle while the write waited | 409 workspace was removed; reopen the project |
| Lock files | .content-locks/ at the root of the volume, outside every workspace; never listed. A lock older than 30 seconds is taken to be left by a replica that died, and is broken |
Project identity
Section titled “Project identity”| Item | Rule |
|---|---|
| Projects list | The ArenaProject objects in the namespace, not a directory walk |
| Display name, description, tags | Read from .project.json in the project’s head snapshot (status.repository.headContentPath) and overlaid on the ArenaProject object. A project with no head has none; a malformed file is ignored |
| Project deleted and recreated under the same name | Starts with an empty repository and no snapshots. The repository’s omnia-owner file and the snapshot tree each record the project’s UID, and a different UID discards what is there |
Sandbox mount role paths
Section titled “Sandbox mount role paths”A mount role’s path template is relative to {workspace}/{namespace}/.
| Rule | Value |
|---|---|
| Built-in roles | scratch, session |
| Placeholders | Exactly one whole segment {id}; at most one whole segment {user} |
| Forbidden to reach or contain | sandboxes/, sessions/, arena/sources/, arena/repos/, arena/snapshots/, arena/deployed/, arena/template-sources/, arena/template-indexes/, skills/, manifests/ |
| Overlap | The paths of two roles may not overlap |
| Symlinks | A mount directory, or a directory on the way to it, that is a symlink is refused |
| Chart default | project: arena/workspaces/{id}/{user}, mustExist: true, minRole: editor |
See Workspace and SandboxClaim for the fields.
Retention
Section titled “Retention”| Content | Rule | Value |
|---|---|---|
Synced source versions (skills/, arena/template-sources/, sync-back targets) | After each sync, versions beyond the most recent by modification time are removed. The version HEAD names is never removed, so a source may hold one directory more than the cap. If HEAD cannot be read, nothing is removed | 10; fixed in the operator and the Arena controller, no chart value |
Project snapshots (arena/sources/{project}/) | The Arena controller sweeps after each head export. It keeps the head snapshot, every snapshot pinned by an unfinished ArenaRun of the project, the newest others, and any snapshot younger than the grace period | 10 others; grace period 1 hour |
| Deployed packs | A deploy compiles its snapshot at once, so a deployed pack pins no snapshot | None |
User clones (arena/workspaces/) | A clone idle past the idle period is deleted if it holds nothing its repository lacks, and flagged and kept otherwise | 14 days; --arena-workspace-idle-after, chart value enterprise.arena.controller.workspaceIdleAfter |
Session directories (sessions/) | Removed when session-api does not hold the session, or its AgentRuntime is deleted, and no claim of the session exists | Three consecutive misses spanning at least 1 hour; checked every 10 minutes |
Scratch directories (sandboxes/) | Removed once no SandboxClaim of that name exists | Same strikes and grace as sessions |
The idle period restarts on status.workspaces[].lastActivity of the ArenaProject. The operator stamps it on a file write, move, create or delete through the project alias, on a source-control verb that changes the clone, and when a builder sandbox mounting the clone binds or is deleted while bound. Reads do not stamp it. See ArenaProject for the status fields.
Who mounts the volume
Section titled “Who mounts the volume”| Component | Access | Scope |
|---|---|---|
| Operator | read-write | The whole share. Content API, SkillSource sync, PromptPack manifests, run and deploy snapshot export |
| Arena controller | read-write | The whole share. Repositories, project heads, template sources, run snapshots, idle clones |
| Arena worker pods | read-only | subPath to one snapshot |
| Agent runtime container | read-only | The workspace subtree at /workspace-content, only when the PromptPack declares skills and the operator has a content path |
| Dev agent runtime (live PromptPack) | read-only | One user’s clone at /workspace-source |
| Sandbox pods | per the SandboxClass mount, ReadWrite by default | One claim’s scratch, session or mount-role directory at the class’s filesystem.workspace.mountPath |
The dashboard does not mount the volume. It reads and writes through the operator’s content API.
Not on the volume
Section titled “Not on the volume”| Content | Location |
|---|---|
| Session media and attachments | The media backend of spec.media.storage: for local, the PVC named by local.volumeClaim, mounted by the facade and the runtime; otherwise an object store |
| Arena run output | The PVC named by output.pvc.claimName when output.type is pvc, or an S3 bucket |
| PromptPack source content | The API server: a PromptPackSource creates an immutable PromptPack and a backing ConfigMap for each version |
| Path | If lost |
|---|---|
arena/repos/ | Every commit of a hosted project is lost. An external project is fetched again from its remote |
arena/workspaces/ | Each user loses what they had not pushed. A clone is created again from the repository on next use |
arena/sources/ | Exported again by the next run or head export |
arena/template-sources/, arena/template-indexes/ | Synced and indexed again |
skills/ | Synced again from the SkillSource |
manifests/ | Rewritten on the next PromptPack reconcile |
sandboxes/, sessions/ | The files of the sandbox or session |
Related
Section titled “Related”- Workspace content layout: why the layout is shaped this way
- ArenaProject
- SandboxClaim
- SkillSource
- Workspace
- Isolate workspace content