Operate runtime feature flags (operator runbooks)¶
These runbooks cover the operator side of runtime feature flags: binding an environment, importing the legacy policy lineage, enrolling API and Project Management (PM), running the workload collector and the deployment coordinator, setting the expected inventory, changing, stopping and restoring a flag, recovery, and the return to Legacy. They are slice 6C.1 of the remaining delivery plan.
The feature behaviour, endpoint contracts and design history are in
Managing Feature Flags. This guide does not repeat them. Where the two seem to
differ, the code on main decides, and the difference is listed in Known gaps.
Related plans:
- Staging rehearsal plan and production prerequisites (6C.3, 6C.4);
- Isolated rehearsal checklist (6C.2, run in the local stack on 2026-10-22).
Status and boundary¶
NOT EXECUTED in staging or production. None of these runbooks has been run against staging or production. Nothing in this repository binds, imports, enrols or activates an environment. A merged PR is not evidence that an environment is ready. The isolated rehearsal (6C.2) ran them in a local E2E stack on 2026-10-22; its outcomes are in the isolated rehearsal checklist.
- Production candidate (decision D2).
disableMembershipis the first and only production candidate. It is the only key whose consumer contract isproductionEligible: true,grain: Global,safeValue: false(src/charts/syrf-common/env-mapping.yaml;RuntimeProductionPolicy.Eligible). Statistics keys stay non-production. - Deploy API and PM together (decision D4). See Mixed-version window.
- Rehearsals (decision D1). A rehearsal beyond the local stack may use a throwaway PR preview environment. Staging and production remain written plans until Chris approves each step.
- Production prerequisite. Production enables and restores need
RuntimeFeatureFlags:FleetInventoryMode=Collectorand a running collector (runbook 4). Since #4407 (server-time retirement, merged as9174f9e9), production admission, the fleet view and deployment completion follow the configured inventory mode. The default is stillStatic, which keeps every old pod's acknowledgement row: after any Kubernetes rollout those rows stop the fleet reaching Active, so production enables and restores are refused in Static mode. The remaining prerequisite is operational: set Collector mode and keep the collector publishing. - Forward only. Deployments and data only move forward (ADR-023). A rollback is a planned replacement release. Every "roll-forward" column in the plans means a new reviewed change, never an undo.
- GitOps only. Configuration changes are cluster-gitops PRs. Never use
kubectl apply,helm installor direct database edits.
Before you start¶
Placeholders¶
| Placeholder | Meaning |
|---|---|
<env> |
Binding environment ID: production, staging, preview:pr-<n> or local:<stack> |
<runtime-env> |
The RuntimeEnvironment the binding runs as: the part of <env> before the colon |
<database> |
The configured MongoDB database name |
<cluster-fingerprint> |
The live cluster fingerprint that status prints, for example <setName>@<hostSuffix> |
<binding-epoch> |
The binding's epoch GUID, from the bind log line or the authority identity |
<api-origin> |
The API's HTTPS origin, with no path |
<identity-origin> |
The Identity service origin (OpenIddict environments only) |
<admin-session> |
An authenticated real administrator: browser session or user bearer token. Never a machine token |
<report-token>, <collector-token>, <coordinator-token> |
Machine access tokens, one scope each (runbooks 4 and 5) |
<key> |
The flag key. For production this is always disableMembership |
<identity> |
The complete policy identity object, copied unchanged from authority-status (below) |
<mutation-id> |
A new GUID per intended change. Reuse it only to retry the same request |
Never put real values, tokens or secrets in this document, a PR description or a log excerpt.
How settings reach a host¶
The .NET hosts read SYRF__-prefixed environment variables, with __ meaning :. So the setting
RuntimeFeatureFlags:Mode is the variable SYRF__RuntimeFeatureFlags__Mode.
Only two groups of these settings are chart values:
migrations.enabledandmigrations.binding.*(API and PM charts), rendered into the migration Jobs asSYRF_MIGRATION_BINDING_EXPECTED_*;runtimePolicyHostMetadata.enabled(API and PM charts), which rendersInstanceUid,ImageVersion,ExpectedReplicasandAutoscaledfrom the Deployment.
Every other RuntimeFeatureFlags:* setting in these runbooks has no env-mapping.yaml entry. It reaches the
host only as a raw env: entry in the service's cluster-gitops values. Raw env: entries are literal
strings (_deployment-dotnet.tpl), so they must never carry a secret.
TO CONFIRM: whether reviewed chart values should replace these raw env: entries before production.
The policy identity¶
Every policy request carries the complete current identity. Read it from the API, then copy it unchanged:
Expected (Shared mode, healthy):
{
"mode": "Shared",
"host": "api",
"fleetScope": "local-process",
"state": "Ready",
"identity": {
"environmentId": "<env>",
"bindingEpoch": "<binding-epoch>",
"storeEpoch": "<store-epoch>",
"revision": "<revision>",
"catalogDigest": "<catalog-digest>",
"baselineDigest": "<baseline-digest>",
"policyDigest": "<policy-digest>"
},
"observedAtUtc": "<time>",
"lastAttemptAtUtc": "<time>",
"reason": null
}
revision is a decimal string. state is one of Ready, Uninitialized, Stale (MaximumAgeExceeded),
Unavailable (AuthorityNotInitialized, AuthorityReadFailed), Legacy, Quarantined (InvalidBinding,
AuthorityNotInitialized, IdentityOrRevisionRegression) or, in Legacy mode, Disabled with reason
LegacyMode, or LegacyModeAuthorityPreserved once that Legacy process has found an initialized authority
(runbook 10). This is one API process's view, not the fleet's.
Retry rules¶
- A 503 with
runtime_policy_commit_unknown, a lost response or a timeout does not prove failure. Retry the same mutation ID and the unchanged request, or look the outcome up (R7.7). - Never mint a new mutation ID because an outcome is unknown.
- A 409
runtime_policy_revision_conflictmeans the identity moved. Read it again and decide again.
Exit codes of the operator commands¶
| Command | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
Migration entry point (SYRF_MIGRATION_MODE) |
Success | Failure (verify problems) |
Usage or binding refusal; nothing written | Lease or writer wait timed out |
Lineage command (SYRF_RUNTIME_POLICY_MODE) |
Artifact written | Unexpected failure; retry the identical reviewed plan | Refused (Runtime policy lineage refused: …) |
n/a |
Historical import (SYRF_RUNTIME_RELEASE_HISTORY_MODE) |
Artifact written | Unexpected failure; retry the identical reviewed preview | Refused (Historical release import refused: …) |
n/a |
scripts/capture-runtime-workload.py |
Capture written | n/a | Capture refused: … |
n/a |
scripts/publish-runtime-workload.py |
Receipt matched | HTTP 3xx/4xx refusal (not 408/425/429) | Local refusal | Outcome unknown; replay the exact capture |
scripts/runtime-compatibility-check.py |
pass |
refuse |
insufficient-evidence |
unusable-input |
Mixed-version window (D4)¶
When application authority is enforced, an API that asks an older PM about the operation-specific runtime
capabilities gets a retryable 503 on the flag-admin endpoints until both run the same version
(#4403, from #4354). The 503 is ProblemDetails with
code: "authority-unavailable" and a Retry-After header. So:
- promote and sync API and PM at the same build, in one change;
- do not start any runbook step that calls an admin route until both services report the same image;
- treat a 503
authority-unavailableduring a rollout as the expected window, not a failed step.
ADR-023 also expects API and PM to run different builds briefly during a rollout. Migrations stay additive for that window.
Evidence store¶
Keep every artifact named in "Evidence to capture" with the run record (see the approval record template). Preview and receipt files are review evidence: never edit or reformat them.
Runbook order¶
| # | Runbook | Writes to the environment |
|---|---|---|
| 1 | Environment binding | Binding record (once) |
| 2 | Lineage preview and apply with writer freeze | Authority document and receipt |
| 3 | Consumer enrolment | Deployment configuration |
| 4 | Collector identity and scope | Workload observations |
| 5 | Coordinator identity | Deployment constraints |
| 6 | Expected inventory and Collector mode | Deployment configuration |
| 7 | Change one flag | Policy revision and receipt |
| 8 | Emergency stop and restore | Policy revision and receipt |
| 9 | Recovery and rebind | Depends on the case |
| 10 | Legacy return | Deployment configuration |
| 11 | Historical release import (written only) | One release record and one event |
Runbooks 1 to 3 run once per environment, in that order. Runbook 2 needs a full API outage (no API process may be registered), and runbook 3 must follow it before the outage ends: an API in Legacy mode starts against an imported authority but serves only the deployed values (runbook 10), so the saved runtime policy applies only once runbook 3 has enrolled the hosts.
Runbook 4 is numbered after runbook 3 but must be working before runbook 2 starts. Runbook 3 sets Collector mode, and a Collector-mode fleet is Active only while both services have observations younger than 30 seconds. So configure the collector (R4.1–R4.2) and prove one capture and publication per service (R4.3–R4.5) first, then keep publishing from the moment the API is back (R3.4).
1. Environment binding¶
Purpose¶
Bind the database once to one environment ID, database name and cluster fingerprint. Migrations 0009
to 0021 declare RequiresBinding, and every runtime authority read and write checks the binding.
Preconditions¶
- The service images contain migrations
0009–0021(checkmigrations.lock.jsonat the image's commit). - The ADR-023 pre-deploy migration Job is available in the API and PM charts (
migrations.enabled). - For staging: Chris has approved the step (owner gate, plan §8). For production: Chris approves the specific cluster-gitops PR.
- No binding exists yet, or the existing one is identical.
bindnever overwrites a different binding. - PR previews: previews do not run
uporbindtoday. Their database lifecycle Job runs index-init only, and the migration Job refusespreview.waitForDatabase. TO CONFIRM: how a D1 preview rehearsal getsbindandup.
Steps¶
- R1.1 Read the live fingerprint. Run the API or PM image once with:
SYRF_MIGRATION_MODE=status
SYRF_MIGRATION_BINDING_EXPECTED_ENVIRONMENT=<env> # optional on this first run
SYRF_MIGRATION_BINDING_EXPECTED_DATABASE=<database> # optional
SYRF_MIGRATION_BINDING_EXPECTED_CLUSTER=<cluster-fingerprint> # optional
TO CONFIRM: no chart template renders a status Job. In the local stack e2e/scripts/pm-migrations.sh
runs it. For staging and production, the reviewed way to run a one-off status (and verify) is not
defined.
2. R1.2 Prepare the cluster-gitops change for both the API and the PM service values:
migrations:
enabled: true
binding:
bind: true
expectedEnvironment: "<env>"
expectedDatabase: "<database>"
expectedCluster: "<cluster-fingerprint>"
Copy expectedCluster from R1.1. Both charts need the three expected values, because each chart's up
Job reads them when a RequiresBinding migration is pending. bind: true adds a PreSync bind Job in
sync wave −1, ahead of up. A second identical bind is a no-op. Whether bind stays set after the first
bind is open (#4263).
3. R1.3 Review the rendered Jobs in the cluster-gitops PR diff: one bind Job and one up Job per
chart, each with the three SYRF_MIGRATION_BINDING_EXPECTED_* variables.
4. R1.4 Merge with the required approval and let ArgoCD sync. Do not sync API and PM at different builds
(D4).
5. R1.5 Verify with SYRF_MIGRATION_MODE=verify (same TO CONFIRM as R1.1), or, once reports are enabled
(runbook 4), with the report's migrations prerequisite.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
R1.1 status, no binding yet |
Environment binding: none in database <database>; live cluster fingerprint <cluster-fingerprint> and, without expected values, Environment binding: no SYRF_MIGRATION_BINDING_EXPECTED_* value is set; nothing compared. Exit 0 |
R1.1 status with expected values set |
Environment binding matches the expected environment '<env>', database '<database>', cluster '<cluster-fingerprint>', or one Environment binding mismatch: … warning per problem. Exit 0 either way |
R1.4 bind Job log |
Environment bound: <env>, database <database>, cluster <cluster-fingerprint>, epoch <binding-epoch>, announcement delivery <mode>, where <mode> is production, testSink (staging) or disabled (previews, local). On a repeat: Environment binding already in place: …; nothing changed. Exit 0 |
R1.4 up Job log |
Environment binding verified: <env>, database <database>, cluster <cluster-fingerprint>, epoch <binding-epoch>, then each migration applied. Exit 0 |
R1.5 verify |
Verify: every migration this binary knows is applied and every declared index exists. Exit 0 |
Evidence to capture¶
- The
statusoutput from R1.1 (fingerprint). - The cluster-gitops PR URL and its approval.
- The
bindandupJob logs, including the binding epoch. - The
verifyoutput, or a report whosemigrationsprerequisite isReady.
If a step is refused¶
Every binding refusal exits 2 and writes nothing. For up there is no partial apply; the previous release
keeps serving because the PreSync phase fails.
Message (prefix Bind refused: or Up refused before applying anything (…):) |
Action |
|---|---|
SYRF_MIGRATION_BINDING_EXPECTED_… is not set; … are all required. |
Set all three values |
SYRF_MIGRATION_BINDING_EXPECTED_ENVIRONMENT must be production, staging, preview:pr-<number> or local:<stack> (got '…') |
Fix the environment ID |
the configured RuntimeEnvironment '…' does not match the expected environment '…' |
The values file renders the wrong environment. Stop and investigate; never change the expected value to fit |
the configured database '…' does not equal the expected database '…' |
Same as above |
the live cluster fingerprint '…' does not equal the expected cluster '…' |
Wrong cluster, or the cluster changed. See runbook 9 |
database '…' is already bound to …; a different binding is never overwritten. |
Runbook 9. Do not edit the record |
database '…' has no environment binding; run SYRF_MIGRATION_MODE=bind first. |
Add the bind Job (R1.2) |
SYRF_MIGRATION_MODE must be one of status, up, down, verify, bind (got '…') |
Fix the mode |
| Exit 3 | The migration lease or writer wait timed out. Find the other runner before retrying |
2. Lineage preview and apply with writer freeze¶
Purpose¶
Turn the legacy runtimeFeatureFlagOverrides document into a bound runtime authority with a reviewed
baseline, keeping the legacy revision and overrides exactly. This is an operator command in the API image,
separate from migrations and start-up. It does not enrol any consumer.
Two branches. Where the legacy document exists, import it (R2.1–R2.7 below). Where no global flag document exists at all, as in production, use Initialise (no legacy document) instead: it creates the authority from the deployed values (owner decision D8=A). Check which applies with the first preflight of each branch. The two never mix: import refuses a missing document and initialise refuses any document.
Preconditions¶
- Runbook 1 is complete and the binding matches.
- Migrations
0009and0010are applied with their expected definitions. The command never creates indexes. - The legacy policy document exists (
_id: "global"). Where there is none (production: legacy mutation is disabled there), do not create one: use Initialise (no legacy document). syrf_metadatahas a well-formedsyrf-apiinstance registry.- The image used for the command is the API build that will serve the imported authority. The catalog digest is part of the identity; a preview made with another catalog is rejected later.
- The consumer enrolment change (runbook 3) is reviewed and ready to merge straight after apply.
- Approval: Chris for staging and production. D1 preview rehearsals follow the staging plan.
- TO CONFIRM: no chart template runs the lineage command in a cluster, and none mounts the reviewed input or keeps the output artifact. The delivery mechanism for staging and production is not defined.
Steps¶
- R2.1 Write the reviewed baseline JSON. It needs a Boolean for every key of the generated catalog
(54 keys on
main), plusapprovalandprovenancetext:
{
"values": { "<key-1>": false, "<key-2>": true },
"approval": "<reviewed baseline decision reference>",
"provenance": "<source configuration and report identities used to reconcile it>"
}
List the keys with:
grep -oE '^ new\("[A-Za-z0-9]+", "' \
src/libs/mongo/SyRF.Mongo.Common/RuntimeFeatureFlags/RuntimeFeatureFlagCatalog.generated.cs \
| sed -E 's/^ +new\("([A-Za-z0-9]+)".*/\1/'
The keys are the first argument of each new("<key>", …) entry in that generated file, which is the
catalog's key list; read it there if grep is unavailable.
Reconcile each value against the rendered values of every consuming service. The command never takes
the API's current settings as the baseline.
2. R2.2 Preview (read-only). Run the API image's normal entry point once with the usual Mongo
configuration and RuntimeEnvironment=<runtime-env>, plus:
SYRF_RUNTIME_POLICY_MODE=preview
SYRF_RUNTIME_POLICY_INPUT=/reviewed/baseline.json
SYRF_RUNTIME_POLICY_OUTPUT=/reviewed/lineage-preview.json
SYRF_MIGRATION_BINDING_EXPECTED_ENVIRONMENT=<env>
SYRF_MIGRATION_BINDING_EXPECTED_DATABASE=<database>
SYRF_MIGRATION_BINDING_EXPECTED_CLUSTER=<cluster-fingerprint>
SYRF_MIGRATION_MODE must not be set at the same time.
3. R2.3 Review the preview file. Check kind: legacyLineageImport, planVersion: 1, sourceRevision
(equal to the legacy revision), target.environmentId = <env>, target.bindingEpoch = <binding-epoch>, a
new target.storeEpoch, the baseline values and previewDigest. It is Canonical Extended JSON: do not
reformat it. Numbers carry their type wrapper, so planVersion reads {"$numberInt": "1"} and
target.revision {"$numberLong": "<revision>"}; sourceRevision is a decimal string; the baseline values are
in target.baseline.
4. R2.4 Freeze writers. Start the freeze recorded in the approval: no cluster-gitops merges for this
environment except this procedure, no auto-promotion, no autoscaling. Scale the API to zero replicas
through cluster-gitops and wait until every API pod has terminated. TO CONFIRM: the freeze mechanism for
staging's auto-merged promotion PRs.
5. R2.5 Apply the exact reviewed preview with the same image and binding values:
SYRF_RUNTIME_POLICY_MODE=apply
SYRF_RUNTIME_POLICY_INPUT=/reviewed/lineage-preview.json
SYRF_RUNTIME_POLICY_OUTPUT=/reviewed/lineage-receipt.json
SYRF_RUNTIME_POLICY_ACTOR=<operator identity>
SYRF_RUNTIME_POLICY_WRITER_FREEZE_ACK=<freeze reference from the approval record>
SYRF_MIGRATION_BINDING_EXPECTED_ENVIRONMENT=<env>
SYRF_MIGRATION_BINDING_EXPECTED_DATABASE=<database>
SYRF_MIGRATION_BINDING_EXPECTED_CLUSTER=<cluster-fingerprint>
- R2.6 If the outcome is unknown (exit 1, lost output), rerun R2.5 with the identical preview and a new output path. A committed import returns its original receipt without writing again.
- R2.7 Go straight to runbook 3, then end the freeze. An API restarted in Legacy mode would start, but it
serves only the deployed values and refuses legacy changes (runbook 10); the imported policy applies only
in Shared mode. An older image (before #4414) instead refuses to start against the imported authority, and it
writes its
syrf-apiwriter-registry entry (DatabaseMetadataWriter) before it refuses, so a refused start still leaves an entry behind (seen in the 6C.2 rehearsal).
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| Before R2.2: legacy document | db.runtimeFeatureFlagOverrides.findOne({_id: "global"}, {_id: 1, revision: 1}) returns a document with no storeEpoch field (read-only query) |
| Before R2.5: writer registry | db.syrf_metadata.findOne({_id: "meta"}, {"services.syrf-api.instances": 1}) shows instances: {}. Apply checks this itself, twice |
| Before R2.5: no API pods | The API Deployment reports 0 ready and 0 total pods |
| R2.2 log | Runtime policy lineage preview completed; artifact: /reviewed/lineage-preview.json. No consumer was enrolled. Exit 0 |
| R2.5 log | Runtime policy lineage apply completed; artifact: /reviewed/lineage-receipt.json. No consumer was enrolled. Exit 0 |
| R2.5 receipt | kind: legacyLineageImport, the preview's mutationId and previewDigest, environmentId, bindingEpoch, storeEpoch, revision equal to the source revision, actor, writerFreezeAcknowledgement, recordedAtUtc |
A registry entry from a pod that did not shut down cleanly stays until a later API start prunes entries older
than 24 hours (DatabaseMetadataWriter). There is no bypass. The one entry the freeze does not count is a
Legacy API that has already found an initialized authority: it marks its own registration
runtimePolicyReadOnly (bound to that registration's connectedAt) because it can never write runtime policy
again (runbook 10). Every other entry, including one from an older image, is counted. TO CONFIRM: the approved way to clear a stale
entry younger than 24 hours, other than waiting.
Evidence to capture¶
- The baseline JSON, its approval and provenance references.
- The preview file and its
previewDigest. - The freeze reference, start and end times, and proof of zero API pods.
- The receipt file and the command logs (no secrets).
If a step is refused¶
Refusals exit 2 with Runtime policy lineage refused: <message>. Nothing is written.
| Message | Action |
|---|---|
SYRF_RUNTIME_POLICY_MODE must be preview or apply. / … is required. |
Fix the invocation |
Lineage import and schema migration modes cannot be combined. |
Unset SYRF_MIGRATION_MODE |
Input and output paths must differ; preserve the reviewed input. / Output already exists; choose a new receipt/preview path. |
Use a new output path |
Input must be an existing JSON file of at most 1 MiB. |
Fix the input path or file |
Lineage import requires an exact verified environment/database/cluster binding: … |
Runbook 1, or runbook 9 |
Missing legacy policy; lineage import cannot invent an initial state. |
Initialise the legacy state with a reviewed edit first |
Legacy source has unknown fields; review its schema before importing. |
Stop; an unknown legacy shape needs review (it may already be imported) |
API writer registry is missing or malformed; absence of evidence is not a writer freeze. |
Investigate the registry; do not create it by hand without approval |
API writers remain registered. Stop/freeze all legacy writers before lineage import; there is no bypass switch. |
Finish R2.4 |
Required migration-owned audit index is missing or incompatible: … |
Apply 0009/0010 (runbook 1) |
The reviewed lineage preview was modified or has an unsupported version. |
Make and review a new preview |
The reviewed lineage preview belongs to a different environment binding. / Environment binding changed after preview. |
Make a new preview against the current binding |
Legacy source changed after preview; obtain and review a new preview. |
A writer changed the policy: the freeze failed. New preview |
Lineage import must preserve the exact legacy revision and overrides. / Legacy source audit references changed after preview. |
New preview |
Mutation ID was already used with a different lineage plan. |
Never reuse a preview's mutation ID for a different plan |
Too many ambiguous source audit references; review the legacy history before importing. |
Stop and review the audit history |
Initialise (no legacy document)¶
Purpose. Create the first bound runtime authority where no global flag document exists. The baseline is
the deployed values: the FeatureFlags configuration the API image renders for this command, for every
catalog key. The authority starts at revision 0 with no overrides and no stops. Production uses this branch
instead of the import (D8=A): legacy mutation is disabled there, so it has nothing to import. The command, its
digest discipline, writer freeze, receipts and output path rules are the import's; only the source differs.
Preconditions. As for the import, except:
- No global document exists, legacy or initialized. The command refuses either.
- Every migration the image knows is applied and every declared index exists (
verifyis clean; runbook 1). The command runs the same read-only check and refuses otherwise. It never creates indexes. - The command runs with the rendered values the API will serve: the same image and the same
FeatureFlagssettings as the enrolled API (runbook 3). Reconcile them with Project Management's rendered values before review. The command reads only the API's. - The same image and settings run preview and apply. Apply reads the deployed values again and refuses if they changed.
Steps.
- R2.I1 Write the reviewed input. Only
approvalandprovenance; the baseline is never supplied by hand:
{
"approval": "<owner approval reference for the initial baseline>",
"provenance": "<rendered values (API and PM) reconciled for this baseline>"
}
- R2.I2 Preview (read-only). As R2.2, plus
SYRF_RUNTIME_POLICY_SOURCE=initialize:
SYRF_RUNTIME_POLICY_MODE=preview
SYRF_RUNTIME_POLICY_SOURCE=initialize
SYRF_RUNTIME_POLICY_INPUT=/reviewed/initialise-input.json
SYRF_RUNTIME_POLICY_OUTPUT=/reviewed/initialise-preview.json
SYRF_MIGRATION_BINDING_EXPECTED_ENVIRONMENT=<env>
SYRF_MIGRATION_BINDING_EXPECTED_DATABASE=<database>
SYRF_MIGRATION_BINDING_EXPECTED_CLUSTER=<cluster-fingerprint>
The preview is deterministic: the same input, image, values, binding and migrations always give a
byte-identical file (the store epoch and mutation ID are derived from them). Run it twice to two paths and
compare (cmp) to prove nothing varies.
3. R2.I3 Review the preview file. Check kind: deployedValueInitialization, planVersion: 1,
source: deployedValues, migrationCatalogueDigest, target.revision: 0, target.overrides: {},
target.killSwitches: {}, target.environmentId = <env>, target.bindingEpoch = <binding-epoch>,
target.storeEpoch, previewDigest, and every value in target.baseline against the reconciled
rendered values. Do not reformat it.
4. R2.I4 Freeze writers. As R2.4.
5. R2.I5 Apply the exact reviewed preview with the same image, values and binding:
SYRF_RUNTIME_POLICY_MODE=apply
SYRF_RUNTIME_POLICY_SOURCE=initialize
SYRF_RUNTIME_POLICY_INPUT=/reviewed/initialise-preview.json
SYRF_RUNTIME_POLICY_OUTPUT=/reviewed/initialise-receipt.json
SYRF_RUNTIME_POLICY_ACTOR=<operator identity>
SYRF_RUNTIME_POLICY_WRITER_FREEZE_ACK=<freeze reference from the approval record>
SYRF_MIGRATION_BINDING_EXPECTED_ENVIRONMENT=<env>
SYRF_MIGRATION_BINDING_EXPECTED_DATABASE=<database>
SYRF_MIGRATION_BINDING_EXPECTED_CLUSTER=<cluster-fingerprint>
- R2.I6 If the outcome is unknown, rerun R2.I5 with the identical preview and a new output path. A committed initialise returns its original receipt without writing, even after the API is back. Any other preview is refused once the authority exists.
- R2.I7 As R2.7: go straight to runbook 3, then end the freeze.
Preflight checks and expected output.
| Check | Expected output |
|---|---|
| Before R2.I2: no global document | db.runtimeFeatureFlagOverrides.countDocuments({_id: "global"}) returns 0 (read-only query) |
| Before R2.I2: migrations | SYRF_MIGRATION_MODE=verify prints Verify: every migration this binary knows is applied and every declared index exists, exit 0 |
| Before R2.I5: writer registry, no API pods | As for the import (R2.5 rows above) |
| R2.I2 log | Runtime policy initialise preview completed; artifact: /reviewed/initialise-preview.json. No consumer was enrolled. Exit 0 |
| R2.I2 repeated | A second preview to another path is byte-identical (cmp prints nothing, exit 0) |
| R2.I5 log | Runtime policy initialise apply completed; artifact: /reviewed/initialise-receipt.json. No consumer was enrolled. Exit 0 |
| R2.I5 receipt | kind: deployedValueInitialization, _id and mutationId equal to the preview's mutationId, the preview's previewDigest, source: deployedValues, migrationCatalogueDigest, sourceOverrides: {}, baseline, environmentId, bindingEpoch, storeEpoch, revision: 0, actor, writerFreezeAcknowledgement, recordedAtUtc |
| After R2.I5 | A new preview is refused with An initialized runtime policy authority already exists; initialise never replaces it. (exit 2) |
| After runbook 3 | authority-status is Shared/Ready at the receipt's store epoch and revision 0 |
Evidence to capture. The input file, both preview files and the cmp result, the reconciled rendered
values, the freeze reference and zero-pod proof, the receipt, and the command logs (no secrets).
If a step is refused. Refusals exit 2 with Runtime policy lineage refused: <message>. Nothing is written.
The import's invocation, path, binding and writer messages (table above) apply unchanged. Initialise adds:
| Message | Action |
|---|---|
SYRF_RUNTIME_POLICY_SOURCE must be import or initialize when set. |
Fix the invocation |
A legacy runtime policy document already exists; initialise never replaces it. Import it with the lineage import (SYRF_RUNTIME_POLICY_SOURCE unset) instead. |
Use the import branch |
An initialized runtime policy authority already exists; initialise never replaces it. |
Stop: the environment is already initialised. If this was a retry, use the identical preview (R2.I6) |
Migrations are not Ready; apply every migration this binary knows before initialising: … |
Runbook 1 (up, then verify) |
Initialise needs this binary's migration catalogue to prove that migrations are Ready. |
Use the API image's entry point; the catalogue is built in |
Initialise input takes only approval and provenance; the baseline is always the deployed values. |
Remove values (and anything else) from the input |
Deployed values changed after preview; obtain and review a new preview. |
The apply ran with other FeatureFlags values than the preview: use the reviewed image and values, or make a new preview |
The migration catalogue changed after preview; obtain and review a new preview. |
The image changed between preview and apply: new preview |
The reviewed plan was made for another lineage source; use the SYRF_RUNTIME_POLICY_SOURCE it was previewed with. |
Set SYRF_RUNTIME_POLICY_SOURCE to match the preview |
An initialise plan must create revision 0 from the deployed values, with no overrides and no stops. |
The preview was altered: discard it, make a new one |
3. Consumer enrolment¶
Purpose¶
Switch API and PM to the bound runtime authority (RuntimeFeatureFlags:Mode=Shared) with deployment-owned
host metadata, so that their decisions and acknowledgements follow the imported policy.
Preconditions¶
- Runbook 2 is complete, and the freeze from R2.4 is still held with the API at zero.
- API and PM images are the same build (D4), and that build matches the catalog in the receipt.
- Migration
0011is applied (acknowledgement index). - Runbook 4 is ready: the API's collector settings (R4.1) are deployed, a token can be obtained (R4.2), and one capture and publication per service (R4.3–R4.5) has succeeded. R3.1 sets Collector mode, so the fleet can only be Active while both services have fresh observations (younger than 30 seconds).
- An operator is ready to run the manual capture-and-publish loop (R3.4) as soon as the API is back. While the API is at zero nothing can be published, because the observations endpoint is on the API.
- Approval for the cluster-gitops change.
- Staging only: staging values may set
SYRF__RuntimeFeatureFlags__SharedReaderModeon API and PM (they did in cluster-gitops on 2026-10-17). Shared mode requiresDisabled, so the same change must set it.
Steps¶
- R3.1 Prepare one cluster-gitops change for API and PM. Shape for the API service values (PM is the same without the API-only lines):
replicaCount: <api-replicas> # fixed, positive
hpa:
enabled: false # an autoscaled fleet is always Unknown
runtimePolicyHostMetadata:
enabled: true
env:
SYRF__RuntimeFeatureFlags__Mode: "Shared"
SYRF__RuntimeFeatureFlags__SharedReaderMode: "Disabled"
SYRF__RuntimeFeatureFlags__Binding__Environment: "<env>"
SYRF__RuntimeFeatureFlags__Binding__Database: "<database>"
SYRF__RuntimeFeatureFlags__Binding__Cluster: "<cluster-fingerprint>"
# Production only, API and PM:
SYRF__RuntimeFeatureFlags__ProductionPolicyMode: "Runtime"
# API only (runbook 6):
SYRF__RuntimeFeatureFlags__FleetExpectedReplicas__api: "<api-replicas>"
SYRF__RuntimeFeatureFlags__FleetExpectedReplicas__project-management: "<pm-replicas>"
SYRF__RuntimeFeatureFlags__FleetInventoryMode: "Collector"
In production, Mode: Shared with ProductionPolicyMode: Legacy (the default) resolves to Legacy, so both
are needed. Production also requires RuntimeEnvironment and ActiveEnvironment to be production.
2. R3.2 Review the rendered Deployments in the PR diff: four SYRF__RuntimeFeatureFlags__ host metadata
entries from the chart, and the raw env: entries above. Raw env: cannot override the four host
metadata entries.
3. R3.3 Merge and sync API and PM together, restoring the API replica count. This ends the API outage.
4. R3.4 Resume publishing, then check every host (preflight below), then end the freeze. No OD-23
scheduler exists yet, so run the documented capture and publication (R4.3–R4.4) for both services by hand,
in a loop whose full cycle, including the pause, stays under 30 seconds: each observation must be under 30 s old when the fleet is evaluated. Time one capture and publication per service before relying on the loop. If both together take more than about 20 s, drop the pause or run one loop per service. The previous IDs are the last accepted observation IDs from R4.5. --expected-observation-id is required here; only a service's first-ever publication omits it (R4.3). Shape (bash, one terminal per environment):
#!/usr/bin/env bash
set -euo pipefail
declare -A previous=([api]="<last-api-id>" [project-management]="<last-pm-id>")
declare -A deployment=([api]="<api-deployment>" [project-management]="<pm-deployment>")
declare -A container=([api]="<api-container>" [project-management]="<pm-container>")
while true; do
for service in api project-management; do
out="observations/${service}-$(date -u +%Y%m%dT%H%M%SZ).json"
python3 scripts/capture-runtime-workload.py --context "<reviewed-context>" --namespace "<namespace>" \
--service "$service" --deployment "${deployment[$service]}" --container "${container[$service]}" \
--environment "<env>" --binding-epoch "<binding-epoch>" \
--expected-observation-id "${previous[$service]}" --output "$out"
receipt="$(SYRF_RUNTIME_COLLECTOR_TOKEN="<collector-token>" python3 scripts/publish-runtime-workload.py \
--api-origin "<api-origin>" --environment "<env>" --binding-epoch "<binding-epoch>" \
--service "$service" --input "$out")"
previous[$service]="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["observationId"])' "$receipt")"
done
sleep 2 # counts towards the 30 s budget
done
The loop stops on the first non-zero exit. On publish exit 3, replay the same $out file before capturing
again (R9.3); on exit 1 or 2, fix the cause (runbook 4 refusals) and restart with the last accepted IDs.
Tokens expire: obtain a new one (R4.2) before it does. Never put the token in a file or in shell history.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| API start-up | No InvalidOperationException from authority options (see refusals). /health/live healthy on every pod |
GET /api/runtime-feature-flags/authority-status |
mode: Shared, state: Ready, identity.environmentId: <env>, identity.bindingEpoch: <binding-epoch>, identity.storeEpoch equal to the receipt's |
GET /api/runtime-feature-flags/authority-evaluation/disableMembership |
state: Evaluated, value equal to the baseline or saved override, safeValue: false, productionEligible: true, maximumAgeSeconds: 30 |
GET /api/runtime-feature-flags/disableMembership/fleet |
Until the loop has published fresh observations of both services: state: Unknown with a Collector… reason (for example CollectorInventoryMissingOrInvalid or CollectorInventoryStaleOrInvalidClock). This is expected. Then state: Active within 60 s, one host row per expected pod of each service, each state: Current; rows of pods that no longer exist show Retired |
GET /api/runtime-feature-flags (anonymous snapshot) |
200, with an authority envelope. A 503 with runtime_policy_unavailable means no current authority |
Legacy PUT PUT /api/runtime-feature-flags/<key> |
403 Runtime changes are not available in this mode. This is expected in Shared mode |
PM has no admin authority routes. Its adoption shows only in the fleet host list.
Evidence to capture¶
- The cluster-gitops PR URL, approval and the rendered env diff.
- The
authority-status,authority-evaluationandfleetresponses after rollout. - The end time of the freeze.
If a step is refused¶
Start-up refusals stop the host before it serves:
| Start-up message | Cause |
|---|---|
RuntimeFeatureFlags:Mode must be Legacy, Shadow or Shared. |
Bad mode value |
Shared authority mode cannot also enroll the legacy-format shared reader. |
SharedReaderMode is not Disabled |
Authority observation requires matching environment identities and explicit production Runtime admission. |
RuntimeEnvironment/ActiveEnvironment differ, or production without Runtime admission |
Authority observation requires explicit binding expectations: … |
A binding value is missing or invalid |
Production Runtime requires Shared mode and both production environment identities. |
Production ProductionPolicyMode=Runtime without Mode=Shared |
ProductionPolicyMode must be Legacy or Runtime. |
Bad value |
runtimePolicyHostMetadata requires boolean hpa.enabled (render) and similar |
Fix the chart values |
Legacy runtime policy provider cannot read the runtime policy document. Inspect the document before restarting this host. |
The legacy store could not read the global document at all. Inspect it (read-only) and stop. An imported authority is not this case: a Legacy API starts against it and serves the deployed values (runbook 10) |
Legacy runtime policy provider cannot serve initialized authority. Deploy the authority-capable adapter before restarting this host. |
Only an image older than #4414, started in Legacy mode against the imported authority. Finish R3.1, or deploy a current image. The refused host has already written a syrf-api writer-registry entry (R2.7) |
After start-up, authority-status Quarantined or Unavailable and fleet states other than Active are
handled in runbooks 6 and 9.
4. Collector identity and scope¶
Purpose¶
Give the environment one workload collector that publishes API and PM Deployment and pod facts to
POST /api/environment/observations, using a machine identity whose only scope is
environment:observations:write. Collector evidence is what lets reports become Complete and what Collector
inventory mode reads.
Preconditions¶
- Migration
0018is applied. - A read-only Kubernetes identity with
geton the named Deployment andliston ReplicaSets and Pods in the namespace. Staging has the precedentsyrf-stats-preflight. Production needs a camarades-infrastructure change (OD-23). - A machine client for the scope:
- OpenIddict environments: Identity seeds
syrf-environment-collectoronly from an explicit strongEnvironmentCollector:ClientSecret(at least 32 characters); - production: an Auth0 machine application with the same scope name, a production-shared tenant change that needs Chris's approval.
- TO CONFIRM: no chart value renders
EnvironmentCollector:ClientSecretinto Identity, and rawenv:must not carry secrets. A chart change followingidentityStatisticsParityEvidenceis needed. - TO CONFIRM: the scheduler that runs capture and publication repeatedly does not exist (OD-23).
Steps¶
- R4.1 Configure the API (raw
env:, API values):
env:
SYRF__RuntimeFeatureFlags__WorkloadCollector__Enabled: "true"
SYRF__RuntimeFeatureFlags__WorkloadCollector__Provider: "<OpenIddict|Auth0>"
SYRF__RuntimeFeatureFlags__WorkloadCollector__ClientId: "<collector-client-id>"
SYRF__RuntimeFeatureFlags__WorkloadCollector__Audience: "<api-audience>"
Audience must equal IdentityService:Audience (OpenIddict) or Auth0:ApiAudience (Auth0). The
SYRF__RuntimeFeatureFlags__Binding__* values (as in R3.1) must also be set, and <runtime-env> must match
them. The collector does not need Shared mode, so this can run before enrolment; Legacy mode ignores the
binding values.
2. R4.2 Obtain a token (OpenIddict), the same form as scripts/fetch-statistics-parity.py:
POST <identity-origin>/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<collector-client-id>&client_secret=<secret>&scope=environment:observations:write
TO CONFIRM: the token request for the production Auth0 machine application (audience parameter). 3. R4.3 Capture one service, read-only, with an explicit reviewed context:
python3 scripts/capture-runtime-workload.py \
--context "<reviewed-context>" --namespace "<namespace>" \
--service api --deployment "<api-deployment>" --container "<api-container>" \
--environment "<env>" --binding-epoch "<binding-epoch>" \
--expected-observation-id "<last-accepted-observation-id>" \
--output api-observation.json
The chart default Deployment and container name is the chart name (syrf-api, syrf-projectmanagement);
check the rendered manifest. Omit --expected-observation-id only for a known first publication.
4. R4.4 Publish it at once (publication refuses snapshots older than 30 seconds):
SYRF_RUNTIME_COLLECTOR_TOKEN="<collector-token>" python3 scripts/publish-runtime-workload.py \
--api-origin "<api-origin>" --environment "<env>" \
--binding-epoch "<binding-epoch>" --service api --input api-observation.json
- R4.5 Repeat R4.3–R4.4 for
project-management. Keep each service's last accepted observation ID. - R4.6 Keep publishing. Collector inventory needs observations strictly younger than 30 seconds for both services, so both must be republished well inside 30 seconds for as long as Collector-mode Active is needed.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| R4.3 capture | Exit 0, api-observation.json created; never overwritten |
| R4.4 publish | Exit 0; stdout only {"observationId": "<observation-id>", "digest": "<64 hex>"} |
| After R4.5: report (runbook 7, R7.3) | Sources workloads:api and workloads:project-management are Current |
| After R4.5: Collector-mode fleet (runbook 6) | state: Active; replaced pods' rows show Retired |
Evidence to capture¶
- The client registration change and its approval (no secret values).
- Each capture file and its publish receipt (observation ID and digest).
- The Kubernetes identity's role definition reference.
If a step is refused¶
| Result | Meaning and action |
|---|---|
Capture exit 2 Capture refused: Output already exists; preserve it for exact replay |
Use a new output path; keep the old file |
Capture exit 2 Capture refused: Workload changed during capture; acquire a new observation |
A rollout is in progress; capture again |
Capture exit 2 Capture refused: Capture clock or freshness budget invalid |
Reads took over 20 seconds or the clock moved back |
Capture exit 2 Capture refused: Observation exceeds publication body limit |
Over 512 KiB; investigate the pod count |
Publish exit 2 Publication refused locally; check origin, binding, capture and credential inputs |
Fix the inputs. HTTPS only, no path; capture must match --environment, --binding-epoch, --service |
Publish exit 1 Publication refused: HTTP 400 |
Invalid workload observation: contract failure, for example a pod without explicit ready/terminating |
Publish exit 1 Publication refused: HTTP 409 |
Workload observation refused. Causes in the store: expected observation missing or not advanced, an observation ID reused with different evidence, a stale or future snapshot, a changed binding, a missing 0018 index, or publication disabled or bound elsewhere. Capture again with the right previous ID |
Publish exit 1 HTTP 401 or HTTP 403 |
Token scope, client, audience or binding mismatch, or the collector is disabled |
Publish exit 3 Publication outcome unknown; preserve and replay the exact capture |
408/425/429, 5xx (503 Workload publication outcome unknown…) or transport loss. Replay the same file |
API start-up Enabled workload collection requires explicit matching provider, client, audience and environment binding. |
Fix R4.1 |
5. Coordinator identity¶
Purpose¶
Give the promotion workflow a machine identity, scope environment:deployments:coordinate, that records,
extends and releases one deployment compatibility constraint per rollout through
PUT and DELETE /api/environment/deployment-constraints/<rollout-id>.
Preconditions¶
- The API is in Shared mode with the binding (runbook 3).
- A machine client:
- OpenIddict: Identity seeds
syrf-environment-coordinatoronly whenEnvironmentCoordinator:ClientSecrethas at least 32 characters; - production: an Auth0 machine application (tenant change, Chris's approval).
- TO CONFIRM: no chart value renders
EnvironmentCoordinator:ClientSecretinto Identity. - TO CONFIRM: no workflow calls the coordinator routes yet, and how the coordinator builds the capability intersection and completion targets from a trusted report is not implemented.
Steps¶
- R5.1 Configure the API (raw
env:):
env:
SYRF__RuntimeFeatureFlags__DeploymentCoordinator__Enabled: "true"
SYRF__RuntimeFeatureFlags__DeploymentCoordinator__Provider: "<OpenIddict|Auth0>"
SYRF__RuntimeFeatureFlags__DeploymentCoordinator__ClientId: "<coordinator-client-id>"
SYRF__RuntimeFeatureFlags__DeploymentCoordinator__Audience: "<api-audience>"
- R5.2 Record a constraint before a rollout. Always send
action; an omitted action means Record:
PUT /api/environment/deployment-constraints/<rollout-id>
Authorization: Bearer <coordinator-token>
Content-Type: application/json
{
"mutationId": "<mutation-id>",
"expectedIdentity": <identity>,
"action": "Record",
"reason": "<rollout reason>",
"expiresAtUtc": "<future time>",
"servingCatalogDigests": ["<serving-catalog-digest>"],
"targetCatalogDigest": "<target-catalog-digest>",
"allowedCapabilities": { "policySchemaVersion": 1, "enabledKeys": ["<key>"] },
"completionTargets": {
"target": { "catalogDigest": "<target-catalog-digest>", "serviceImages": { "api": "<repo>@sha256:<digest>", "project-management": "<repo>@sha256:<digest>" } },
"rollback": { "catalogDigest": "<serving-catalog-digest>", "serviceImages": { "api": "<repo>@sha256:<digest>", "project-management": "<repo>@sha256:<digest>" } }
}
}
- R5.3 Extend with
action: "Extend", the reviewedexpectedConstraintDigestand a laterexpiresAtUtc. - R5.4 Release after the collector shows every replica on the target (or a completed rollback):
DELETE /api/environment/deployment-constraints/<rollout-id>
Authorization: Bearer <coordinator-token>
Content-Type: application/json
{ "mutationId": "<mutation-id>", "expectedIdentity": <identity>, "action": "Release",
"reason": "<reason>", "expectedConstraintDigest": "<constraint-digest>", "completionKind": "Target" }
Clients and proxies must keep the JSON body on DELETE.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
Before R5.2: report (purpose=promotion) |
deploymentConstraint source Current; projection null (no active constraint) |
| R5.2–R5.4 response | 200 {"mutationId": "<mutation-id>", "rolloutId": "<rollout-id>", "action": "<action>", "constraintDigest": "<digest or null after release>", "recordedAtUtc": "<time>"} |
| After R5.2: report | deploymentConstraint.state: Active, rolloutId, constraintDigest |
Evidence to capture¶
- Each request and receipt (mutation ID, rollout ID, constraint digest).
- The report before record and after release.
If a step is refused¶
| Response | Meaning and action |
|---|---|
400 Invalid coordinator operation |
Wrong verb for the action (Record/Extend need PUT, Release needs DELETE) or a rollout ID that is not one ASCII segment of up to 128 [A-Za-z0-9-_.:] |
400 Invalid coordinator request |
Malformed identity or fields |
409 Policy identity changed; read a fresh report |
Revalidate against the new policy, then start again |
409 Deployment constraint operation refused |
The store refused. Possible causes: another active constraint, policy outside the capability intersection, missing completion expectations, unproven completion, not the owning coordinator, expiry not moved forward. The response does not name the cause |
503 Coordinator outcome unknown; retry the exact mutation ID and request |
Retry unchanged |
| 401/403 | Wrong scope, client, audience or binding; coordinator disabled |
API start-up Enabled deployment coordination requires explicit matching provider, client, audience and environment binding. |
Fix R5.1 |
A stranded constraint is released by an administrator through break-glass (runbook 9). An expired constraint keeps blocking incompatible changes; expiry is never a release.
6. Expected inventory and Collector mode¶
Purpose¶
Tell the API how many API and PM hosts must acknowledge a policy before the fleet is Active, and let it read that membership from fresh collector observations instead of every stored acknowledgement row.
Preconditions¶
- Runbook 3 (hosts publish acknowledgements) and runbook 4 (fresh observations for both services).
- Fixed replica counts and
hpa.enabled: falsefor API and PM. - For production enables and restores: Collector mode with the collector publishing (see Status and boundary). Static mode refuses them after any rollout.
Steps¶
- R6.1 Set the per-host metadata through the chart (
runtimePolicyHostMetadata.enabled: true). Each pod rendersInstanceUidfrommetadata.uid,ImageVersionfrom the exact image,ExpectedReplicasfromreplicaCountandAutoscaledfromhpa.enabled. - R6.2 Set the fleet inventory on the API:
SYRF__RuntimeFeatureFlags__FleetExpectedReplicas__apiandSYRF__RuntimeFeatureFlags__FleetExpectedReplicas__project-management, each equal to that service'sreplicaCount. No count is inferred from observed hosts. - R6.3 Set
SYRF__RuntimeFeatureFlags__FleetInventoryMode: "Collector"on the API. The default isStatic. Any other value refuses inspection. - R6.4 Change counts only together. A replica change edits
replicaCountand the matchingFleetExpectedReplicasvalue in the same cluster-gitops change.
Notes:
replicaCount: 0rendersExpectedReplicas: "0"as metadata only; a zero-count host publishes no acknowledgement.FleetExpectedReplicasmust be positive, so a zero-count service leaves the fleetUnknown(ExpectedInventoryMissingOrInvalid).- Acknowledgements count as current only if younger than 30 seconds. Hosts poll every 8–12 seconds, and 6B
measured adoption up to 11.5 seconds. That margin is close to the limit; treat brief
Applyingas normal. - Static mode suits only hosts whose instance UIDs stay the same (the local harness). In Kubernetes every
replaced pod has a new UID, and old rows have no TTL, so a Static fleet reads
Applying, thenDegraded, after every rollout. - In Collector mode a row absent from fresh collector inventory is retired only once MongoDB has not seen it rewritten for the 35-second quiet period (30-second freshness plus 5 seconds, server time; #4407). So a rollout reaches Active about 35 seconds after the old pods stop. Rows with no server stamp, written by images older than #4407, need 15 minutes, the assumed bound on host clock skew, so the first rollout away from those images can take up to 15 minutes.
Preflight checks and expected output¶
GET /api/runtime-feature-flags/disableMembership/fleet (Cache-Control: no-store):
Fleet state / reason |
Meaning |
|---|---|
Active / null |
Every expected host of both services is Current on this identity |
Saved or Applying / HostsUnconfirmed |
Within the 60-second confirmation window |
Degraded / HostsUnconfirmed |
Window passed with hosts missing or behind |
Unknown / ExpectedInventoryMissingOrInvalid |
A FleetExpectedReplicas value is missing, non-positive or names another service |
Unknown / AutoscaledInventory |
A host reports Autoscaled |
Unknown / DomainReadinessNotEstablished |
Any key other than disableMembership |
Unknown / Collector… reasons |
CollectorInventoryMissingOrInvalid, CollectorBindingMismatch, CollectorInventoryStaleOrInvalidClock, CollectorExpectedCountDisagrees, CollectorRolloutIncomplete, CollectorHostServiceDisagrees, CollectorHostObservationDisagrees |
Action required / ExpectedInventoryDisagrees, DuplicateInstanceIdentity, IncompatibleHost, InvalidCommitClock |
Fix the configuration or the incompatible host |
Host rows carry state Current, Behind (PolicyNotApplied), Unknown (HostMetadataMissing,
StaleOrInvalidObservationClock, or the host's error class), Incompatible (IdentityOrCatalogMismatch) or,
in Collector mode, Retired (AbsentFromFreshCollectorInventory). Retired rows stay stored and visible.
Evidence to capture¶
- The values change and the fleet response showing
Activewith the expected host count. - In Collector mode, a fleet response after a rollout showing old rows
Retired.
If a step is refused¶
- 403
Fleet inspection requires Shared API authority mode.: runbook 3 is not complete on this API. - 503
runtime_policy_unavailable: the snapshot could not be read; retry. - An unsupported
FleetInventoryModevalue refuses inspection (Unsupported fleet inventory mode.).
7. Change one flag¶
Purpose¶
Make one reviewed policy change to <key> (in production, disableMembership only): preview it, commit
exactly the previewed change, then confirm that every expected host has adopted it.
Preconditions¶
- Runbooks 3 and 6 complete; fleet
Activefor the current identity. - The operator holds
ChangeRuntimeFeaturePolicy(Administrator, OD-2). - For production enables: Collector mode with the collector publishing, the release guidance (OD-26) and Chris's approval.
- The admin page's preview/commit UI (#4381) is not on
main; use the API routes.
Steps¶
- R7.1 Read the identity (
authority-status, above). Expectstate: Ready. - R7.2 Check the fleet (
GET /api/runtime-feature-flags/<key>/fleet). Expectstate: Active. - R7.3 Check compatibility. Collect a report with the report token, save it, then run the checker:
GET /api/environment/compatibility-reports/current?purpose=policy-change
Authorization: Bearer <report-token>
python3 scripts/runtime-compatibility-check.py --report report.json --candidate candidate.json \
--environment "<env>" --binding-epoch "<binding-epoch>" --purpose policy-change
The candidate is the serving build (syrf.runtime-candidate/v1). Never pass --now for a real decision.
Reports need RuntimeFeatureFlags:CompatibilityReport settings (Enabled, Provider, ClientId,
Audience) and the client syrf-environment-report with EnvironmentReport:ClientSecret (same chart gap
as runbook 4).
4. R7.4 Preview (read-only, allowed under support impersonation):
POST /api/runtime-feature-flags/<key>/preview
<admin-session>
Content-Type: application/json
{
"mutationId": "<mutation-id>",
"expectedIdentity": <identity>,
"value": true,
"reason": "<reason>",
"action": "Change"
}
value is true, false or null (clear the override). If the response is 422
featureFlagDependenciesRequired, review the set and repeat with "confirmedDependencies": {…}, the exact
key/value set. Keep includeDependencies false.
5. R7.5 Commit the same body plus the preview digest:
PUT /api/runtime-feature-flags/<key>/policy
<admin-session>
Content-Type: application/json
{ …the R7.4 body unchanged…, "previewDigest": "<preview-digest>" }
- R7.6 Confirm adoption. Poll the fleet until
state: Activeand every host row shows the committed revision withstate: Current. Checkauthority-evaluation/<key>on the API for the new revision and value. Browsers are not part of adoption. The commit wakes the writing pod's own refresh, so its tabs get aRuntimeFeatureFlagsRevisionhint as soon as that pod's reader has the new identity (one authority read, bounded by its 5 s deadline); tabs on other API pods get one when that pod's next refresh (8–12 s) observes it. Either way the tab refetches the snapshot. A failed hint is only a log line (Runtime policy revision … hint failedor… hint unavailable); it never changes the receipt. - R7.7 If the response was lost, look up the receipt, then retry the unchanged request if needed:
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| R7.3 report | completeness: Complete, purpose: policy-change, every entry of requiredSources Current. In Static mode reports stay Incomplete (fleetInventoryMode:Static in unknownOrUnsupported) |
| R7.3 checker | {"exitCode": 0, "reasons": [], "verdict": "pass"}, exit 0 |
| R7.4 preview | 200 with previewDigest, before (the current identity), proposed (revision + 1), changes containing {"key": "<key>", "proposedOverride": true, "beforeEffective": false, "proposedEffective": true, "cause": "Requested", …} and fleetReadiness: the current fleet state in production (Active needed to enable), Unknown elsewhere |
| R7.5 commit | 200 {"mutationId": "<mutation-id>", "identity": {…, "revision": "<revision + 1>"}}. Committed, not yet adopted |
| R7.6 fleet | Active for the new identity within the 60-second window; 6B measured adoption up to 11.5 seconds |
| R7.7 lookup | {"mutationId": "<mutation-id>", "outcome": "Committed", "committedIdentity": {…}}, or outcome: Unknown with no identity, which does not prove an abort |
Evidence to capture¶
- The report ID, the report file and the checker output.
- The preview response, the commit receipt and the fleet responses until Active, with timestamps.
If a step is refused¶
| Response | Meaning and action |
|---|---|
| 403 (no body) | The caller lacks ChangeRuntimeFeaturePolicy |
503 authority-unavailable |
Application authority could not decide; retry after Retry-After. During a rollout, see D4 |
403 Policy administration requires Shared API authority mode. |
This API is not enrolled |
409 runtime_policy_revision_conflict with actualRevision |
Someone changed the policy. Re-read and decide again |
422 featureFlagDependenciesRequired |
Confirm dependencies (R7.4). canApplyAutomatically: false means a deployment change is needed first |
400 runtime_policy_constraint, message: The reviewed preview is missing or stale. Preview the current policy and retry. |
Preview again; the digest no longer matches |
… Production enable or restore requires fresh fixed-fleet acknowledgement of the current policy. |
Fleet not Active. In Static mode this follows every rollout; set Collector mode and keep the collector publishing (runbook 6) |
… This key is not eligible for production runtime policy. |
Only disableMembership |
… A production plan cannot change ineligible dependencies or safety pins. |
The change would touch other keys in production |
… Policy requires a capability outside the deployment intersection. |
An active deployment constraint forbids it; wait for release |
… Emergency stops use their independent safe-state path. |
Use runbook 8 |
… Authority mutation requires an exact verified environment binding., Environment binding changed before the authority transaction., Authority identity does not match the verified binding., The request belongs to a different authority lineage. |
Runbook 9 |
… Mutation ID was already used with a different request. |
Use a new mutation ID for a new request |
503 runtime_policy_commit_unknown |
Retry the same mutation ID and unchanged request (R7.7) |
503 runtime_policy_unavailable |
Preview or read failed; nothing was written |
| Checker exit 1 or 2 | See the reason codes in Checking a candidate offline |
8. Emergency stop and restore¶
Purpose¶
Force an audited global safe-false control to its safe value at once, keeping the saved baseline and overrides, and later restore the saved policy through a reviewed preview and commit.
Preconditions¶
- The API is in Shared mode. The operator holds
EmergencyStopRuntimeFeaturesfor a stop andChangeRuntimeFeaturePolicyfor the restore. - A stop needs no fleet readiness, deployment-constraint clearance, content or new audit indexes. It still needs the binding, lineage and receipt integrity.
- A production restore that makes the control effective On needs the fleet Active on the current (stopped) policy, so the same Collector-mode and running-collector prerequisite applies.
Steps¶
- R8.1 Read the identity (
authority-status). - R8.2 Stop:
POST /api/runtime-feature-flags/<key>/emergency-stop
<admin-session>
Content-Type: application/json
{ "mutationId": "<mutation-id>", "expectedIdentity": <identity>, "reason": "<reason>" }
- R8.3 Confirm the stop reached every host:
authority-evaluation/<key>showsvalue: falseat the stop revision on the API, and the fleet host rows show the stop revisionCurrent. A 503 or lost response is retried with the same mutation ID and body. - R8.4 Restore when the incident is resolved. Read the identity again, then preview and commit
ReleaseStopwith a null value and no dependency changes:
POST /api/runtime-feature-flags/<key>/preview
<admin-session>
Content-Type: application/json
{ "mutationId": "<new-mutation-id>", "expectedIdentity": <identity>, "value": null,
"reason": "<reason>", "action": "ReleaseStop" }
Then PUT /api/runtime-feature-flags/<key>/policy with the same body and previewDigest. The local harness
sends action as its ordinal (2). The API reads request bodies with Newtonsoft.Json, which also accepts
the name: the 6C.2 rehearsal sent "ReleaseStop" (and "Change" in R7.4) as names and both were accepted.
5. R8.5 Confirm adoption of the restore revision as in R7.6.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| R8.2 response | 200 {"mutationId": "<mutation-id>", "identity": {…}}: durably committed, not yet adopted |
| R8.3 | Every API evaluates false at the stop revision; the 6B harness checks this within 60 seconds with a PM host paused |
| R8.4 preview | The changes entry shows wasStopped: true, willBeStopped: false and proposedOverride equal to the saved override |
| R8.5 | Every API evaluates the restored saved value; fleet Active |
A new mutation ID always records a new revision, even if the key is already stopped. Idempotency is per mutation ID.
Evidence to capture¶
- The reason, the stop receipt and the per-host adoption times.
- The restore preview, receipt and fleet
Activeresponse.
If a step is refused¶
| Response | Meaning and action |
|---|---|
| 403 (no body) | The caller lacks EmergencyStopRuntimeFeatures |
403 Emergency stops require Shared API authority mode. |
Not enrolled |
409 runtime_policy_revision_conflict |
Re-read the identity and decide; a stop is often still wanted |
400 runtime_policy_constraint: Stop actions require an audited global safe-state contract and cannot change saved values or dependencies. |
The key has no audited safe-false contract |
503 runtime_policy_commit_unknown |
Retry the same mutation ID and request |
| Restore refusals | As runbook 7, including the production fleet refusal |
9. Recovery and rebind¶
Purpose¶
Recover from the failure cases the code reports, without bypassing a binding, a lineage or an audit record. Every recovery is a forward action.
Preconditions¶
- The failure is identified from a response, log line or state below.
- Approval as for the original step. Anything that would edit the binding, the authority document or the audit by hand needs Chris and is not covered here.
Steps¶
- R9.1 Lost or unknown policy outcome.
GET /api/runtime-feature-flags/mutations/<mutation-id>. OnCommitted, record it and stop. OnUnknown, retry the unchanged original request. - R9.2 Lost lineage apply outcome. Rerun R2.5 with the identical preview and a new output path.
- R9.3 Unknown workload publication (exit 3). Replay the exact capture file. Do not change evidence under an existing observation ID.
- R9.4 Mongo outage. No operator action. Decisions fail closed to the safe value on the first failed read
and recover by themselves after the outage (6B
mongo-outagescenario). Confirm with R7.6 afterwards. - R9.5 Reader quarantine.
authority-statusshowsQuarantined(InvalidBinding,IdentityOrRevisionRegression, orAuthorityNotInitializedafter a snapshot). There is no automatic reset. Find out why the identity, binding or revision changed, then restart the affected pods with a reviewed cluster-gitops change. TO CONFIRM: the reviewed recovery procedure that must precede the restart is not written. - R9.6 Stranded deployment constraint. A real administrator (no impersonation, no machine scope) calls:
POST /api/environment/deployment-constraints/<rollout-id>/break-glass-release
<admin-session>
Content-Type: application/json
{ "mutationId": "<mutation-id>", "expectedIdentity": <identity>,
"reason": "<reason, max 1000 characters>", "expectedConstraintDigest": "<64-character digest>" }
It needs BreakGlassRuntimeDeploymentConstraint and Shared mode. Expected: 200 with the coordinator receipt
shape.
7. R9.7 Restored database or changed cluster (rebind). A restored copy carries the source binding, so
up refuses with exit 2 and the reader quarantines. rebind is not built: no supported command replaces
a binding. Stop and raise it with Chris. Do not edit runtimeEnvironmentBinding by hand.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| R9.1 | outcome: Committed with committedIdentity, or Unknown |
| R9.4 after recovery | authority-evaluation state: Evaluated at the committed revision; fleet Active |
| R9.5 after restart | authority-status state: Ready with the expected identity |
| R9.6 | 200 {"mutationId", "rolloutId", "action", "constraintDigest": null, "recordedAtUtc"}; the report's constraint projection is null |
| R9.7 | status shows the binding record and the live fingerprint, and the mismatch |
Evidence to capture¶
- The failure response or log line, the lookup result and the retry receipt.
- For break-glass: the reason, the before-state report and the receipt.
- For R9.7: the
statusoutput and the escalation record.
If a step is refused¶
| Response | Action |
|---|---|
Mutation lookup 403 Mutation outcomes require Shared API authority mode. |
Use a Shared-mode API |
| Mutation lookup 503 | Storage failure: no outcome. Retry the lookup |
| Break-glass 403 | Impersonation, a machine scope, more than one identity, or a missing capability |
Break-glass 403 Break-glass requires Shared API authority mode. |
Not enrolled |
Break-glass 400 Invalid rollout identity |
Fix the rollout ID |
Break-glass 409 Policy identity changed; inspect current state / Deployment constraint recovery refused |
Read the current policy and constraint, then decide again |
Break-glass 503 Break-glass outcome unknown; retry the same mutation ID and unchanged request |
Retry unchanged |
10. Legacy return¶
A host in Legacy mode against an imported authority (owner decision D7=A, spec 6C.5) starts and serves the
deployment-controlled values for every key. It never applies the authority's saved overrides or stops,
never writes the authority document, refuses legacy changes, and registers as a read-only reader so it cannot
block a later lineage freeze. The saved policy, baseline, receipts, audit and stops stay exactly as they were,
so returning to Shared (or production Runtime) resumes the same identity. The legacy-return scenario
of the runtime-fleet harness rehearses this runbook end to end on an isolated stack.
Purpose¶
Return API and PM from runtime policy (Shared, or Runtime in production) to deployment-controlled values
without any change in behaviour (RT-15), and come back without losing the saved policy.
Preconditions¶
- No active stop on any production-eligible key.
- For every production-eligible key (today
disableMembership), the runtime decision equals the deployedFeatureFlagsvalue. - Collector mode, so the report can be
Complete. - Chris's approval of the mode-switch change.
Steps¶
- R10.1 Make the values agree. Either commit a reviewed forward revision so the runtime value equals the
deployed value (runbook 7), or change the deployed value in GitOps (
featureFlags.disableMembership) to the runtime value. Release any stop first (runbook 8). - R10.2 Collect a legacy-return report:
GET /api/environment/compatibility-reports/current?purpose=legacy-return
Authorization: Bearer <report-token>
- R10.3 Run the checker with a candidate whose
targetModeisLegacy(the build that will serve):
python3 scripts/runtime-compatibility-check.py --report report.json --candidate candidate.json \
--environment "<env>" --binding-epoch "<binding-epoch>" --purpose legacy-return
Record the authority identity from authority-status (all seven fields) before the switch.
4. R10.4 Switch the mode for API and PM in one cluster-gitops change, changing nothing else: production
SYRF__RuntimeFeatureFlags__ProductionPolicyMode: "Legacy"; elsewhere SYRF__RuntimeFeatureFlags__Mode:
"Legacy". Saved policy, baseline, receipts, audit and stops are not changed by the switch.
5. R10.5 Confirm after the rollout, on each API:
- authority-status returns mode: Legacy, state: Disabled, reason: LegacyModeAuthorityPreserved and
no identity (a Legacy host never reads or verifies the authority);
- the anonymous snapshot GET /api/runtime-feature-flags has policyState:
DeploymentControlledAuthorityPreserved, mutationAllowed: false, no overrideValue on any flag, and
disableMembership at the report's legacyValue;
- provider-status has policyState: DeploymentControlledAuthorityPreserved and policyStateDetail saying
the runtime policy is preserved but not applied;
- the admin page shows "Deployment-controlled; runtime policy preserved" and offers no changes.
6. R10.6 Return to runtime policy when wanted: revert only the mode (production ProductionPolicyMode:
"Runtime"; elsewhere Mode: "Shared") for API and PM together. authority-status returns Ready with the
identity recorded in R10.3, unchanged in all seven fields, and the fleet reaches Active.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| R10.2 report | completeness: Complete, purpose: legacy-return, legacyParity.agrees: true, each entry stopActive: false, legacyValue equal to runtimeValue |
| R10.3 checker | {"exitCode": 0, "reasons": [], "verdict": "pass"}, exit 0 |
| R10.5 legacy change | PUT /api/runtime-feature-flags/<key> (or /groups/<key>) answers 409 with code: runtime_policy_authority_preserved; nothing is written |
| R10.5 registry | db.syrf_metadata.findOne({_id: "meta"}, {"services.syrf-api.instances": 1}) (read-only) shows each Legacy API with runtimePolicyReadOnly: {reason: "LegacyReaderAuthorityPreserved", connectedAt: <its own connectedAt>} |
| R10.5 authority unchanged | The global document and the audit row count read before R10.4 are unchanged (read-only queries) |
| R10.6 return | authority-status Ready with the R10.3 identity; fleet Active |
Evidence to capture¶
- The report ID and file, the candidate manifest and the checker output.
- The forward revision or GitOps change from R10.1.
- The identity before R10.4 and after R10.6, and the R10.5 outputs.
- The mode-switch PRs and their approvals.
If a step is refused¶
| Checker reason | Exit | Action |
|---|---|---|
legacy-parity-stop-active |
1 | Release the named key's stop (runbook 8) |
legacy-parity-differs |
1 | R10.1 for the named key |
legacy-parity-missing |
2 | The report has no bound policy; collect it while the hosts are still in Shared/Runtime |
binding-mismatch |
1 | Wrong --environment/--binding-epoch, or a report from another environment |
report-incomplete, required-source-unknown, report-expired, report-unstable, purpose-mismatch |
2 | Collect a fresh report with purpose=legacy-return in Collector mode |
migration-not-applied, catalog-key-missing, catalog-digest-incompatible |
1 | The target build cannot honour the current state; plan a replacement release |
Report 400 environment_report_purpose_invalid |
n/a | purpose must be exactly policy-change, promotion or legacy-return, given once |
Report 503 environment_report_unavailable |
n/a | Missing binding, indexes or evidence; retry, then investigate |
The checker exits 2 whenever any insufficient-evidence reason is present, even if refusal reasons are listed too.
For example, a Static-mode report whose values differ gives exit 2 with both report-incomplete and
legacy-parity-differs.
A Legacy host never refuses to start over the authority. If R10.5 shows a runtime value instead of the
deployed one, the host is not in Legacy mode: check the rendered Mode/ProductionPolicyMode.
11. Historical release import (written only)¶
Written only. This runbook has not been run in any environment beyond the isolated tests. It is an operational prerequisite for showing past releases (for example the August 2026 What's New entries) with their real dates, and needs Chris's approval before any shared environment.
Purpose¶
Record a release that happened before release records existed (6D PR F, RT-27), with its date precision, source and reviewer, and without notifying anyone. Contract, fields and refusals: Importing past releases.
Preconditions¶
- Runbook 1 done (verified binding) and migrations
0012–0015and0021applied (SYRF_MIGRATION_MODE=verify). - The feature exists in the deployed image's feature definition catalog. Historical features without a definition
(for example bulk PDF) need a
feature-definitions.yamlchange first. - A reviewed input file per release:
featureId,releaseKey,stage,precision,occurredOn,source,reviewer. UseUnknownwhen no date is evidenced; never guess one. - No API outage or writer freeze is needed: the command writes no policy.
Steps¶
- R11.1 Preview with
SYRF_RUNTIME_RELEASE_HISTORY_MODE=preview,..._INPUT(the reviewed input) and..._OUTPUT(a new path), plus the Mongo settings,RuntimeEnvironmentand the threeSYRF_MIGRATION_BINDING_EXPECTED_*values. - R11.2 Review the preview:
record(statusActive,imported: true, noapplied),event(HistoricalImported,provenanceDetail), the precision andoccurredOn, andpreviewDigest. - R11.3 Apply the unmodified preview with
SYRF_RUNTIME_RELEASE_HISTORY_MODE=apply,..._INPUT(the preview), a new..._OUTPUTandSYRF_RUNTIME_RELEASE_HISTORY_ACTOR=<operator identity>.
Preflight checks and expected output¶
| Check | Expected output |
|---|---|
| R11.1 | Exit 0; Historical release import preview completed; preview file written; nothing in the database changed |
| R11.3 | Exit 0; receipt with the stored record and event; runtimeFeatureReleaseOutbox and runtimeFeatureFlagAudit counts unchanged |
| Replay | Applying the same preview to a new output path exits 0 with the original receipt and writes nothing |
Evidence to capture¶
- The reviewed input, preview and receipt files, unedited, and the reviewer's approval.
If a step is refused¶
Refusals exit 2 with Historical release import refused: <message>. Nothing is written.
| Message starts with | Action |
|---|---|
Unknown feature |
Add the feature definition first, in its own PR |
The reviewed import preview was modified |
Preview again; never edit a preview |
The reviewed import preview is stale |
The binding changed (runbook 9) or the input no longer gives the plan; preview again |
A release with this feature and release key already exists |
The key is taken by another import or a prepared release; check the existing record, or use a new key for a genuinely separate release |
Required release index is missing or incompatible |
Apply migrations (runbook 1 preconditions); the command never creates indexes |
Output already exists |
Choose a new output path |
An exit 1 is an unexpected failure: retry the identical preview with a new output path. This includes a failure to write the receipt file: the receipt is written after the database commit, so the import may already be committed. The retry is an exact replay, which writes nothing and returns the original receipt.
Known gaps and TO CONFIRM items¶
These were found while checking the runbooks against main. None is fixed by this docs-only change. They
are tracked, with the same numbers, in #4403. Each needs a
tooling PR or an owner decision before the staging rehearsal (entry criterion 5).
- No chart-rendered way to run
SYRF_MIGRATION_MODE=statusorverifyin a cluster (R1.1, R1.5). - PR previews do not run
bindorup(only index-init), so a D1 preview rehearsal cannot apply0009–0021today. - No chart template runs the lineage command, mounts its reviewed input or keeps its output (runbook 2).
- The freeze mechanism for staging's auto-merged promotions is not defined (R2.4).
- No approved way to clear a stale writer-registry entry younger than 24 hours (runbook 2).
- No chart values for
EnvironmentReport:ClientSecret,EnvironmentCollector:ClientSecretorEnvironmentCoordinator:ClientSecreton Identity, and rawenv:must not carry secrets (runbooks 4, 5, 7). - Whether reviewed chart values should replace the raw
env:runtime settings (Mode,Binding,ProductionPolicyMode,FleetInventoryMode,FleetExpectedReplicas, report, collector and coordinator sections) before production. - No scheduler runs capture and publication continuously (OD-23), and the capture script supplies no
desiredGitOps provenance, sopromotionreports cannot beCompleteyet. Until a scheduler exists, the manual loop in R3.4 is the only way to keep observations fresh. - The token request for production Auth0 machine applications (its audience parameter) is not documented.
- No workflow calls the coordinator routes, and nothing builds its capability intersection or completion targets from a report.
- No tool emits a candidate manifest; the catalog digest is computed at run time only.
- The reviewed recovery procedure before restarting a quarantined reader is not written (R9.5).
rebindis not built (R9.7).- Resolved by #4414 (owner decision D7=A, spec 6C.5): a
Legacy host against an imported authority serves the deployed values, writes nothing and registers
read-only, so runbook 10 is executable; the runtime-fleet
legacy-returnscenario rehearses it. - Resolved by owner decision D8=A (slice 6C.6): production uses Initialise (no legacy document) instead of the lineage import, so no legacy policy document has to be created. Its in-cluster delivery is still gap 3.
-
The E2E mock OIDC provider issues the collector machine token since #4409 and the report-scope token (
environment:report:read) since #4414, which the runtime-fleetlegacy-returnandinitialise-policy-changescenarios use to collect a compatibility report. It still has no mapping for the coordinator scope. -
No chart template runs the historical release import command or keeps its artifacts (runbook 11), as for the lineage command (gap 3).
Not a gap since #4407: production enables and restores need Collector mode and a running collector. Static, the default, refuses them after any rollout. That is a configuration and operations prerequisite (runbook 6).