soul.yml
The config for the agent on a managed host. One file per host, by convention /etc/soul/soul.yml (the binary accepts --config <path>).
The config is needed in pull mode (the daemon holds a long-lived stream to Keeper). In push mode most of the fields go unused: Keeper hands the agent an already-rendered run plan over stdin rather than through persistent configuration. Raw Destiny/Essence never reaches the host in either mode — Keeper does the rendering.
The agent has no auth block and no Vault client. Authentication to Keeper is over mTLS only (a client certificate issued during onboarding). This is a deliberate isolation measure: the agent has no way to read secrets from Vault.
Type conventions
Section titled “Type conventions”| Notation | Meaning |
|---|---|
string | a UTF-8 string. |
int | an integer. |
bool | true / false. |
duration | a Go duration (5s / 500ms / 1h30m). |
enum{a,b,c} | a string from the listed set. |
string(host:port) | host:port; host is an IP or DNS name, port is 1..65535. |
fqdn | a DNS name (dot-separated labels). |
path | an absolute path on the host’s filesystem. |
default: — marks a required field.
sid — agent identifier
Section titled “sid — agent identifier”| Field | Type | Default | Purpose |
|---|---|---|---|
sid | fqdn | the host’s FQDN | Optional. Defaults to the host’s FQDN. Rarely overridden: if sid does not match the one the agent’s certificate was issued for, the connection will not pass the TLS layer. |
paths — filesystem paths
Section titled “paths — filesystem paths”If the host follows the standard /var/lib/soul-stack/ layout, the block can be omitted — the defaults apply.
| Field | Type | Default | Purpose |
|---|---|---|---|
paths.modules | path | /var/lib/soul-stack/modules | A cache of custom modules (plugins). |
paths.seed | path | /var/lib/soul-stack/seed | The directory of the agent’s mTLS identity (certificate + CA). The private key is generated locally during onboarding and never leaves this directory. |
keeper — connection to the Keeper cluster
Section titled “keeper — connection to the Keeper cluster”A list of endpoints with priorities (a fallback list), a retry policy, failback, and the path to the CA. The agent initiates the connection itself — no inbound ports need to be opened on the host.
keeper: endpoints: - host: keeper-1.example.com event_stream_port: 8443 # mTLS, soul run mode bootstrap_port: 9442 # server-only TLS, soul init mode - host: keeper-2.example.com event_stream_port: 8443 bootstrap_port: 9442 priority: 2 # lower priority = fallback retry: max_attempts: 5 backoff: { initial: 1s, max: 30s, jitter: true } handshake_timeout: 10s failback: enabled: true interval: 1h spray: 10m max_apply_size_mb: 8 tls: ca: /var/lib/soul-stack/seed/ca.crt| Field | Type | Default | Purpose |
|---|---|---|---|
keeper.endpoints | list | — | A non-empty list of the cluster’s endpoints. At least one. |
keeper.endpoints[].host | string | — | The Keeper instance’s host (FQDN or IP), shared by both phases. Required. |
keeper.endpoints[].event_stream_port | int (1..65535) | — | The port of the long-lived stream (mTLS, soul run mode). Required. |
keeper.endpoints[].bootstrap_port | int (1..65535) | — | The onboarding port (server-only TLS, soul init mode). Explicitly required — there is no silent fallback to another port. |
keeper.endpoints[].priority | int (≥1) | 1 | The priority (lower = preferred). Orders both phases. |
keeper.retry.max_attempts | int (≥1) | 5 | How many times in a row to try one endpoint before moving to the next. |
keeper.retry.backoff.initial | duration | 1s | The initial interval of the exponential backoff. |
keeper.retry.backoff.max | duration | 30s | The upper bound of the backoff. |
keeper.retry.backoff.jitter | bool | true | Whether to apply random jitter to the backoff. |
keeper.retry.handshake_timeout | duration | 10s | The timeout for establishing the TLS+gRPC connection to one endpoint. |
keeper.failback.enabled | bool | true | Whether to return to a higher-priority endpoint after failing down. |
keeper.failback.interval | duration | 1h | How often to try to return. |
keeper.failback.spray | duration | 10m | The amplitude of random spread around interval — protection against a thundering herd across thousands of agents. |
keeper.max_apply_size_mb | int (MiB, ≥1) | 8 | The size ceiling for a single inbound message from Keeper (a batch of rendered tasks). Must be ≥ Keeper’s send limit (listen.grpc.event_stream.max_apply_size_mb in keeper.yml); the defaults on both sides match (8 MiB). |
keeper.tls.ca | path | — | The cluster’s CA certificate: the agent uses it to validate the server side during the mTLS handshake. The client certificate and key live in paths.seed. |
In push mode the keeper block is ignored — the plan arrives over stdin, not over the stream.
soulprint — facts about the host
Section titled “soulprint — facts about the host”| Field | Type | Default | Purpose |
|---|---|---|---|
soulprint.refresh_interval | duration | 5m | How often the agent recollects facts about the system and (in pull) sends an update over the stream. |
The set of collected facts (OS family, distribution, architecture, package manager, init system, kernel, CPU, memory, network) is fixed by the binary and is not declared in the config.
cleanup — local cache cleanup
Section titled “cleanup — local cache cleanup”Applies in pull mode (the daemon cleans the cache periodically). In push, cleanup is handled on the Keeper side.
| Field | Type | Default | Purpose |
|---|---|---|---|
cleanup.modules_ttl_days | int (days) | 30 | How many days an unused module version lives in the cache before deletion. |
cleanup.run_interval | duration | 24h | How often the daemon sweeps the cache. |
logging
Section titled “logging”Behavior is symmetric to Keeper: without logging.file — output to stderr without rotation; with a file — built-in rotation without logrotate.
| Field | Type | Default | Purpose |
|---|---|---|---|
logging.level | enum{debug,info,warn,error} | info | The log level. |
logging.format | enum{json,text} | json | json for machine processing, text for humans. |
logging.file | path | — (stderr) | The path to the log file. Empty — stderr without rotation. |
logging.rotation.max_size_mb | int (MB) | 50 | The file size before rotation. |
logging.rotation.max_age_days | int (≥0) | 7 | How many days to keep an archive. |
logging.rotation.max_files | int | 5 | How many archives to keep. |
logging.rotation.compress | bool | true | Whether to compress archives. |
metrics
Section titled “metrics”Publishing the agent’s metrics. Unlike Keeper (where metrics are mandatory), on the agent they are optional: not every host wants to open a port.
metrics: enabled: true listen: "127.0.0.1:9091" # loopback by default — not reachable from outside basic_auth: # optional; needed only when bound off loopback enabled: true username: scrape password_file: /etc/soul/metrics-password # mode 0400, a single line| Field | Type | Default | Purpose |
|---|---|---|---|
metrics.enabled | bool | true | Enable publishing. With false the listener does not come up. |
metrics.listen | string(host:port) | 127.0.0.1:9091 | The /metrics address. Loopback by default — not exposed outward. |
metrics.basic_auth.enabled | bool | false | Enable Basic auth. Needed when bound off loopback (scraping from another host). |
metrics.basic_auth.username | string | — | The username. Required with enabled: true. |
metrics.basic_auth.password_file | path | — | The path to the password file (a single line). The source is a file, not Vault: the agent has no Vault client. Plaintext in YAML is forbidden. File permissions are the operator’s responsibility (0400 recommended). |
otel: enabled: true endpoint: "otel-collector.example.com:4317"OpenTelemetry is push: when enabled, the agent sends traces to an OTLP receiver itself. In the default distribution the receiving side can be a Keeper instance with OTLP reception enabled.
| Field | Type | Default | Purpose |
|---|---|---|---|
otel.enabled | bool | false | Enable OTLP export. |
otel.endpoint | string(host:port) | — | The OTLP receiver address (gRPC). Required with enabled: true. |
otel.export_metrics | bool | false | Optionally push metrics over OTLP. At this stage only traces are exported. |
plugin_runtime
Section titled “plugin_runtime”The lifecycle of plugins on the Soul side (custom modules). The structure and defaults are symmetric to Keeper (only the socket directory path differs). Full field table — keeper.yml → plugin_runtime; on the agent socket_dir defaults to /var/run/soul-stack/plugins/.
hot_reload
Section titled “hot_reload”The structure and defaults are identical to Keeper (enable_signal / enable_inotify / audit_correlation_id), see keeper.yml → hot_reload. On the agent, hot-reload works in pull mode via SIGHUP; in push mode it does not apply (the process is short-lived and there is no file on disk).
Hot-reload
Section titled “Hot-reload”In pull mode the daemon re-reads soul.yml on SIGHUP: parse → validation → atomic swap. A validation error leaves the current state untouched.
Without a restart, the following apply: the log level, retry/failback parameters, the fact-collection interval, local-cleanup parameters, and plugin policies.
A restart is needed for: sid, paths (paths.*), Keeper endpoints, the TLS CA, the recv limit, the metrics address/auth, the OTel exporter, and log paths and rotation parameters.
An API/MCP reload path (as on Keeper) is not provided on the agent in the current release. Centralized rollout of soul.yml goes through your config-delivery system; after the new file lands, the agent is sent a SIGHUP.
What is NOT in soul.yml
Section titled “What is NOT in soul.yml”- The
authblock — the agent authenticates over mTLS, not JWT. - Destiny and Essence — they never reach the host raw; Keeper renders them on its side and hands over the finished plan.
- The bootstrap token — used once during onboarding (from stdin or an environment variable), then not stored.
- The list of modules and their sources — the module registry lives on Keeper.
- The
versionfield — the binary’s version is determined by the artifact and is not duplicated in the config.
Full example
Section titled “Full example”# sid: redis-1.prod.example.com # optional; defaults to the host's FQDN
paths: modules: /var/lib/soul-stack/modules seed: /var/lib/soul-stack/seed
keeper: endpoints: - host: keeper-1.example.com event_stream_port: 8443 bootstrap_port: 9442 - host: keeper-2.example.com event_stream_port: 8443 bootstrap_port: 9442 - host: keeper-dr.example.com event_stream_port: 8443 bootstrap_port: 9442 priority: 2 retry: max_attempts: 5 backoff: { initial: 1s, max: 30s, jitter: true } handshake_timeout: 10s failback: enabled: true interval: 1h spray: 10m tls: ca: /var/lib/soul-stack/seed/ca.crt
soulprint: refresh_interval: 5m
cleanup: modules_ttl_days: 30 run_interval: 24h
logging: level: info format: json rotation: { max_size_mb: 50, max_files: 5, compress: true }
metrics: enabled: true listen: "127.0.0.1:9091"
otel: enabled: true endpoint: "otel-collector.example.com:4317"See also
Section titled “See also”- keeper.yml — the Keeper config (including
authfor operators). - Configuration → Overview — secrets, cross-cutting capabilities, hot-reload in brief.
- Soul — the agent’s role, pull/push modes, onboarding.