Skip to main content
Version: v5.7.0

Settings

Included from Basic tier

OpsChain configuration has two authorities:

  1. 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 settings and opschain admin settings.
  2. Deployment configuration — config.yaml (plus its environment twins), deploy-authoritative and read-only at runtime. Its key catalog is opschain 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:

AttributeMeaning
dataThe document — one JSON object, replaced wholesale on each write.
versionThe current version number; every write creates version n+1 and the chain is immutable history.
created_atWhen this version was written.

Surfaces:

  • GET /api/settings/{id} — the document (reached through the owning node's settings relationship; /api/system_configuration's settings relationship leads to the global document).
  • PATCH /api/settings/{id} — replace data as a new version. With a version attribute 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), with exclude_data=true dropping each version's data. GET …/versions/{version} reads one settings_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_settings and GET /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:

  1. the global document,
  2. 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),
  3. for a change: the change's own override settings, deep-merged plain and last.

Two specialisations:

  • The legacy requires_approval_from key 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 with opschain approval-policies convert-settings-approvers (see cli/approval-policies-convert-settings-approvers and change-governance). The step-level requires_approval_from: wait gate in workflow and composed-action YAML is a different mechanism: still enforced, superseded by approval policies, converted with opschain approval-policies convert-step-gates (see cli/approval-policies-convert-step-gates).
  • A change's static_settings snapshot 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 example mintpress) overrides it at the project level without moving every other project.
  • Any node level, nearest wins: the runner placement 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). See opschain docs fleet-queue for 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 same runner block beside group/selectors (change-level metadata wins over asset over environment over project; the global document is not consulted for runner placement — 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 the packs:mintpress feature) 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 (an actions.rb# bridge or a mintpress# 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 upstream pod_per_step use 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). See opschain docs dispatch for 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 same runner block beside group/selectors/execution_scope (change-level metadata wins over asset over environment over project, frozen into each step at enqueue; like the rest of the runner block, 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 — see opschain docs runner and opschain docs cli/runner. See opschain docs dispatch for the claim admission and opschain docs runner-executors for the shared-workspace contract.
  • Any node level, nearest wins: the orchestration.retries subtree — {"orchestration": {"retries": {"attempts": 2, "backoff": "30s"}}} — the default automatic in-place retry policy for steps declaring no retries: of their own. Resolved global → project → environment → asset at run creation and frozen on the run; a step's own retries: declaration is most specific. attempts is the total attempt cap (1–100; 1 = no retries), backoff a duration string; the winning level is audited as each recorded retry's policy_source. on_error behaviour is deliberately not a setting — whether a failure may be swallowed is a per-step authoring decision. See opschain 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; a for_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's cap_source). Must be at least 1; an out-of-range or malformed value is skipped with a notice. See opschain 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's change: 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. See opschain learn 2.1.
  • Change level: the create payload's override settings and the static_settings snapshot, both visible in the change's converged view.

Levers, and who owns them​

LeverAuthorityEffect
A node's settings documentDB-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 documentDB-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 keysDeploy-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 ladderPer key, wins over both the config file and the stored value.
version on a settings PATCHThe callerTurns the write into a compare-and-set; omit it and the write is unchecked.
opschain admin settings pruneThe 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_key wins, 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.
  • opschain docs settings-reference — the per-key dictionary: every stored key the server reads, every operator OPSCHAIN_* variable, with types, units, defaults, permitted values and precedence.
  • opschain docs config — the deploy-authoritative key catalog.
  • opschain docs fleet-queue — how the runner settings block places work, and when it is resolved.
  • opschain docs cli/settings and opschain docs cli/admin-settings — the CLI surfaces.
  • opschain docs identity — the authentication stack that worked-example ladder configures.