core.soul
Register a Soul by SID in the Keeper's registry and bind it to stable Coven labels.
core.soul.registered binds a Soul (by SID) to a set of stable Coven labels in the Keeper's registries (Postgres souls + coven) — the step runs on the Keeper itself, and the on: keeper dispatcher is required (otherwise a scenario validation error). It is idempotent by design: changed=true only if the record was created by the module or the resulting coven set changed (order-independent comparison). It accepts a single SID or a list (via CEL register.<step>.hosts) and optionally carries the blocking await_online onboarding barrier; the module does not issue bootstrap tokens or SoulSeed — that is the onboarding's responsibility.
Requirements
- Rootnot required
- Side
keeper-side - Collection
soulstack.orchestration - Category
orchestration
States
core.soul.registered — The Soul with the given sid is in the Keeper's registry and bound to a set of Coven labels (per mode); it optionally carries the await_online onboarding barrier.
The souls record was created by the module, or the resulting coven set differs from the current one (order-independent comparison).
The souls record already existed and the coven set matched.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
sid | string | required | SID (FQDN) of the target Soul: a single string OR a list (via CEL `${ register.<step>.hosts }`) — register N hosts in one barrier step, ADR-061. |
coven | list<string> | required | A set of Coven labels (kebab-case), applied to all SIDs. |
mode | string | optional | append (default) | replace | remove. |
refresh_soulprint | bool | optional | Mid-run re-resolve of the run roster (ADR-061; stratification/re-resolve — slices S2/S3, echo refreshed:false). |
await_online | bool | optional | Onboarding barrier (ADR-061): after registration, block until the created Souls come online (Redis SID-lease). |
await_timeout | stringformat: duration | optional | Upper bound on the barrier wait. Required when await_online: true. Ceiling — keeper.yml::max_await_timeout. |
await_min_count | int | optional | Minimum online hosts for the barrier to succeed. Default — the number of registered SIDs (all). |
await_poll_interval | stringformat: duration | optional | Presence poll interval (default ~2s). |
Example — Bind a new Soul to the incarnation's root coven (append by default)
- name: Bind new replica to the incarnation root coven on: keeper module: core.soul.registered params: sid: "${ vars.new_sid }" coven: ["${ incarnation.name }"]Example — Register a list of provisioned VMs with an onboarding barrier (ADR-061)
- name: Register provisioned shards and await onboarding on: keeper module: core.soul.registered register: shards params: sid: "${ register.provision.hosts }" coven: ["${ incarnation.name }"] await_online: true await_timeout: 10mOutput
| Field | Type | Description |
|---|---|---|
sid | string | array<string> | echo of the input: a string for a single sid, an array for a list |
coven | array<string> | the resulting coven set after applying mode (not the passed argument) |
mode | string | the applied mode |
created | bool | true if at least one souls record was created by the module |
refreshed | bool | echo of refresh_soulprint |
removed | array<string> | only with mode: remove: the labels actually removed |
online | array<string> | only with await_online: the SIDs that became online by the time of success/timeout |
pending | array<string> | only with await_online: the SIDs that did not reach online by the timeout (B1-strict diagnostics) |
satisfied | bool | only with await_online: whether await_min_count was reached |
Reference
mode semantics
The strategy for applying the passed coven set to the host's already-bound labels.
| mode | Resulting coven set | Edge behavior |
|---|---|---|
| append (default) | existing ∪ passed | repeat with the same set — no-op (changed=false) |
| replace | only the passed ones (unmentioned are removed) | empty coven: [] — error (the host must keep at least one label) |
| remove | existing minus passed | labels not present on the host are skipped without error |
Notes
- Keeper-side, does not touch the host: all side effects are in the Keeper's registries (Postgres souls + coven), not in the file system or the Soul's processes. The module has no required_capabilities manifest — this is a keeper-internal operation on Postgres, and the host's root/capability do not apply here (access is the operator's keeper permission, see secure_defaults).
- If there is no souls record for the SID yet, the module creates one with status: pending (transport: agent, empty coven, CreatedByAID/LastSeenAt = null): a new host from a scenario or a host after a cloud-create. It does not issue bootstrap tokens or SoulSeed.
- list-SID: sid accepts a string OR a list. The target case is one create-scenario creating N VMs (their sids arrive as a list in register.<provision>.hosts from core.cloud.created), and this step registers them and (with await_online) waits for onboarding with a single barrier; the passed coven is applied to each SID. A literal list sid: [a, b] does not pass soul-lint statically (the manifest declares type: string) — the list always arrives as a CEL expression register.*. The output form follows the input: a single sid → a string, a list → an array.
- The await_online onboarding barrier: after registering all SIDs, the step blocks and polls presence (period await_poll_interval, default ~2s) under a shared await_timeout until the number of ready hosts is ≥ await_min_count (default — all SIDs). The source of online is the Redis SID-lease (a live EventStream), not PG souls.status. await_timeout is required when await_online: true and is capped from above by keeper.yml::max_await_timeout. A quorum shortfall at the timeout → the step fails → fail-stop of the run (B1-strict). await_online without a configured presence-checker on the keeper → failed (a silent success is not allowed).
- refresh_soulprint (ADR-061, S2/S3): true makes the step a stratification boundary — after it succeeds the scenario-runner re-resolves the run's roster before the next Passage (a live snapshot), and the refreshed output echoes the flag. Together with await_online it tightens the barrier to a facts-wait: a SID is ready = online AND a typed soulprint is written to PG (souls.soulprint_facts IS NOT NULL) — this removes the render_failed race on provision-from-zero.
Security & defaults
- Keeper-side, not Soul-side: the step runs in the Keeper's process (the on: keeper dispatcher), not by a soul agent on the host, so root/capability semantics do not apply. Access to running such a scenario is governed by the operator's RBAC at the run level (a keeper permission); the core module itself declares no separate permission, and the created souls record is written with CreatedByAID: null (a keeper-internal action).
- Input validation against injecting junk into the registry: sid is checked as an FQDN (keepersoul.ValidSID), each coven label as kebab-case 1..63 (keepersoul.ValidCoven); anything invalid — the step fails and nothing reaches the registry. This mirrors the POST /v1/souls API boundary, so the scenario path is not a back door around the checks.
- Footgun protection for mode: replace: an empty coven: [] is rejected with an error — the host must keep at least one Coven label, otherwise it would lose the incarnation's root coven and drop out of targeting.
- It does not issue bootstrap tokens or SoulSeed — the module neither produces nor discloses secrets; issuing bootstrap credentials remains the onboarding's responsibility.
- DoS guard for the onboarding barrier: await_timeout is capped from above by the operator ceiling keeper.yml::max_await_timeout (default 30m, fail-closed) — a step with an inflated await_timeout is rejected as failed BEFORE polling, rather than silently clamped, so a mistaken await_timeout does not keep a run goroutine / Acolyte worker busy.