Deploy an Execution-Agent Fleet
This guide stands up an execution-agent fleet:
a pool of DB-less cloacina-agent workers that the server offloads task
execution to. For why the fleet exists and how it works internally, read the
explanation; this is the operational path.
- A running
cloacina-server(see Deploying the API Server). - A running
cloacina-compiler, or another way packages get built, so the server has.cloacinacdylibs for agents to fetch (see Running the Compiler). - An API key for the tenant whose work the fleet will run. The agent’s API key tenant scope decides which tenants’ tasks it may receive — scope it deliberately.
- Network reachability: agents must reach the server over HTTP/WebSocket.
- On each agent host: the shared libraries the compiled workflow cdylibs link
(
libpq,libpython,libssl,libsasl2, …). The published agent image carries these; a hand-rolled host must install them ordlopenfails at load.
Execution topology is a single server-level knob: the default executor key.
Every task is dispatched to that one executor — there is no per-task matching.
The default is default (the in-process thread executor); set it to fleet to
send all work to the agent fleet.
The preferred surface is a [server] section in ~/.cloacina/config.toml, which
cloacinactl server start reads and forwards to the cloacina-server binary:
[server]
default_executor = "fleet"
For ad-hoc or direct runs you can override it on the binary or via the
environment (precedence: explicit CLI/env > config.toml > built-in default):
# All three forms are equivalent overrides:
cloacina-server --default-executor fleet --bind 0.0.0.0:8080
CLOACINA_DEFAULT_EXECUTOR=fleet cloacina-server --bind 0.0.0.0:8080
cloacinactl server start --default-executor fleet
Deploy the fleet before you select it. The configured key is hard-matched against registered executors at server startup.
fleetis only a registered executor when you’ve opted into the fleet; if you setdefault_executor = "fleet"without the fleet deployed, the server fails fast at boot with an error listing the valid keys (e.g.default). There is no silent fallback todefault. Setfleettogether with (or after) standing up the agents below.
Each agent is a standalone cloacina-agent process. The minimum is a server URL
and an API key:
cloacina-agent \
--server http://cloacina-server:8080 \
--api-key "$CLOACINA_API_KEY" \
--max-concurrency 4
Or with the published image / Kubernetes — run N replicas, all pointed at the server, with the key from a secret:
env:
- name: CLOACINA_SERVER
value: "http://cloacina-server:8080"
- name: CLOACINA_API_KEY
valueFrom:
secretKeyRef: { name: cloacina-agent, key: api-key }
args: ["--max-concurrency", "4"]
Scale by adding replicas; the server spreads work across live agents greedily by free capacity. Useful options (full list in the CLI reference):
--max-concurrency <N>(default 4) — packets this agent runs at once; a saturated agent refuses further work.--cache-dir <PATH>/CLOACINA_AGENT_CACHE_DIR— persist the fetched-cdylib cache across restarts to skip re-fetching artifacts.--capabilities a,b— advertise free-form tags at registration.
Build profile must match. Agents
dlopenthe compiler’s cdylibs, and the fidius wire format depends on the build profile (debug = JSON, release = bincode). Release agents need release-built packages. Production images are release; build your workflow packages release too.
With default_executor = "fleet", every task runs on the fleet. Upload and run
any workflow, then confirm it executed on an agent rather than in-process:
cloacinactl package upload my-workflow.cloacina
cloacinactl workflow run my_workflow # prints an execution id
cloacinactl execution status <execution-id> # -> Completed
Confirm the fleet path (not the default executor) handled it:
- Server log:
fleet: agent reported; reconciling via shared TaskResultHandler. - Agent log: the agent registering the package’s cdylib as it loads it.
- Metrics:
/metricsexposes thecloacina_fleet_*andcloacina_delivery_outbox_openseries.
If the task instead runs on default, the server isn’t configured for the fleet
— re-check that default_executor resolves to fleet (config.toml [server],
CLOACINA_DEFAULT_EXECUTOR, or --default-executor).
Dead-agent detection and in-flight reclaim are governed by two server flags (defaults reproduce the prior hard-coded 15s / 45s):
| Flag / env | Default | Effect |
|---|---|---|
--agent-heartbeat-interval-s / CLOACINA_AGENT_HEARTBEAT_INTERVAL_S |
15 |
Heartbeat cadence advertised to agents + the sweep tick. |
--agent-liveness-misses / CLOACINA_AGENT_LIVENESS_MISSES |
3 |
Missed beats before an agent is declared dead. |
Effective dead-after = interval × misses. For faster failover (at the cost of more heartbeat traffic), e.g. ~10s detection:
CLOACINA_AGENT_HEARTBEAT_INTERVAL_S=5 CLOACINA_AGENT_LIVENESS_MISSES=2 cloacina-server ...
- Losing an agent is safe. When an agent dies, the sweeper reclaims its in-flight work onto a live agent in the same tenant and the task completes there — no workflow-level failure. The work re-runs from the start, so tasks run on the fleet should be idempotent.
- Watch these signals:
cloacina_delivery_outbox_openclimbing → delivery is wedged (often no live agent for a tenant’s work, or agents can’t connect).cloacina_fleet_agents_evicted_totalrising → agents dying / flapping.cloacina_fleet_work_reassigned_total→ how much work crashed agents are shedding onto survivors.
- Capacity: if
delivery_outbox_opengrows under steady load, add agents or raise--max-concurrency. - Draining: to retire an agent gracefully, stop sending it new work by scaling the pool and let it finish in-flight packets; a hard kill is recovered by the reclaim path within the detection window.
Everything above runs agents by hand. The control plane (CLOACI-I-0127) can
instead provision and scale a per-tenant agent pool for you, on a pluggable
substrate (a FleetActuator). It is off by default
(CLOACINA_FLEET_ACTUATOR=none); opt in per substrate.
Each tenant has an effective limit — the platform default
(CLOACINA_DEFAULT_MAX_AGENTS, default 4) unless a platform admin grants a
per-tenant override — and a desired_count in [0, effective_limit]
that a tenant scales for itself via the
tenant agent fleet API.
Provisioning only sets the target; an actuator (below) turns it into running
agents.
# Tenant-admin: view, then scale this tenant's fleet (+1 / -1).
curl -H "Authorization: Bearer $TENANT_ADMIN_KEY" \
http://cloacina-server:8080/v1/tenants/tenant_acme/fleet
curl -X POST -H "Authorization: Bearer $TENANT_ADMIN_KEY" \
http://cloacina-server:8080/v1/tenants/tenant_acme/fleet/provision # → 409 at capacity
# Platform-admin (god key): raise the tenant's ceiling.
curl -X POST -H "Authorization: Bearer $ADMIN_KEY" \
-H "content-type: application/json" -d '{"max_agents": 8}' \
http://cloacina-server:8080/v1/tenants/tenant_acme/limits
A newly created tenant is auto-seeded with
min(CLOACINA_INITIAL_AGENTS, CLOACINA_DEFAULT_MAX_AGENTS) (default 1) on
POST /v1/tenants; set CLOACINA_INITIAL_AGENTS=0 to disable.
For a single-host dev loop, the Docker actuator spawns/stops cloacina-agent
containers to match each tenant’s desired_count:
CLOACINA_FLEET_ACTUATOR=docker \
CLOACINA_AGENT_IMAGE=cloacina-agent:latest \
CLOACINA_AGENT_NETWORK=cloacina_net \
CLOACINA_AGENT_SERVER_URL=http://server:8080 \
cloacina-server --default-executor fleet --bind 0.0.0.0:8080
The actuator mints a tenant-scoped key per container and injects it as
CLOACINA_API_KEY, so you don’t manage agent keys yourself. The substrate guard
is fail-closed: docker refuses to start if it detects Kubernetes or finds
no Docker socket. It is dev-only — for production use the
Kubernetes actuator,
which scales a per-tenant agent Deployment with least-privilege RBAC.
On Kubernetes the Helm chart drives the actuator for you (fleet.actuator=kubernetes).
Beyond least-privilege RBAC and per-tenant namespaces, it hardens the fleet for a
production cluster:
restricted-clean agent pods — non-root (uid/gid10001), dropped capabilities,readOnlyRootFilesystemwithemptyDirs for the agent’s writable paths,seccompProfile: RuntimeDefault, and configurable resource requests/limits (fleet.agentResources). The pods carry no probes — the agent has no health endpoint; liveness is the server’s heartbeat/eviction sweep, tuned byCLOACINA_AGENT_HEARTBEAT_INTERVAL_S/CLOACINA_AGENT_LIVENESS_MISSES.- Per-tenant
NetworkPolicy(fleet.networkPolicy.enabled, default on) — deny all ingress; egress only to DNS + thecloacina-server. Defense-in-depth; the server-side ABAC remains the real boundary.
See Deploying to Kubernetes → Hardened agent pods + NetworkPolicy for the full knob list.
When an actuator is active, a leader-gated control loop autoscales
desired_count from per-tenant utilization (Σ in_flight / Σ max_concurrency)
and reconciles toward it. Tune it with the CLOACINA_AUTOSCALE_* knobs — up/down
thresholds, cooldown, floor, and tick interval — documented in
Environment Variables.
Set CLOACINA_AUTOSCALE=false to freeze automatic scaling and drive
desired_count by hand through the provision API while reconciliation keeps
running.
- Execution-Agent Fleet — how it works.
- CLI Reference —
cloacina-agent+ server fleet flags. - Environment Variables — agent + fleet env vars.
- Metrics Catalog — the
cloacina_fleet_*series.