Settings
Included from Basic tier
OpsChain configuration has two authorities:
- Stored settings documents — database-authoritative, versioned JSON
documents owned by nodes: the global document on the system
configuration, and per-node documents down the tree
(project → environment → asset). Edited at runtime via the API, the
GUI,
opschain settingsandopschain admin settings. - Deployment configuration —
config.yaml(plus its environment twins), deploy-authoritative and read-only at runtime. Its key catalog isopschain docs config; this page does not restate it.
Where both speak to the same concern, the deployment side wins:
stored < config file < environment, per key. The worked example is
authentication: OPSCHAIN_TOKEN_SECRET_KEY wins over the stored
token.secret_key setting; the config-file ldap: block overlays the
stored ldap.* settings per key and OPSCHAIN_LDAP_* /
OPSCHAIN_LDAP_PASSWORD win over both; a config-file
enable_basic_auth beats the stored setting and
OPSCHAIN_ENABLE_BASIC_AUTH beats the file. Unset levels fall through —
an overlay never blanks a stored value, it only wins where it is set.
The settings document
A node's stored settings are one settings resource:
| Attribute | Meaning |
|---|---|
data | The document — one JSON object, replaced wholesale on each write. |
version | The current version number; every write creates version n+1 and the chain is immutable history. |
created_at | When this version was written. |
Surfaces:
GET /api/settings/{id}— the document (reached through the owning node'ssettingsrelationship;/api/system_configuration's settings relationship leads to the global document).PATCH /api/settings/{id}— replacedataas a new version. With aversionattribute the write is compare-and-set; a stale version answers the named 422:Version mismatch, the settings are now at version <n> whilst these modifications were made to version <m>. Please fetch the latest version of the settings and reapply the edits.Without one, the write is unchecked (last writer wins).GET /api/settings/{id}/versions— the history, version descending by default (filter[sorts]=version|created_at asc|desc), default limit 1000 (max 10,000), withexclude_data=truedropping each version's data.GET …/versions/{version}reads onesettings_version(data,version,created_at).
Reads require read on the owning node's path; writes require update on it
(404-on-deny). CLI: opschain settings show | history | edit for node
documents (opschain docs cli/settings — edit is a versioned
compare-and-set replace, not a merge), and opschain admin settings show | prune for the global document (opschain docs cli/admin-settings;
prune strips legacy keys the Go server does not read, as a new recoverable
version — opschain docs cli/admin-settings-prune).
The global document
The global settings live on the SystemConfiguration node's settings row —
the base every converge starts from. A fresh installation seeds it with a
welcome banner only; representative keys the Go server reads from it
include token.* (secret key, access/refresh expiry, the v2 session
lane's session_absolute_expiry, cookies), enable_basic_auth,
mfa.elevation_window (minutes, default 15 — the non-sliding step-up
window), login.escape_hatch_label and login.escape_hatch_url (an
optional secondary link the sign-in page renders under the provider
buttons — for example "Prefer not to sign in? Book a guided walkthrough";
both must be set and the URL must be http(s), otherwise the page renders
nothing — opschain docs identity), repo_folder,
chains_max_window_days and the stored licence. The retired auth_strategy/auth.providers chain
selectors and the ldap.* subtree are read only by the
upgrade boot seed and are pruneable afterwards (sign-in ordering is the
per-connection display_order on the identity provider connections —
opschain docs identity). The product_name setting is retired too —
the product name is fixed, a stored key is ignored and pruneable, and
tokens minted under a previously customised value keep validating
(verification never reads the iss claim it once stamped). The authentication stack reads it through a snapshot
re-read at most once per minute, so a stored auth edit takes effect within
that staleness envelope without a restart; other readers (the chain
window, the repo folder, the converge itself) read per request.
Convergence and inheritance
What a run actually sees is the converged view, served at:
GET /api/projects/…<node path>/converged_settings(gated by read on<node path>/converged_settings),GET /api/changes/{id}/converged_settingsandGET /api/steps/{id}/converged_settings(a step resolves to its change).
The merge, in order — nearest node wins because later layers deep-merge over earlier ones:
- the global document,
- each node down the target path in path order (a templated node contributes its template's and template version's settings layers immediately before its own; a node with no settings document contributes nothing),
- for a change: the change's own override settings, deep-merged plain and last.
Two specialisations:
- The legacy
requires_approval_fromkey is retired and ignored. The old node-settings approver entries were never enforced by this server — they described an approval gate that did not exist — so the converge strips the key from every layer (stored entries, overrides and static snapshots alike) and never serves it. Their enforced successor is the change-governance approval policy; convert stored entries withopschain approval-policies convert-settings-approvers(seecli/approval-policies-convert-settings-approversandchange-governance). The step-levelrequires_approval_from:wait gate in workflow and composed-action YAML is a different mechanism: still enforced, superseded by approval policies, converted withopschain approval-policies convert-step-gates(seecli/approval-policies-convert-step-gates). - A change's
static_settingssnapshot replaces the merge entirely when the change captured one at creation — the run is pinned to the settings it was created under.
The served data is the diff against the current global settings
(only entries that are new or differ), with sensitive values
cipher-stacked (encrypted) before serving: the fixed secured fields (ldap.password,
vault.token/username/password/recovery_key, token.secret_key,
pguser, pgpassword, secret_vault_unseal_key) plus any key matching
password|token$|user($|name|_name)|^bundle_ at the top level or one
level down. Already-encrypted values, secret-vault:// references and
blanks pass through untouched — a plaintext credential is never served.
meta.setting_sources.by_setting attributes each leaf to the layer that
supplied it (System Configuration, a named path node, its template or
template version, or Change).
Recorded gaps, deliberate and documented in the source: the
deploy-authoritative half (the old values.yaml ConfigMap overlay) has no
Go-side source in this converge — stored values are served verbatim; and
secret-vault:// references are served as references, not retrieved.
Which settings live at which node level
Any key may be stored at any level — convergence is uniform — but the levels have distinct consumers the code shows:
- Global only: the authentication, token, LDAP, product and licence
keys above;
chains_max_window_days(the chain explorer's window cap). - Any node level, nearest wins:
repo_folder(where git-committed properties live) — the global value is the default root and a node's settings document re-roots the repository properties walk for its own subtree, so a project whose template repository keeps its properties under a different folder (for examplemintpress) overrides it at the project level without moving every other project. - Any node level, nearest wins: the
runnerplacement block —{"runner": {"group": "…", "selectors": {"key": "value" | ["in","set"]}}}— read down the target path's ancestry at enqueue to place the node's runs on the fleet; a selector value is a string (equality) or a list (in-set). Seeopschain docs fleet-queuefor how the requirement is resolved and frozen per entry. - Any node level, nearest wins:
runner.execution_scope("run"|"step") — the executor-scope override, resolved in the samerunnerblock besidegroup/selectors(change-level metadata wins over asset over environment over project; the global document is not consulted forrunnerplacement — the SystemConfiguration node is not an ancestor of any project). Absent, the derived default applies: actions.rb changes (the Ruby lane — only applicable to existing MintPress customers and those using the MintPress SDK or thepacks:mintpressfeature) are run-scoped — one runner and one shared workspace across the run's steps, upstream's single change pod preserved — and so is a declared change whose tree carries any Ruby-lane step (anactions.rb#bridge or amintpress#graft, found by the creation-time census), while pure-YAML declarative/shell-step runs and workflows are step-scoped (fresh workspace per step, any eligible runner)."step"on an actions.rb change trades filesystem continuity for per-step isolation (the upstreampod_per_stepuse case);"run"pins a declarative run to one runner and one workspace. Any other value is refused at write time (change create and settings PATCH alike). Seeopschain docs dispatchfor the run→runner pin this drives. - Any node level, nearest wins:
runner.max_parallel_steps(positive integer, default 1) — the pinned-run concurrency cap, resolved in the samerunnerblock besidegroup/selectors/execution_scope(change-level metadata wins over asset over environment over project, frozen into each step at enqueue; like the rest of therunnerblock, the global document is not consulted). It applies only to run-scoped runs: the run's pinned runner may hold and execute up to this many of the run's steps at once in the shared run workspace, so parallel branches of an actions.rb tree run concurrently the way upstream's change pod ran them — the engine's readiness ordering still decides what is eligible, and the run never spreads past its one runner. 1 keeps today's strictly serial behaviour; a value above 1 needs a protocol-4 runner (older runners stay serial, with a system log line on the step saying so). A zero/negative value is refused at write time. Step-scoped runs ignore it (their parallelism is the fleet). The runner may bound the setting to its own resources with a declared capacity (opschain runner --max-parallel-steps/OPSCHAIN_RUNNER_MAX_PARALLEL_STEPS): effective concurrency is min(this setting, the runner's declaration), with one per-run notice in the step log when the clamp binds — seeopschain docs runnerandopschain docs cli/runner. Seeopschain docs dispatchfor the claim admission andopschain docs runner-executorsfor the shared-workspace contract. - Any node level, nearest wins: the
orchestration.retriessubtree —{"orchestration": {"retries": {"attempts": 2, "backoff": "30s"}}}— the default automatic in-place retry policy for steps declaring noretries:of their own. Resolved global → project → environment → asset at run creation and frozen on the run; a step's ownretries:declaration is most specific.attemptsis the total attempt cap (1–100; 1 = no retries),backoffa duration string; the winning level is audited as each recorded retry'spolicy_source.on_errorbehaviour is deliberately not a setting — whether a failure may be swallowed is a per-step authoring decision. Seeopschain docs expressions. - Any node level, nearest wins:
orchestration.fan_out_limit—{"orchestration": {"fan_out_limit": 100}}— the fan-out expansion cap (engine default 50). Resolved global → project → environment → asset at run creation and frozen on the run; afor_each:expansion past the resolved cap fails the step loudly, naming the count, the cap and the level that supplied it (recorded as the expansion audit'scap_source). Must be at least 1; an out-of-range or malformed value is skipped with a notice. Seeopschain docs expressions. - Any node level, nearest wins:
orchestration.change_depth_limit—{"orchestration": {"change_depth_limit": 3}}— the composition depth cap for child changes created by a YAML action'schange:step (engine default 5). Resolved global → project → environment → asset against the PARENT run's target path at the moment the step is reached; a child whose correlation chain would exceed the cap refuses with a named error identifying the chain and the level that supplied the cap. Must be at least 1; an out-of-range or malformed value is skipped with a notice. Seeopschain learn 2.1. - Change level: the create payload's override settings and the
static_settingssnapshot, both visible in the change's converged view.
Levers, and who owns them
| Lever | Authority | Effect |
|---|---|---|
| A node's settings document | DB-authoritative; update on the owner node's path (PATCH /api/settings/{id}, opschain settings edit, the GUI) | Configures the subtree below the node, nearest node winning per key. |
| The global settings document | DB-authoritative; the system-configuration settings authorisation (opschain admin settings) | The estate-wide base of every converge, and the keys the server itself reads (auth, tokens, LDAP, …). |
config.yaml keys | Deploy-authoritative (opschain docs config) | Per key, wins over the stored value where an overlay exists (auth/LDAP/basic-auth/token secret today); read-only at runtime. |
Environment variables (OPSCHAIN_TOKEN_SECRET_KEY, OPSCHAIN_LDAP_*, OPSCHAIN_ENABLE_BASIC_AUTH, …) | Deploy-authoritative, top of the ladder | Per key, wins over both the config file and the stored value. |
version on a settings PATCH | The caller | Turns the write into a compare-and-set; omit it and the write is unchecked. |
opschain admin settings prune | The operator (admin bearer for the --ldap precondition) | Strips legacy stored keys as a new, recoverable version. |
Failure modes
- A lost edit race is loud, not silent — the versioned compare-and-set surfaces the named version-mismatch 422; re-fetch, re-apply, re-submit. An unversioned PATCH forgoes that protection.
- Nothing is ever lost to an edit: every write is a new version and the history is immutable — any previous document is readable (and a pruned value recoverable) via the versions endpoints.
- A stored secret cannot leak through the converged view: securing runs on every serve, and re-securing an already-encrypted snapshot is a no-op.
- An overlay disagreement is announced, not hidden — e.g. an explicit
token secret that disagrees with the stored
token.secret_keywins, and the boot notice warns that tokens minted elsewhere against the stored secret will fail verification here. - Staleness is bounded: the authentication stack reads stored settings through a one-minute TTL snapshot, so an auth edit may take up to a minute to bite — without any restart.
Related pages
opschain docs settings-reference— the per-key dictionary: every stored key the server reads, every operatorOPSCHAIN_*variable, with types, units, defaults, permitted values and precedence.opschain docs config— the deploy-authoritative key catalog.opschain docs fleet-queue— how therunnersettings block places work, and when it is resolved.opschain docs cli/settingsandopschain docs cli/admin-settings— the CLI surfaces.opschain docs identity— the authentication stack that worked-example ladder configures.