Node-side rollout activation¶
autonomy runtime start --rollout-gated turns the release poller into a
condition-gated rollout activator. Instead of hot-swapping a new policy bundle
the moment the control plane advertises one, the node activates only when it
is both cohort-eligible and operationally safe — for example idle, sufficiently
charged, and with no human within safe distance.
This is opt-in. Without --rollout-gated, runtime start behaves exactly as
before: a verified release is activated unconditionally on the next poll.
How it works¶
Each poll response may carry a rollout plan pointer (a plan_id and
signature fields). Gating is layered onto the existing release poll loop so that
activation never runs ahead of release verification:
Refresh (every cycle, before verification): the poller’s rollout handler parses the plan pointer and fetches the full, signed plan and its status from
GET /v1/rollouts/{plan_id}, refreshing the node’s plan cache. This step has no side effects — it never activates.Verify: the poller verifies the release artifact’s signature (cosign), as it does for any release.
Gate (only after verification): the gated activator runs the rollout package’s cohort/integrity/slot/staleness evaluation, then the operational gate — the
rollout.activatepolicy over the node’s live state (below).Activate: only if the gate allows does it hot-swap the verified release (pull → load → reload). On a deny the current policy is left untouched.
A deny at step 3 defers activation; the node retries on the next poll and emits a
rollout.activation.deferred lifecycle event. Because a deferred node never
advances its active fingerprint, the next poll re-evaluates the gate — so the
node adopts the release as soon as it becomes safe.
The node-state contract¶
The node reports its live operational state through the existing scene-state
channel, using the reserved source name node:
POST /v1/scene-state
{
"source": "node",
"fields": {
"operational_state": "idle",
"battery": 82,
"hardware": "aarch64",
"labels": {"site": "warehouse-3"},
"human_within_safe_distance": false
}
}
The gate passes this source’s fields verbatim to the policy as
params.node — there is no field-by-field mapping in the runtime, so the policy
contract can evolve without code changes. The authoritative, non-spoofable half
of the input — params.rollout (plan, stage, slot_rank) — is filled in
from the verified plan, not from the node.
If no node scene source has been posted (or it is empty), activation is
deferred, fail-closed: a node that has not reported its state does not activate.
The built-in policy requires at least operational_state and battery.
Customizing the activation policy¶
The gate evaluates against your existing policy bundle — the same bundle
that governs /v1/tool. You do not produce a separate bundle for rollout
activation.
Out of the box: if your bundle does not define rollout activation, the runtime injects a built-in default (the documented safe-distance / idle / battery policy) so gating works immediately.
To customize: add
rollout.activaterules to your existing bundle. The contract is a top-levelallowthat delegates to a rule namedallow_rollout_activation(seepolicy/rollout.regofor the reference policy). As soon as your bundle definesallow_rollout_activation, the runtime uses your rules and stops injecting the default.
A minimal in-bundle override (added to your package autonomy policy) that only
activates idle nodes above 30% battery:
# rollout.activate delegation — lives in your EXISTING bundle.
allow if {
input.kind == "rollout.activate"
allow_rollout_activation
}
allow_rollout_activation if {
input.params.node.operational_state == "idle"
input.params.node.battery > 30
}
Because your bundle already declares default allow := false, do not add a
second default allow for rollout — the delegation above is all that is needed.
Delivering a plan to the node (control-plane side)¶
The node learns about a rollout entirely through its release poll: when the
channel’s latest release has an active rollout plan, the orchestrator
advertises a plan pointer on GET /v1/releases/latest, and the gated poller
fetches the full plan from GET /v1/rollouts/{plan_id}. There is no per-node
plan endpoint — the poll response is the delivery channel.
To make a plan node-consumable:
Publish the release to the channel the node polls (e.g.
stable). The release the plan targets must be that channel’s latest — the orchestrator only advertises a plan for the release it currently serves on that channel.Create the rollout for that release:
autonomy deploy <release_id> --fleet all
Use the release id (not an image ref).
deployresolves the release from the control plane and stamps its fullReleaseRef—artifact_ref,policy_ref,target_lock_fingerprint— into the plan, which the node-side gate binds against; a bare image ref that isn’t a registered release is rejected. A freshly created plan ispublishedwith its first stageopen, so it is advertised on the next poll.
Once both hold, GET /v1/releases/latest?channel=stable carries a rollout
field and the node begins gating it. If the poll shows no rollout field, the
plan’s target release is not the channel’s latest, or the plan is in a terminal
phase (cancelled/completed/rolled_back).
Flags¶
Flag |
Purpose |
|---|---|
|
Enable node-side, condition-gated activation. Requires a managed bundle and non-strict mode. Off by default. |
|
Node identity for slot-rank computation and telemetry. Default: hostname. Env: |
|
Path to an Ed25519 PKIX PEM public key used to verify plan integrity before activation. Env: |
Worked example: defer until idle¶
# 1. (Control plane) Create the rollout for the channel's latest release so the
# node's poll starts advertising a plan pointer (see "Delivering a plan" above).
autonomy deploy <release_id> --fleet all
# 2. Start the gated runtime pointed at your orchestrator.
autonomy runtime start --rollout-gated --orchestrator-url https://cp.example \
--cosign-pubkey /etc/autonomy/cosign.pub
# 3. The node is busy — report task_active.
curl -s localhost:7777/v1/scene-state -d \
'{"source":"node","fields":{"operational_state":"task_active","battery":90}}'
# → next poll defers activation (telemetry: rollout.activation.deferred)
# 4. The node goes idle.
curl -s localhost:7777/v1/scene-state -d \
'{"source":"node","fields":{"operational_state":"idle","battery":90}}'
# → next poll activates: the new policy bundle is verified, pulled, and hot-swapped.
Scope¶
This flow ships the behavior / policy-bundle rollout kind end-to-end. Model
(model) and OS-reconstruction (os_reconstruction) kinds remain fail-closed
(no activation callback); host node-state collection is the node’s
responsibility (self-report via /v1/scene-state); mesh-hint integration is
tracked separately.