Runbook: Policy Engine — Policy Version Conflicts

Use this runbook when policy-gated release decisions look inconsistent or stale — for example, two policy-engine replicas reaching different verdicts for the same artifact, or a publish/activate that does not appear to take effect. It is written for the Platform on-call team and assumes familiarity with the Stella Ops policy versioning and activation model (summarized below).

The key thing to internalize: Stella Ops policy packs are versioned in PostgreSQL, not distributed as polled OPA bundles. “Different nodes on different versions” is therefore almost always a stale process cache at a policy-engine replica, not a distribution-layer failure. The corrected version model is documented under Version model (ground truth).

Metadata

FieldValue
ComponentPolicy Engine
SeverityMedium
On-call scopePlatform team
Last updated2026-05-30
Doctor checkcheck.policy.engine (plugin stellaops.doctor.policy; PolicyEngineHealthCheck)

Source-of-truth note (reconciled 2026-05-30): there is no dedicated check.policy.version-consistency Doctor check and no PolicyVersionMismatch alert in the codebase. The closest implemented diagnostic is check.policy.engine, which probes the policy engine’s compilation, evaluation, and storage health (it reports a policy_count evidence field but does not compare versions across replicas — and despite its name it surfaces no policy_versions evidence field: the storage probe counts stored data entries internally but never emits that count). The version model is also not the multi-node “bundle distribution” model this runbook originally assumed — see the corrected model below. Items that describe an unimplemented design are flagged NOT IMPLEMENTED.


Version model (ground truth)

Policy packs are versioned in PostgreSQL, not distributed as polled OPA bundles:


Symptoms


Impact

Impact TypeDescription
User-facingInconsistent policy decisions; unpredictable gate results
Data integrityDecisions may not match expected policy behavior
SLA impactGate accuracy SLO violated; trust in decisions reduced

Diagnosis

Quick checks

  1. Check Doctor diagnostics (the implemented policy-engine health check):

    stella doctor --check check.policy.engine
    

    This probes compilation, evaluation, and storage against the policy engine URL (Policy:Engine:Url / PolicyEngine:BaseUrl). Its evidence includes engine_type, engine_version, engine_url, the *_status fields, policy_count, compilation_time_ms, evaluation_latency_p50_ms, and cache_hit_ratio (there is no policy_versions evidence field).

  2. Inspect a policy’s run history (implemented). Note: history lists policy run records (filterable by --from / --to / --status), not pack version/publish state — to confirm which versions are published, query policy.pack_versions directly (see Deep diagnosis):

    stella policy history <policy-id>
    
  3. Check the engine and its dependencies (implemented Doctor checks):

    stella doctor --check check.postgres.connectivity
    stella doctor --check check.policy.engine
    

NOT IMPLEMENTED. stella policy version --all-nodes, stella policy active --show-version, stella policy version diff --all-instances, stella policy bundle status, stella policy deployments list, and stella policy opa bundle-status do not exist. The stella policy command group in the CLI exposes lint, compile, simulate, version bump, submit, review, publish, activate, rollback, promote, history, explain, edit, test, new, init, sign, verify-signature, lattice, and verdicts — there is no --all-nodes flag, no bundle/deployment/opa subcommands, and no multi-node version-diff surface (the version model is single shared Postgres, not per-node bundles).

Deep diagnosis

  1. Confirm the active version per tenant in the database. The live version is the active_version column of policy.packs for the affected tenant; published versions live in policy.pack_versions. A replica diverging from this row is the real “version conflict” condition.

  2. Confirm storage reachability. check.policy.engine storage probe failing, or check.postgres.connectivity failing, points at the shared store the engine reads versions from.

  3. Check OPA reachability (if OPA gates are in use). Two different consumers reach OPA over REST: the OpaGateAdapter (base URL from the Opa config section, default http://localhost:8181) and the check.policy.engine Doctor probe (URL from Policy:Engine:Url / PolicyEngine:BaseUrl, same default). A failing check.policy.engine evaluation/compilation probe surfaces OPA REST errors and records them as evaluation_error / last_compilation_error evidence.


Resolution

Immediate mitigation

  1. Re-pin the active version explicitly (implemented). Activate the intended, already-published version for the affected tenant; only published versions can be activated, and activation does not mutate version content:

    stella policy activate <policy-id> --version <version>
    
  2. Roll back to a previous published version if the newly-activated one is the problem (implemented). The flag is --target-version (defaults to the previous version when omitted):

    stella policy rollback <policy-id> --target-version <version>
    
  3. Restart the diverging policy-engine replica so it re-reads the active version from Postgres. Use your deployment tooling (e.g. docker compose restart policy-engine); there is no stella service restart CLI command.

Root cause fix

If a replica is reading a stale version:

  1. Confirm the authoritative active version in policy.packs.active_version for the tenant (see Deep diagnosis).
  2. Restart / cycle the replica with the deployment tooling so it reloads from the shared store. Because all replicas read the same Postgres row, divergence is a process-cache condition, not a distribution failure.

If storage is unavailable:

  1. Run stella doctor --check check.postgres.connectivity and stella doctor --check check.policy.engine.
  2. Follow policy-storage-unavailable.md to restore the policy store, then re-verify the active version.

If an OPA gate is misbehaving:

  1. Confirm OPA is reachable. The gate adapter uses the Opa config section (default http://localhost:8181); the Doctor probe uses Policy:Engine:Url / PolicyEngine:BaseUrl (same default). A failing check.policy.engine evaluation/compilation probe surfaces the OPA REST error.
  2. If OPA itself has crashed, follow policy-opa-crash.md.

NOT IMPLEMENTED. The original “bundle distribution”, “node resync”, “deployment locking”, and “OPA bundle polling” remediations describe a design that does not exist in code. There is no stella policy sync, stella policy pin, stella policy bundle …, stella policy status --node, stella policy resolve-conflict, stella policy config set deployment.locking, or stella policy opa config set bundle.polling.* (the bundle.polling.min_delay_seconds / max_delay_seconds config keys appear nowhere in the source). Treat version convergence as: activate the published version + restart the stale replica.

Verification

# Re-run the policy engine health check (compilation/evaluation/storage)
stella doctor --check check.policy.engine

# Review the policy's run history (NB: run records, not version/publish state;
# to confirm a version is published, query policy.pack_versions directly)
stella policy history <policy-id>

# Re-run a DSL simulation against the policy to confirm expected output
# (this is the policy-DSL `simulate`: a .stella file + a signals JSON context)
stella policy simulate <policy-file> --signals <signals.json>

The authoritative cross-replica check is confirming each engine evaluates against the same policy.packs.active_version row; there is no stella policy version --all-nodes, stella policy evaluate --all-nodes, or stella policy logs command to do this from the CLI today.


Prevention

NOT IMPLEMENTED. “Enable deployment locking” and “configure aggressive bundle polling for fast convergence” describe controls that do not exist (no deployment-lock config, no bundle-polling layer). They are retained here only to mark them as absent.


The originally-referenced docs/operations/policy-deployment.md does not exist in the docs tree and has been dropped.