Runbook: Policy Engine - Policy Compilation Errors

For platform on-call and policy authors. Use this when a Stella Ops policy version fails to compile or activate — a :compile endpoint returning 400 ERR_POL_001, or stella policy compile/lint reporting POLICY-DSL-* diagnostics. It maps the diagnostic codes to their causes and shows how to fix the source and re-activate. Compilation targets the native stella-dsl@1 DSL; OPA/Rego is a secondary interop layer, not the compiler.

Sprint: SPRINT_20260117_029_DOCS_runbook_coverage Task: RUN-003 - Policy Engine Runbooks

Reconciliation note (2026-05-30): This runbook was originally written as if the Policy Engine were OPA/Rego-native. That is incorrect. The primary policy language is StellaOps’ own DSL (stella-dsl@1), compiled by the PolicyCompiler (src/Policy/StellaOps.PolicyDsl/) into a deterministic intermediate representation (IR) with a SHA-256 checksum. OPA/Rego is a secondary interop/gate layer, not the compilation engine — Rego can be exported/imported alongside the native engine, and an optional OpaGateAdapter can call an external OPA server for attestation gates. See docs/modules/policy/architecture.md §13 (“The C# engine remains primary; Rego serves as an interoperability adapter”). Commands, error codes, and checks below have been corrected to match src/.

Metadata

FieldValue
ComponentPolicy Engine (StellaOps.Policy.Engine, StellaOps.PolicyDsl)
SeverityHigh
On-call scopePlatform team
Last updated2026-05-31
Doctor checkcheck.policy.engine (src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Policy/Checks/PolicyEngineHealthCheck.cs)

Note on the Doctor check: there is a single aggregate check check.policy.engine that covers compilation, evaluation, and storage health. The previously documented check.policy.compilation-health does not exist in source. The check only runs when a policy-engine URL is configured (Policy:Engine:Url / PolicyEngine:BaseUrl, default http://localhost:8181).

Scope caveat: this check probes an OPA HTTP server — its “compilation” sub-check lists policies via OPA’s /v1/policies, reads /v1/status, and exercises /v1/data, reporting engine_type as opa (PolicyEngineHealthCheck.cs). It therefore validates the OPA gate/interop layer’s reachability, not the native stella-dsl@1 compiler. A native-DSL compilation failure surfaces through stella policy lint/compile and the :compile HTTP endpoint (below), not through this Doctor check.


Symptoms


Impact

Impact TypeDescription
User-facingNew policy versions cannot be compiled/activated; the engine keeps evaluating the last successfully compiled source.
Data integrityExisting active policies continue to evaluate; new rules are not enforced. Compilation is deterministic — the same source always yields the same IR checksum.
SLA impactPolicy updates blocked; security posture may be outdated until the source is fixed.

Diagnosis

Quick checks

  1. Check Doctor diagnostics:

    stella doctor --check check.policy.engine
    

    Verify the exact CLI flag against stella doctor --help. The check aggregates compilation, evaluation, and storage sub-results and reports engine_type, engine_version, policy_count, compilation_time_ms, and last_compilation_error.

  2. Lint the policy source locally (native DSL):

    stella policy lint <policy-file>.stella
    # JSON output for tooling:
    stella policy lint <policy-file>.stella --output json
    

    lint parses the source and reports every POLICY-DSL-LEX-* / POLICY-DSL-PARSE-* diagnostic with its code, path, and message. In the default text output it exits 1 if any error-severity diagnostic is present; note that the --output jsonpath currently always exits 0 and signals failure via the "success": false field in the payload instead (PolicyCliCommandModule.cs — for CI gating on JSON output, branch on success, not the exit code).

  3. Compile the source to IR (native DSL):

    stella policy compile <policy-file>.stella
    # Emit only the deterministic checksum:
    stella policy compile <policy-file>.stella --checksum-only
    # Write IR to a file:
    stella policy compile <policy-file>.stella -o compiled-ir.json
    

    On failure, compile writes the error-severity diagnostics to stderr and exits 1.

    Two policy compile implementations exist and their flags differ slightly. The CLI plugin module (StellaOps.Cli.Plugins.Policy/PolicyCliCommandModule.cs) offers --checksum-only and -o <file>. The core CLI build (CommandFactory.cs ~line 3458) offers --output/-o, --no-ir, --no-digest, --optimize, --strict, and --format. Both compile via the same PolicyCompiler and emit the same SHA-256 IR digest. If --checksum-only is not recognized in your build, omit --no-digest to get the digest instead.

Deep diagnosis

  1. Read the diagnostic code. Native DSL codes are defined in src/Policy/StellaOps.PolicyDsl/DiagnosticCodes.cs:

    CodeMeaning
    POLICY-DSL-LEX-001Unexpected character
    POLICY-DSL-LEX-002Unterminated string
    POLICY-DSL-LEX-003Invalid escape sequence
    POLICY-DSL-LEX-004Invalid number
    POLICY-DSL-PARSE-001Unexpected token
    POLICY-DSL-PARSE-002Duplicate section
    POLICY-DSL-PARSE-003Missing policy header
    POLICY-DSL-PARSE-004Unsupported syntax version (must be stella-dsl@1)
    POLICY-DSL-PARSE-005Duplicate rule name
    POLICY-DSL-PARSE-006Missing because clause
    POLICY-DSL-PARSE-007Missing terminator
    POLICY-DSL-PARSE-008Invalid action
    POLICY-DSL-PARSE-009Invalid literal
    POLICY-DSL-PARSE-010Unexpected section

    The diagnostic path (e.g. policy.syntax, policy.body, policy.name) points at the offending construct.

  2. Verify the syntax version. The parser only accepts syntax "stella-dsl@1"; any other value yields POLICY-DSL-PARSE-004.

  3. Check the server-side compile endpoint (used during version activation):

    POST /api/policy/policies/{policyId}/versions/{version}:compile
    

    Requires scope policy:edit. A compilation failure returns 400 with problem code ERR_POL_001 and the parse/lint diagnostics in the diagnostics array; a success returns the deterministic digest, statistics, complexity report, and any warning-severity diagnostics. Source: PolicyCompilationEndpoints.cs.

  4. (Optional) Run determinism lint to catch non-deterministic constructs (wall-clock, RNG, network/filesystem access) before activation:

    POST /api/v1/policy/lint/analyze         # single source
    POST /api/v1/policy/lint/analyze-batch   # bundle
    GET  /api/v1/policy/lint/rules           # list rules
    

    These require scope policy:read. Source: PolicyLintEndpoints.cs. This is a separate concern from DSL syntax compilation.


Resolution

Immediate mitigation

There is no separate “deploy” step that can leave a half-broken policy live: a version only becomes active if its source recompiles successfully (runtime reload recompiles the persisted source text — see docs/modules/policy/architecture.md §“Reload recompiles persisted source text”). A failed compilation therefore leaves the last good source active. Mitigation is to stop attempting to activate the broken version and fix the source.

  1. Keep the last good version active. Do not activate/publish the failing version; the previously compiled source continues to evaluate. Re-author and re-compile before re-activating.

  2. Revert to a prior version (CLI, backend-backed). A rollback subcommand does exist and drives the backend lifecycle (IBackendOperationsClient.RollbackPolicyAsync, CommandHandlers.HandlePolicyRollbackAsync):

    stella policy rollback <policy-id> --target-version <n> --reason "<why>"
    # optional: --env <environment> --incident <id> --tenant <id> --json
    

    With no --target-version it rolls back to the previous version. The command is network-gated (refuses in offline mode). Source: CommandFactory.cs (policy rollback, ~line 3958) → CommandHandlers.cs:15420.

  3. Lifecycle subcommands (CLI, backend-backed). Pack/version lifecycle is also driven through the CLI in addition to the Policy Engine HTTP API / Policy Studio governance flow. The policy command exposes (among others) activate, publish, promote, sign, verify-signature, plus a review group (with approve):

    stella policy activate <policy-id> --version <n> [--note "..."]
    stella policy publish  <policy-id> ...
    stella policy promote  <policy-id> ...
    

    These call backend governance endpoints (HandlePolicyActivateAsync / HandlePolicyPublishAsync / HandlePolicyPromoteAsync in CommandHandlers.cs) and are gated server-side by the policy:activate / policy:publish / policy:promote / policy:approve scopes (all present in StellaOpsScopes.cs).

Genuinely NOT IMPLEMENTED in the CLI as of 2026-05-31: the original runbook referenced stella policy disable <id>, stella policy reload, and stella policy bundle load --version <v>. None of these three subcommands exist. (Offline pack bundles are handled by stella policy export-bundle / import-bundle, not bundle load.) The earlier rollback --to-last-good flag spelling is also wrong — use rollback --target-version <n> as shown above.

Root cause fix

If a lex/parse error (POLICY-DSL-LEX-* / POLICY-DSL-PARSE-*):

  1. Re-run lint to get the exact code and path:
    stella policy lint <policy-file>.stella
    
  2. Common DSL issues:
    • Missing policy "<name>" header (POLICY-DSL-PARSE-003)
    • Wrong/absent syntax "stella-dsl@1" (POLICY-DSL-PARSE-004)
    • Missing because clause on a rule (POLICY-DSL-PARSE-006)
    • Unbalanced braces / missing terminator (POLICY-DSL-PARSE-007)
    • Duplicate rule name or duplicate section (POLICY-DSL-PARSE-005 / -002)
    • Unterminated string or bad escape (POLICY-DSL-LEX-002 / -003)
  3. Fix and re-validate:
    stella policy lint <fixed-policy>.stella
    stella policy compile <fixed-policy>.stella --checksum-only
    

If the source compiles but evaluates unexpectedly:

  1. Simulate against a signal context to inspect matched rules and actions:
    stella policy simulate <policy-file>.stella --signals signals.json --output json
    
    The output reports policyChecksum, matched rules, and the resolved actions (including else-branch actions). Source: PolicyCliCommandModule.cs.

If working with OPA/Rego or JSON PolicyPacks (interop layer):

  1. Validate against the PolicyPack v2 schema (auto-detects json/rego):
    stella policy validate --file <pack>.json
    stella policy validate --file <pack>.rego --format rego
    
  2. Import (dry-run / validate-only first):
    stella policy import --file <pack>.rego --dry-run
    
    The import mapping reports which rules map to native gates vs. those that remain OPA-evaluated (no native equivalent). Source: PolicyInteropCommandGroup.cs.
  3. Evaluate a pack against evidence input:
    stella policy evaluate --policy <pack>.json --input evidence.json --output ci
    
    Interop commands use exit codes: 0 success, 1 warnings, 2 block/errors, 10 input error, 12 policy error.

Verification

# Lint clean (no error-severity diagnostics)
stella policy lint <fixed-policy>.stella

# Compile succeeds and yields a stable checksum
stella policy compile <fixed-policy>.stella --checksum-only

# Simulate to confirm intended rule matches/actions
stella policy simulate <fixed-policy>.stella --signals signals.json

# Re-run the engine health check
stella doctor --check check.policy.engine

The previously documented stella policy deploy --file ... command does not exist, and stella policy evaluate has no --test flag (its options are --policy/--input/--format/--environment/--include-remediation/--output). Activation is performed through the Policy Engine HTTP API / Policy Studio governance flow or the backend-backed stella policy activate/publish/promote commands, not a CLI deploy command.

A stella policy test command does exist (it runs coverage fixtures against a DSL file: policy test <file> [--fixtures <dir>] [--filter <pat>] [--fail-fast], CommandFactory.cs ~line 3107 → CommandHandlers.HandlePolicyTestAsync). Use it in CI to exercise expected verdicts before activation:

stella policy test <fixed-policy>.stella --fixtures tests/policy/<name>/cases

Prevention