The mental model: build vs. profile
Two independent things decide what a device can do. Confusing them is the most common source of “why isn’t my module running” questions.The build decides what is POSSIBLE
Omega is compiled with a set of Cargo features (a “SKU”). A feature that is not compiled in simply does not exist in that binary. See Build Omega.
The profile decides what RUNS
The profile’s
modules.required list names the modules to start, and security.capabilities grants the control-plane permissions. Nothing runs unless it is listed.Where the profile lives and how it loads
Omega loads its profile from a file path passed on the command line:config.yaml in the config directory) with the device’s identity and certificate paths filled in, and registers the service to run omega-agent --client <that path>. To regenerate it, delete the file and re-run the installer.
There is no hidden fallback: the file named on the command line is the only profile Omega reads. A malformed or invalid profile is fatal (see Validation).
Top-level structure
Anything else at the top level is treated as a module section (e.g.
sqlite-replication:) and is decoded by the module that owns it. Unknown sections are warned about (likely a typo or a section for a module you did not enable) but are not fatal.
connection
Everything Omega needs to establish an authenticated session with the platform.
connection.mqtt
connection.mqtt.tls
Each path field has a
<field>_env twin (ca_cert_path_env, client_cert_path_env, client_key_path_env) that reads the path from an environment variable instead. See Environment overrides.
connection.jitr — enrollment
Present only on a device that has not yet enrolled. Just-in-time registration (JITR) exchanges a factory-provisioned bootstrap key/cert for the device’s own certificate, then this block is removed. Full walkthrough: Enrollment (JITR).
connection.cert_renewal
Controls automatic certificate renewal (a background thread; see How renewal works).
modules
The single list of modules Omega runs. There is exactly one list — required — and it means what it says.
- A module in
requiredmust be compiled into this binary. If it is not (its build feature is disabled), Omega fails to start with a precise error naming the module — it is never silently skipped. - Nothing auto-runs. Only modules listed here start. The build deciding a module can exist does not make it run; the profile must name it.
- Module order does not need to be managed by hand — dependencies are resolved automatically.
Earlier versions of Omega had a second
optional list whose modules were skipped when unavailable. That is gone — silent skipping hid real build/config mismatches. A legacy profile that still carries an optional: key parses (the key is ignored), but those modules will not run. Move anything you actually want to required.required module that isn’t compiled is fatal, the profile’s modules.required is effectively a manifest that the binary must satisfy — which is exactly the guarantee you want: an online device is one whose declared modules are all present.
security
Capability grants, control-message authentication, and resource quotas.
security.capabilities
A map of capability → bool. A capability gates whether a module will accept control messages for its domain — a module can be running but reject every command if its capability is not granted (true).
Two rules make this predictable:
- A granted capability must be available in this build. If you set
some-cap: truebut nothing in the binary providessome-cap, Omega fails to start with an exact error (security.capabilities enables "some-cap" but it is not available in this build…). Capabilities are validated against what is compiled, so there is no separate list to keep in sync with the build. - A grant for a module that isn’t in
modules.requiredis reported as inert (a warning) — the grant does nothing because the module isn’t running.
ota, rpc, device-control, …). A few are provided internally by the ota module rather than being standalone modules — inventory and node — and are valid whenever ota is compiled.
Control-message signing — security.signed_control
Resource quotas
Shell command allowlist — security.rpc.allowed_commands
The rpc (and remote-command) capability runs only programs on this allowlist. An empty list denies everything. Keep it as narrow as the device actually needs — a broad allowlist is effectively remote shell.
Environment overrides
Almost every string field inconnection has a <field>_env twin that reads the value from an environment variable instead of hard-coding it in the file. For example server_url / server_url_env, device_id / device_id_env, tls.client_key_path / tls.client_key_path_env. Prefer the _env form for secrets and per-device values so the profile itself stays free of machine-specific state.
A few behaviors are driven by process environment variables rather than the profile:
Validation
Validate a profile without starting the device:- Exit
0and✓ config valid— the profile parses and every module and capability it names is present in this binary. - Exit
1with oneERROR:line per problem — validation collects all errors, each naming the exact field and cause, e.g.:
What happens on a bad profile at startup
If the profile is malformed or names something the binary can’t provide, Omega does not come up half-working. It:- writes the exact reason to
logs/startup-error.log(and, on Windows, the Application Event Log), - exits with a distinct code (
78), and - the service supervisor stops the service cleanly with that reason surfaced — so an operator sees what is wrong instead of a generic “service failed to start.”
Writing a profile from scratch
1
Start from identity
Set
name, and the connection block: transport, server_url, device_id, root_topic, and cert_dir. These are the fields that make the device reachable.2
Point TLS at the certificates
Fill
connection.mqtt.tls with the CA bundle and the device cert/key paths — or, for a fresh device, add a connection.jitr block so it enrolls on first boot and writes its own cert into cert_dir.3
List the modules you want to run
Add each module to
modules.required. Every one must be compiled into the binary you deploy — check with omega-agent --validate.4
Grant the matching capabilities
For each module that accepts control messages, add
security.capabilities.<module>: true. Validation will tell you immediately if a grant references something the build doesn’t have.5
Set quotas and signing
Set
max_payload_bytes to fit your broker’s packet limit, and enable signed_control for authenticated control.6
Validate before you ship
omega-agent --validate --client config.yaml — fix every ERROR: line until it prints ✓ config valid.Minimal profile
The smallest profile that boots, enrolls, and stays managed:Fuller profile
Adds enrollment, renewal, a capability grant, signed control, and quotas:Common pitfalls
Module listed in required but not compiled into the binary
Module listed in required but not compiled into the binary
Fatal at startup with
required module "X" is not available…. Either build a binary that includes that module’s feature, or remove it from modules.required. Run omega-agent --validate to catch it before deploying.Capability granted but its module isn't compiled
Capability granted but its module isn't compiled
Fatal with
security.capabilities enables "X" but it is not available in this build. Capabilities are validated against the build — remove the grant or add the feature.Reports never reach the platform
Reports never reach the platform
Usually
max_payload_bytes set larger than the broker or platform accepts, so large publishes are dropped in transit while small ones get through. Align it with the smallest packet-size limit on the path.Device comes up but rejects every command
Device comes up but rejects every command
The module is running but its
security.capabilities.<module> grant is missing or false. Grant it (true).See also
- Build Omega — how features/SKUs decide which modules a binary contains.
- Connect — transport, broker, and the topic namespace in depth.
- Enrollment (JITR) — provisioning a device its first certificate.