Skip to content

Config, secrets, and variables

Partial

Everything an operator sets resolves the same way: a typed value, owned at a scope, resolved most-specific-wins down the cascade. Three kinds share that resolution but differ in what they are keyed to and what lifecycle they carry:

| | config | secret | variable (macro) | |---|---|---|---| | what it is | a declared device setting | an access secret, encrypted at rest | a free interpolated value | | keyed by | a canonical signal (property_type) | its own secret_type shape name | its own name, an org-local cascade namespace | | has an observed side? | yes, a sample via a get function | its validity, not the secret value | no | | lifecycle | drift → reconcile (a set function) | refresh + rotation + expiry (deferred) | none; resolved and interpolated | | example | video-input = HDMI1 | an snmp-community, a basic-auth | poll-interval = 30s, a base URL, a label |

The common thread is the cascade and an exclusive-arc scope (exactly one of platform | template | location | system | component): the same exclusive-arc ownership as samples, plus a template-scoped binding the sample arc lacks (and unlike samples, config is not node-owned).

config: declared device state, keyed to a signal

Section titled “config: declared device state, keyed to a signal”

A secret is a typed value, encrypted at rest, owned on the exclusive arc (platform | location | component; secrets have no system band, ADR-0052) and resolved down the cascade (ADR-0017, built). A secret is its own primitive (stored encrypted, read back only through a masked, audited path), not a sensitivity flag on ordinary values; within it, the admin_sensitive flag below splits admin-only platform credentials from operator-visible device secrets.

A secret carries a label beside its name for the same reason a variable does: the name is the cascade key, unique per owner and on the entity name rule, and the label is what an operator reads in the directory. It reaches every projection the name reaches, the directory and the per-component cascade both; the reveal and the copy carry no identity at all, only the decrypted field map, so there is nothing there for it to appear in.

Two senses of “platform”, kept apart. A platform credential is a kind of secret (a Zoom client secret), marked by admin_sensitive at whatever scope it sits; the platform tier is a place on the cascade, the install-wide rung. The words are unrelated.

Shape is a secret_type registry: a per-field list of {name, type, secret, origin}, so one field is secret (a password) while another is plaintext (a username), origin marking whether the operator or the lifecycle fills it. A default_admin_sensitive boolean seeds the create form’s admin_sensitive default: a device type (snmp-community, basic-auth) defaults operational, a platform-integration type (oauth2-client) admin-sensitive; those are the ship-with types, with official marking shipped-canonical versus org-local as in the sample and role registries.

Envelope-encrypted at rest. Crypto is envelope AES-256-GCM behind a pluggable KEK provider: the key comes from the env (OMNIGLASS_SECRET_KEY), a file (OMNIGLASS_SECRET_KEY_FILE), or a warned fallback under OMNIGLASS_DATA_DIR, a KMS or Vault able to drop in behind the same seam. Each secret field is sealed under a per-value DEK wrapped by the KEK, (owner, name, field) bound as AAD so a ciphertext cannot be lifted from one row into another, and stored as its {ciphertext, nonce, wrapped_dek, key_id} envelope (a non-secret field plaintext).

Consumed at the site, by token. The cascade is the reuse mechanism (define once high, inherit below); interpolation is a $sec:name.path token in an interface input or function arg, never inside a secret’s own fields (the splicing consumer is the deferred collection-driver slice).

Masked everywhere except an audited decrypt. A secret’s value is masked (••••••) in every read. Reading the plaintext is a separate, privileged action: secret:reveal gates the decrypt (on-screen reveal and clipboard copy, recorded under distinct reveal and copy verbs), and every decrypt writes an audit row.

Two axes decide who reaches a secret (ADR-0025). Placement scope gives locality: a Singapore-scoped field tech works device secrets under Singapore, not one attached at the platform tier. The per-secret admin_sensitive flag gives same-scope sensitivity: when set, every action on that secret lifts to the :admin tier, so a platform credential stays admin/owner-only beside an operational device secret. secret is also a sensitive resource off the bare *:read floor: a viewer reads no secrets, an operator/deploy holds an explicit scoped secret:read,reveal,create,update. Concretely:

  • The /secrets directory is scope-filtered and hides admin-sensitive rows from a caller without the admin tier.
  • Reveal, update, and delete of an admin-sensitive secret without the admin tier is a non-disclosing 404 (identical to an out-of-read-scope row), never a 403.
  • Creating a secret marked admin_sensitive needs secret:create:admin (nor may an operator pick a type that defaults admin-sensitive).
  • Sealing, editing, and revealing an operational secret are open to operators in scope; delete stays admin-and-owner. admin holds secret:> (reaching the :admin tier a two-token secret:* cannot); owner’s > covers everything.

Shared versus per-device is just scope: a fleet-wide SNMP community sits high in the cascade, a unique-per-device secret at component scope. Nothing else to model.

variable: free interpolated values (macros)

Section titled “variable: free interpolated values (macros)”

A variable is the leftover, and the most familiar: a value spliced into behavior that is not a device signal and carries no lifecycle (a poll interval, a base URL, an environment label). Zabbix-style macros, resolved down the same cascade and interpolated as $var:<name> into functions, interface definitions, and rule scopes.

  • A name is org-specific, not a canonical signal, the one place the “operator-defined, not curated” namespace applies: no registry, no pre-registration. The ordinary entity name rule still holds, though (one segment of lowercase letters, digits, and hyphens, no dot), so newtron-ssh names a variable and newtron.ssh is refused. Sprawl is controlled by a creation role-gate (IAM) and by every variable being surfaced in the tree.
  • A label rides beside the name, the friendly string an operator reads (“Poll Interval”) where the name is what the cascade resolves by (ADR-0119). Optional, not an identifier, and never interpolated: $var: reads the name and only the name. A variable with no label reads its name verbatim.
  • Install-wide and template-local are the same primitive at different scopes (a company-wide NTP server versus a device class’s poll interval).
  • No observed side and no reconcile, exactly what separates it from config.

Scalar shapes (string, int, float, bool, json) cover the common case; a variable may be flagged secret (a webhook signing token) without being a full secret cell, since it has no lifecycle.

property: one typed name, a classifier contract, a stored value

Section titled “property: one typed name, a classifier contract, a stored value”

A property is one typed name used three ways: the catalog says the name exists and what it means, a classifier contract says which names its instances carry (and their defaults), and a value row says what one owner holds. Where a variable or secret is a single cascaded cell, a property is a named slot a whole class carries, set per instance (“every Boreal Edge 55 carries an asset_tag” is one declaration, the fix for Zabbix’s fixed inventory_1..40).

The provenance column is why this is one primitive: the same name and store hold a value a device observed, a rule calculated, config intended, and an operator declared.

The Properties panel can set, re-set, and clear a value: the effective read returns the property id (value_id) alongside the literal, so each override row offers a revert deleting the value and falling back to the contract default (the owning entity’s own :update write).

A tag is an operator key: value label for dimensions Omniglass does not model natively (category: audio-dsp, environment: prod): not a signal, no lifecycle, riding the cascade with a union-on-key, override-on-value combinator over a tenant-wide governed vocabulary. The tags page owns the model and the cascade the precedence.

  • The cascade. One resolver serves all four kinds; the structural chain, precedence key, and fall-through to a declaration default are stated once, on cascade; tags ride it with their own combinator (tags).
  • Interpolation renders variables ($var:) and secret fields ($sec:) into requests (a later consumer slice for both); config is read by key like a sample.

The shape registries, the value cells, and the operator-label tables; the physical layout (the owner arc, the cascade key) lives on storage. The secret and variable cells are built; the config cell is Design.

The two shipped members each got their own table rather than a shared discriminated cell (the secret_type + secret pair carries encryption and a masked read path; variable types inline). Whether the config cell joins variable in one table with a discriminator is open; they share the cascade and the exclusive-arc scope either way.

secret_type (Built) is the secret shape registry (snmp-community, basic-auth seeded); official marks shipped-canonical versus org-local.

secret_type (config): generated from the migrated schema by make gen
Column Type Constraints Notes
name text not null
official boolean not null, default false Shipped-canonical versus org-local; official rows are read-only
label text
schema jsonb not null, default '[]'::jsonb The per-field shape: {name, type, secret, origin} per field
created_at timestamp with time zone not null, default now()
default_admin_sensitive boolean not null, default true
id uuid PK, default uuidv7()
CHECK constraints and unique indexes on secret_type
  • secret_type_name_key: CREATE UNIQUE INDEX secret_type_name_key ON public.secret_type USING btree (name)

secret (Built) is the encrypted cell and the $sec: cascade key; scope is the exclusive arc without its system arm (ADR-0052); visibility and the masked read path are per ADR-0025 above.

secret (config): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
name text not null
owner_kind text not null
value jsonb not null, default '{}'::jsonb Secret fields as {ciphertext, nonce, wrapped_dek, key_id} envelopes; non-secret fields plaintext
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
admin_sensitive boolean not null, default true Flips the secret to the admin tier regardless of placement
component_id uuid FK → component.id
system_id uuid FK → system.id
location_id uuid FK → location.id
secret_type uuid FK → secret_type.id, not null
label text The friendly string an operator reads; NULL falls back to the name, verbatim
CHECK constraints and unique indexes on secret
  • secret_owner_arc: CHECK ((((owner_kind = 'platform'::text) AND (component_id IS NULL) AND (system_id IS NULL) AND (location_id IS NULL)) OR ((owner_kind = 'component'::text) AND (component_id IS NOT NULL) AND (system_id IS NULL) AND (location_id IS NULL)) OR ((owner_kind = 'system'::text) AND (system_id IS NOT NULL) AND (component_id IS NULL) AND (location_id IS NULL)) OR ((owner_kind = 'location'::text) AND (location_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL))))
  • secret_owner_kind_check: CHECK ((owner_kind = ANY (ARRAY['platform'::text, 'component'::text, 'location'::text])))
  • secret_component_key_uuid: CREATE UNIQUE INDEX secret_component_key_uuid ON public.secret USING btree (name, component_id) WHERE (owner_kind = 'component'::text)
  • secret_location_key_uuid: CREATE UNIQUE INDEX secret_location_key_uuid ON public.secret USING btree (name, location_id) WHERE (owner_kind = 'location'::text)
  • secret_platform_name: CREATE UNIQUE INDEX secret_platform_name ON public.secret USING btree (name) WHERE (owner_kind = 'platform'::text)
  • secret_system_key_uuid: CREATE UNIQUE INDEX secret_system_key_uuid ON public.secret USING btree (name, system_id) WHERE (owner_kind = 'system'::text)

variable (Built) is the plaintext variable cell and the $var: cascade key; scope is the exclusive arc. Typed inline (no variable_type registry: the value is validated against value_type in the app), no observed side. The config cell is a separate, deferred member.

variable (config): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
name text not null
value_type text not null string, int, float, bool, or json; the app validates value against it
owner_kind text not null
value jsonb not null The cascade-resolved payload
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
component_id uuid FK → component.id
system_id uuid FK → system.id
location_id uuid FK → location.id
label text The friendly string an operator reads; NULL falls back to the name, verbatim
CHECK constraints and unique indexes on variable
  • variable_owner_arc: CHECK ((((owner_kind = 'platform'::text) AND (component_id IS NULL) AND (system_id IS NULL) AND (location_id IS NULL)) OR ((owner_kind = 'component'::text) AND (component_id IS NOT NULL) AND (system_id IS NULL) AND (location_id IS NULL)) OR ((owner_kind = 'system'::text) AND (system_id IS NOT NULL) AND (component_id IS NULL) AND (location_id IS NULL)) OR ((owner_kind = 'location'::text) AND (location_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL))))
  • variable_owner_kind_check: CHECK ((owner_kind = ANY (ARRAY['platform'::text, 'component'::text, 'system'::text, 'location'::text])))
  • variable_value_type_check: CHECK ((value_type = ANY (ARRAY['string'::text, 'int'::text, 'float'::text, 'bool'::text, 'json'::text])))
  • variable_component_key_uuid: CREATE UNIQUE INDEX variable_component_key_uuid ON public.variable USING btree (name, component_id) WHERE (owner_kind = 'component'::text)
  • variable_location_key_uuid: CREATE UNIQUE INDEX variable_location_key_uuid ON public.variable USING btree (name, location_id) WHERE (owner_kind = 'location'::text)
  • variable_platform_name: CREATE UNIQUE INDEX variable_platform_name ON public.variable USING btree (name) WHERE (owner_kind = 'platform'::text)
  • variable_system_key_uuid: CREATE UNIQUE INDEX variable_system_key_uuid ON public.variable USING btree (name, system_id) WHERE (owner_kind = 'system'::text)

The other value-bearing tables this page leans on are rendered on their home pages, once each: property_type, the classifier contracts (product_property and siblings), and the property series on samples; tag and tag_binding on tags.