Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CRD reference — Workspace and CalibanTask

Both custom resources live in group caliban.caliban-ai.dev, version v1alpha1, and are namespaced with a status subresource. v1alpha1 means the shape may change without conversion webhooks (ADR 0001).

The Rust types in src/crd.rs and src/workspace.rs are the source of truth; deploy/crd/{calibantask,workspace}.yaml is generated from them and a test fails if the committed YAML drifts. See Regenerating the CRDs.

KindPluralShort namePrinter columns
WorkspaceworkspacescwsPhase, Age
CalibanTaskcalibantasksctaskPhase, Posture, Age

Runnable samples: deploy/samples/.


Workspace

Durable, shared config a CalibanTask references by name: git sources, named model providers (with their credential references), workspace-wide env, egress and agent policy. The operator is the sole reader of provider credential Secrets — a client such as prospero only ever sees the by-name reference (ADR 0004).

The Workspace controller provisions nothing. It validates the spec and writes status.

spec

FieldTypeRequiredDefaultNotes
displayNamestring (min 1)yes—Dashboard label.
sources[]listno[]Git checkouts. A workspace may be registered bare and gain sources later.
providers[]list (min 1)yes—Named providers a task can bind to.
defaultProviderstringno—Implicit when exactly one provider is defined.
env[]{name, value}no[]Non-secret env injected into every agent pod. Never put credentials here — use a provider credentialsRef.
isolationobjectno—Default isolation for agents in this workspace; a task's spec.isolation overrides it.
egressobjectno—Unset keeps allow-all egress. A task cannot override it.
agentPolicyobjectno—What agents here may do. A task cannot override it.

sources[] entries:

FieldTypeRequiredDefaultNotes
namestring (min 1)yes—Matches caliband's workspace source name.
repostring (min 1)yes—Git remote to clone.
refstringnomainBranch or tag only — git clone --depth 1 --branch rejects a raw commit SHA.
pathstring (min 1)yes—Absolute checkout path. Must be a directory strictly under the operator's workspace root (default /work) and distinct from every other source's path, or the Workspace goes Failed.

providers[] entries:

FieldTypeRequiredNotes
namestring (min 1)yesUnique within the workspace.
kindstring (min 1)yesanthropic, openai, google/gemini take their exact env names from caliban-contract. Other kinds (azure, bedrock, vertex, …) get <KIND>_BASE_URL / <KIND>_API_KEY, with azure/azure-openai mapped to AZURE_OPENAI_*.
baseUrlstringnoProjected under the kind's native base-URL env var.
modelstringnoRecorded in the pin. Not projected as env — the model reaches the worker through caliband's SpawnSpec.
credentialsRef{secretName, key}noOmit for keyless providers (e.g. a local openai-compatible endpoint). Reaches the pod as a secretKeyRef; the operator never inlines the value.

Base URL and API key are projected under provider-native env names, because caliban implements no CALIBAN_* namespace for them — it reads OPENAI_BASE_URL, ANTHROPIC_API_KEY and so on. The kind is additionally projected as CALIBAN_PROVIDER, not as a provider selector (selection travels in caliband's SpawnSpec) but because it is the documented input to a user-configured apiKeyHelper.

egress restricts agent-pod egress to DNS plus the listed destinations, replacing the default allow-all rule. An empty allow list is DNS-only.

FieldTypeRequiredDefaultNotes
egress.allow[].cidrstring (min 1)yes—IPv4 or IPv6 CIDR, e.g. 192.0.2.0/24, 2001:db8::/32. A malformed CIDR or a prefix longer than the address fails the Workspace.
egress.allow[].ports[]list of intno[]TCP ports. Empty allows every port. Anything outside 1-65535 fails the Workspace.

When you set egress, remember to include the workspace's git remotes, its model provider endpoints, and gonzalod if a task uses spec.state.

agentPolicy — what agents in this workspace may do

Typed settings for caliban's own permission and extension surface, so a cluster author configures them as CRD fields rather than as raw env entries. They win over an env entry of the same name. Every field is optional, and an unset field leaves caliban's own default alone — which is not the same as setting it to false.

The whole block is pinned into status.resolvedWorkspace.agentPolicy with the rest of the Workspace, so it cannot change under a running task.

The authorization switch:

FieldTypeDefaultNotes
allowUnattendedboolfalseAuthorizes both a task asking for permissionPosture: unattended and the unsupervised settings in the next table. Whoever can edit this Workspace controls it — treat Workspace write access as privileged (ADR 0006).

Permission settings. The three marked ⚠ leave no human answering prompts, so each is admitted only when allowUnattended: true — otherwise the task is denied before pinning with reason PostureNotPermitted, and the message names the setting. One switch is the whole answer to "may agents here run unsupervised?", so the same power cannot be reached through a back door (ADR 0006, decision 7).

FieldTypeNotes
permissionModeenumdefault, acceptEdits, plan, auto, ⚠ dontAsk, ⚠ bypassPermissions. Projected as CALIBAN_DEFAULT_PERMISSION_MODE; caliban's own --permission-mode flag still wins over it. dontAsk turns every Ask into Allow by caliban's definition, so it grants what bypassPermissions does; auto still classifies each call, so it does not count as unsupervised.
autoAllowbool⚠ Allow tools that would otherwise prompt. CALIBAN_AUTO_ALLOW.
noPermissionsbool⚠ Disable permission gating altogether — the bluntest lever. CALIBAN_NO_PERMISSIONS.

Extension settings. These reduce what an agent can reach rather than granting it power, so none of them needs allowUnattended.

FieldTypeNotes
noMcpboolNo MCP server discovery. CALIBAN_NO_MCP.
noHooksboolBypass every external hook handler; caliban's in-process hooks still run. CALIBAN_NO_HOOKS.
noSkillsboolNo skill discovery at startup. CALIBAN_NO_SKILLS.
noSubAgentboolDisable the built-in agent tool, so an agent cannot spawn sub-agents. CALIBAN_NO_SUB_AGENT.
strictKnownMarketplacesboolBlock plugin installs from marketplaces caliban does not recognise. CALIBAN_STRICT_KNOWN_MARKETPLACES.
enabledPlugins[]list of stringAn allow-list: every other discovered plugin is disabled. CALIBAN_ENABLED_PLUGINS, comma-joined. An empty list enables none, which is deliberately different from omitting the field — omitted enables everything caliban discovers, so the two cannot be collapsed.
blockedMarketplaces[]list of stringMarketplace names to block; unset blocks none. CALIBAN_BLOCKED_MARKETPLACES, comma-joined.
parallelToolLimitinteger ≥ 1Maximum concurrent tool invocations per turn. CALIBAN_PARALLEL_TOOL_LIMIT. The schema enforces minimum: 1 because caliban refuses 0 at startup; unset leaves caliban's default of one less than the CPU count.

Unlike a task's own posture, this policy is checked at admission only. It travels inside the pin, so it cannot change under a running task — and revoking a setting therefore does not disturb a task already running.

status

FieldNotes
phasePending (not yet reconciled) → Ready (all providers and credential Secrets resolve) or Failed.
messageWhy it failed, e.g. provider 'planner': secret 'anthropic-key' key 'api-key' not found.
observedGenerationThe .metadata.generation this status reflects.
conditions[]Standard Kubernetes conditions.

Validation runs in this order, first problem wins: source paths (under root, distinct) → egress CIDRs and ports → duplicate provider names → a resolvable defaultProvider → an existing Secret key for every credentialsRef.

The controller deliberately does not watch Secrets — that would need cluster-wide list/watch on every Secret. Recovery rides the requeue instead: a Ready workspace is re-checked every 300s, a Pending or Failed one every 15s, so creating a missing Secret unblocks it within seconds.


CalibanTask

One agent session: a reference to a Workspace, the prompt, and per-run overrides. The operator reconciles it into an agent-sandbox Sandbox, a token-less ServiceAccount and a NetworkPolicy (ADR 0002).

spec

FieldTypeRequiredDefaultConsumed by
workspaceRef.namestring (min 1)yes—operator
providerRefstringnothe workspace's defaultProvider, else its sole provideroperator
task.promptstring (min 1)yes—prospero
task.agentTypestringno—carried on the CR; the operator does not read it
task.interactiveboolnofalseprospero (SpawnSpec.interactive); the operator only carries it
task.permissionPostureenumnosupervisedoperator (admission gate) + prospero
model.namestring (min 1)nothe resolved provider's modeloperator (written into the pin)
model.routerConfigRefstringno—operator
state.gonzaloEndpointstringno—operator
state.modeenum: remote | localnoremote when gonzaloEndpoint is setoperator
state.tokenRef{secretName, key}no—operator
isolation.runtimeClassstringnothe workspace defaultoperator (pod runtimeClassName)
isolation.worktreesstringno—declared, not yet consumed
resources.classstringno—declared; SandboxTemplate selection is deferred
lifecycle.idleTimeoutstringno—declared; idle→pause mapping is deferred
lifecycle.onDeletestring: checkpoint | deleteno—declared; the drain finalizer is deferred
tools[]list of stringno—declared, not yet consumed

Which closed sets the API server enforces. task.permissionPosture, state.mode and the Workspace's agentPolicy.permissionMode carry an enum in the generated schema, so a typo in any of them is rejected at admission.

state.mode is a deliberate hybrid: the enum lives in the schema while the Rust type stays a String. A task stored before the enum existed may hold any value, and a typed enum would make such an object fail to deserialize — the operator could not read it at all. So storage::state_problem still rejects an unknown mode at reconcile time (InvalidState), which is what those pre-existing objects get.

The strings in isolation.worktrees, resources.class and lifecycle.* carry no schema enum and are unread. For lifecycle.onDelete that is deliberate rather than an oversight: #42 implements it (drain finalizer → Draining phase → status.checkpointRef, designed in caliban ADR 0057) and waits only on prospero reporting AgentsDrained, so pinning the accepted values now would freeze a surface that work may still refine.

model.name — per-task model override

A task may name its own model without the Workspace binding a whole new provider for it. The override is applied to the pinned resolution, so:

  • the bound provider's endpoint and credential still apply — only the model moves;
  • it cannot change under a running task, like everything else in the pin (ADR 0004);
  • prospero needs no change, since it already builds SpawnSpec.model from the pinned provider.

It is applied after admission, because which model to run is not an admissibility question. A blank name is treated as a misconfiguration rather than a selection: it leaves the provider's model alone instead of clearing it, and the schema's minLength: 1 rejects it outright. model.name is independent of model.routerConfigRef and may be set alongside it.

spec.isolation is a per-run override that wins over the workspace default. That is exactly why egress and agentPolicy are Workspace-level fields and not part of the shared isolation block — a task must not be able to widen a restriction the workspace imposed.

permissionPosture

ValueMeaning
supervisedA permission prompt goes to a human, who answers it. The default, and what an unset field means — the system fails closed.
unattendedThe session runs under an authorized bypass profile with no human in the loop.

unattended is admitted only when the referenced Workspace sets agentPolicy.allowUnattended: true. A missing policy — including a pin made before the field existed — does not permit it. The check runs before pinning, so a denied task is never pinned to the policy that denied it; allowing it on the Workspace, or editing the task back to supervised, recovers it. The operator never silently downgrades a posture.

The same switch also authorizes the Workspace's own unsupervised agentPolicy settings, so a task is denied with the same PostureNotPermitted reason whether the unsupervised power was asked for by the task or by the workspace — see agentPolicy.

A pinned task is re-checked against its pin on every reconcile. Revoking allowUnattended therefore does not disturb a task already running, but editing a running task's spec to unattended fails it if the pin does not allow it.

The operator does not hand the posture to caliband. prospero maps it onto SpawnSpec.permission_posture, reading status.permissionPosture — the posture the operator admitted — not the spec field.

state (gonzalo persistence)

spec.state becomes caliban's storage settings by environment (CALIBAN_STORAGE_SUBSTRATE, CALIBAN_STORAGE_REMOTE_URL, CALIBAN_STORAGE_REMOTE_TOKEN_ENV), so the operator never authors caliban's settings file. The CRD's local is caliban's fs. Requires a caliban build with environment overrides for those settings.

A tokenRef projects the gonzalod bearer token into the pod as GONZALO_TOKEN from the named Secret; only the variable's name appears in the pod spec. Use a principal with read/write on the gonzalod caliban namespace only — no write on fleet or fleet-audit.

These combinations fail the task with reason InvalidState: an unknown mode; mode: remote without gonzaloEndpoint; mode: local with a gonzaloEndpoint it would never use; a tokenRef without remote storage; a tokenRef whose Secret or key is missing.

status

FieldOwnerNotes
phaseoperatorSee below.
calibandEndpointoperator<serviceFQDN>:<calibandPort>, set only once the Sandbox is Ready.
sandboxRef.nameoperator<task>-sbx.
resolvedWorkspaceoperatorThe pinned config (sources, the single resolved provider, env, isolation, egress, agentPolicy). Set once at admission; later Workspace edits do not re-pin a running task.
permissionPostureoperatorThe posture the task was admitted with. The only posture prospero acts on.
conditions[]splitA map-list keyed by type. The operator owns Ready; prospero owns AgentsSettled. Each applies only its own entries under its own field manager, so neither clobbers the other (ADR 0005).
checkpointRef—Declared; checkpointing is deferred.

The operator writes status with server-side apply under field manager caliban-operator. A field it owns but omits is removed, so a cleared value is omitted rather than serialized as null.

Phases

PhaseWhen
PendingObserved before its first successful apply — no Sandbox yet.
ProvisioningThe Sandbox is applied but has not reported Ready.
Runningagent-sandbox reports the Sandbox Ready=True — the pod passed its readiness probe on caliband's control port and the Service is ready.
DrainingDeclared; not yet produced (the drain/checkpoint lifecycle is deferred).
Completedprospero's AgentsSettled=True with reason Succeeded.
Failedprospero's AgentsSettled=True with reason Failed, or a terminal operator-side failure (below).

A serviceFQDN alone is not readiness: it appears as soon as the Service object exists, before caliband has bound its port. Running gates on agent-sandbox's own Ready condition instead.

Terminal phases come from prospero, because infrastructure cannot tell "agent running" from "agent finished, pod still up" — only caliband's agent list knows, and the operator is never a caliband client. Without prospero deployed, a finished task stays Running.

Ready condition reasons (operator-owned)

reasonstatusMeaning and recovery
RunningTruecaliband is up and attachable.
SandboxNotReadyFalseagent-sandbox reported not-ready; message carries its detail (e.g. "Pod is Running but not Ready").
CompletedFalseAgents finished successfully; the task is done, so not Ready.
AgentsFailedFalseOne or more agents failed or crashed.
InvalidNameFalseThe task name is too long for its Sandbox's Service name (<task>-sbx, max 63 chars). Names are immutable — recreate under a shorter name. The operator stops retrying.
WorkspaceUnresolvedFalseNo such Workspace, a Failed one, or an unresolvable/ambiguous providerRef. message names which. Re-checked every 30s; fixing the Workspace recovers the task.
InvalidStateFalsespec.state is malformed or its token Secret is missing. Re-checked every 30s.
PostureNotPermittedFalseUnsupervised power without agentPolicy.allowUnattended — either the task's permissionPosture: unattended or the Workspace's own permissionMode: dontAsk/bypassPermissions, autoAllow, noPermissions. message names which setting. Re-checked every 30s.

AgentsSettled reasons are prospero's: Succeeded, Failed, or AgentsActive (not settled — an idle interactive task stays unsettled).

A task whose Workspace exists but is still Pending is not failed: it is not pinned either, and the reconcile simply requeues in 10s to re-check once the Workspace controller has validated it.

Events

kubectl describe calibantask <name> shows what the logs would otherwise hold. Notes are truncated at 1 kB; publishing is best-effort and never fails or delays a reconcile.

ReasonTypeWhen
PhaseChangedNormalThe phase moves, e.g. Provisioning → Running.
UnattendedAdmittedNormalA task was admitted unattended; the note names the Workspace whose policy allowed it.
ReconcileErrorWarningA reconcile returned an error.
InvalidName / WorkspaceUnresolved / InvalidState / PostureNotPermittedWarningThe task was marked Failed; the Event reason is the failure's own reason code.

What one CalibanTask creates

ObjectNameNotes
Sandbox (agents.x-k8s.io/v1beta1)<task>-sbxThe caliband pod, its workspace PVC, and a headless Service giving the stable serviceFQDN.
ServiceAccount<task>-saautomountServiceAccountToken: false and no Role/RoleBinding — the pod holds zero API permissions. The token-less SA is the RBAC posture.
NetworkPolicy<task>-netpolpolicyTypes: [Ingress, Egress], selecting the pod by the labels the operator propagates.

All three carry a controller OwnerReference to the task, so deleting the task cascades, and are applied with server-side apply so re-reconciles converge on one object. Deleting a task tears its Sandbox down immediately, without checkpointing in-flight agents (the drain finalizer is deferred).

The pod runs every container — the git-clone init container included — as one uid/gid (default 10001, caliban's app user) with runAsNonRoot: true and fsGroup set to the same gid, so the clone step and the agent cannot drift apart and a non-root clone succeeds whatever mode the volume provisioner gave the volume root. See Deploying for the one case this does not repair.

The reconcile re-runs on every change to the task and to the Sandboxes it owns, so a Sandbox becoming Ready reconciles the task at once. The 300s periodic resync is only a safety net against missed events.


Regenerating the CRDs

The committed YAML is generated, never hand-edited. A *_is_in_sync test fails if it drifts from the Rust types:

cargo run --bin crdgen calibantask > deploy/crd/calibantask.yaml
cargo run --bin crdgen workspace   > deploy/crd/workspace.yaml
cargo test --workspace

The Sandbox type in src/sandbox.rs is a read/write view of a CRD agent-sandbox owns, not a schema of record. crdgen never emits it and no test drift-guards it; fields the operator omits are pruned by the API server's structural schema.

After a CRD change, the copies in the caliban-crds Helm chart need the same update — that chart names deploy/crd/*.yaml here as its source of truth.