Policy Lifecycle & Approvals

Audience: Policy authors, reviewers, security approvers and operators. Scope: The registered Policy Engine pack/revision API and the governance work it does not implement. Verified against source: 00f64ca4d165fc54e7c291795b65326869b7627d (2026-08-31). Full Policy tests passed1916/1916; source verification is not a receipt for a deployed policy or approval. Re-verification commands are in §10.

New or significantly changed policies still require review, coverage, shadow evidence and determinism checks before enforcement. These requirements are not waived by the available metadata/activation API. The Engine does not register the separate Registry submit/review/shadow/publish workflow. If an explicitly approved procedure cannot supply required evidence, hold promotion/activation and record the gap; never invent a successful review, shadow run or attestation.

See the overview, DSL reference, governance guide and API reference. Distinguish design requirements from registered endpoints.

1 · Registered lifecycle and its limits

Program.cs registers IPolicyPackRepository as PostgresPolicyPackRepository and calls MapPolicyPacks(). Policy owns the durable policy.packs and policy.pack_versions state. The exposed revision states are Draft, Approved and Active:

Explicit Draft metadata -> compile/store bundle -> explicit Approved metadata -> activation
                                                       |
                                  configured two-person activation: first actor = pending

This is the operational sequence over the actual API, not an enforced review state machine.

2 · Authorization

Engine operationRequired scope
Create pack, upsert revision metadata, compile/store bundlepolicy:edit
List pack summaries, evaluate a specified revision, read capabilitiespolicy:read
Activate an approved revisionpolicy:activate

Use an Authority-issued identity for the intended tenant. Tenant identity comes from validated identity, not a caller-supplied tenant header. Use real authenticated operator identities for activation; never fabricate an actor to satisfy the distinct-actor check. Other Policy scopes do not make Registry review or publication available.

Both native HTTP and Router-dispatched pack operations require that validated identity in Policy’s request-local tenant context. Router invokes the endpoint delegate without the HTTP tenant middleware, so the pack group’s RequireTenantContext() filter is required. Missing context must fail closed; it must not select public. Pack queries also predicate the captured tenant explicitly: an RLS-enabled table does not isolate a database-owner connection that bypasses RLS. A token’s tenant is not proof of the tenant under which an older request stored data.

An exact operator project wildcard * is normalized to no project refinement consistently on both transports. Pack evaluation’s cache and correlation identity include the captured tenant and exact pack/version; content-equivalent revisions do not share response identities or attestation references across those boundaries.

3 · Durable Engine procedure

Mutate only the approved policy/target. Record runtime/source identity, tenant, pack, positive integer revision and reviewed source/bundle digest. Keep secret values in the existing credential channel, not examples, logs or receipts.

3.1 Create the pack and explicit Draft metadata

  1. POST /api/policy/packs accepts packId and optional displayName. Retain the returned identity; creating the container does not compile a policy.
  2. POST /api/policy/packs/{packId}/revisions accepts explicit positive version, initialStatus: "Draft" and the approved requiresTwoPersonApproval requirement. Do not omit status: the DTO defaults to Approved.
  3. Verify the returned status and requiresTwoPersonApproval.

PolicyEngine:Activation:ForceTwoPersonApproval takes precedence; otherwise an explicit request wins over DefaultRequiresTwoPersonApproval. The requirement is persisted on first creation: upserting an existing revision does not change it. Direct bundle storage can create a Draft with the repository’s false requirement, so create metadata first. Do not assume a later configuration change retrofits existing revisions.

3.2 Compile and retain the exact bundle

POST /api/policy/packs/{packId}/revisions/{version}/bundle accepts dsl with syntax and source, optional signingKeyId, and optional provenance. Use reviewed stella-dsl@1 source. Check HTTP success and response success, diagnostics, digest, size and source/artifact metadata. Compilation failure returns HTTP 400.

The bundle route requires a positive revision. Zero or negative values return HTTP 400 with error_code: policy.bundle.invalid_revision before compilation or storage; no Location is returned. A positive revision need not already exist, although creating reviewed Draft metadata first preserves the activation requirement described above. To allocate a revision, call the revision metadata route with version omitted and use the returned positive version in the bundle request. Allocation starts at 1 for empty, zero-only or negative-only revision sets; otherwise it follows the highest stored positive revision. Existing imported values are not rewritten. Its Location identifies the same pack/revision tuple that was stored.

The assigned pack identity and positive allocation floor were verified at source b5c85b895aa7d44d43f10dee248bcde697c79f61. Re-verify with pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Policy/__Tests/StellaOps.Policy.Engine.Tests/StellaOps.Policy.Engine.Tests.csproj -Class "*PolicyPackVersionAssignedIdentityTests" -BuildProjectReferences -Restore. These disposable owner-storage controls do not establish a public import host, trusted signing or live authorization.

Validation errors use application/problem+json with ProblemDetails. Compilation errors keep the existing application/json response with success: false and diagnostics.

The repository stores source text/syntax, payload, digest and compilation metadata. Reload recompiles persisted source. Storage does not approve the Draft; replacing reviewed bundle bytes cannot inherit the old review.

Signing limit: PolicyBundleService.Sign is an explicit deterministic offline-testing signature stub. Its sig:sha256:... value is not DSSE or an operator signature; this method sets no attestation reference. It does not satisfy trusted publication or approval evidence.

The positive bundle revision contract was verified at source e2a457c47d96d04656f7e5af140d51c3239bce57. Re-verify with pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Policy/__Tests/StellaOps.Policy.Engine.Tests/StellaOps.Policy.Engine.Tests.csproj -Class "*PolicyPackVersionAssignedIdentityTests" -BuildProjectReferences. This source/fixture check does not establish live authorization or trusted signing.

3.3 Verify content and real governance evidence

Run applicable lint, coverage and deterministic checks against the exact source. POST /api/policy/packs/{packId}/revisions/{version}/evaluate accepts matching packId, version and the intended subject. The body pack ID must match the route exactly, including case. A name or version mismatch returns 400 before bundle lookup or evaluation; the existing mismatch body is retained. Both 400 and 404 use application/json ProblemDetails. The route returns 404 when the bundle is missing. It evaluates a specified bundle, not a Registry simulation or shadow run.

The exact evaluation identity and error-response contract was verified at source f7476d456ffa435578775944902b279d721c5b05. Re-verify with pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Policy/__Tests/StellaOps.Policy.Engine.Tests/StellaOps.Policy.Engine.Tests.csproj -Class "*PolicyPackVersionAssignedIdentityTests" -BuildProjectReferences. This source/fixture check uses an unavailable Attestor adapter and does not establish trusted attestation or live authorization.

Retain actual reviewer decisions, rationale and required shadow/coverage results. Evaluation success is not authorization to activate. Missing evidence remains a gap under the governing procedure; the metadata endpoint does not validate attachments or reviewer quorum.

3.4 Record Approved metadata explicitly

After governing review requirements are satisfied, upsert the same explicit revision with initialStatus: "Approved" and verify its response. This is a metadata operation, not Registry submission/review or a DSSE publication. Do not infer approval from compilation success or the storage column name is_published.

3.5 Activate with the configured actor threshold

POST /api/policy/packs/{packId}/revisions/{version}:activate accepts a JSON body containing optional comment and requires policy:activate.

ResponseMeaning
202, pending_second_approvalFirst distinct approval recorded; not active yet.
200, activatedThreshold met and pack active-version pointer changed.
200, already_activeAlready active; not a new approval or forcing proof.
400, not approved / approval already recordedDraft or duplicate actor refused.
404Pack or revision missing.

When required, a second legitimate identity in the same tenant must approve. Actor IDs are compared case-insensitively. This is not an author-versus-reviewer separation check: preserve stronger organizational separation as a real prerequisite. Retain the response and approvals; neither pending status nor a repeated identity completes activation.

3.6 Verify the actual consumer separately

Activation updates Policy state and records the attempt. It does not enqueue a full run, promote a Release Orchestrator environment or prove consumer selection of the intended bundle. Verify exact pack/revision/digest and positive/negative forcing cases through the approved consumer workflow. Metadata success is not release-gate acceptance.

3.7 Archival and rollback boundary

MapPolicyPacks has no archive transition. A previous approved revision can be a rollback candidate through the same activation endpoint, but retained approvals and actual responses must be checked; do not assume a fresh quorum or automatic run. Retain incident/change approval and verify the consumer separately. Do not edit database rows, erase approvals or invent an archive endpoint to finish the procedure.

4 · Tooling availability

5 · Audit and persistence evidence

PostgresPolicyPackRepository stores revision metadata and activation approvals in policy.packs / policy.pack_versions. Pack creation, revision metadata changes and activation carry endpoint audit metadata. PolicyActivationAuditor emits structured attempt logs when PolicyEngine:Activation:EmitAuditLogs is enabled.

Retain actual responses, actor/tenant, source/bundle digests, governing approval and consumer receipts. This path does not establish Registry reviewer comments, submission attachments, publish/archive events or signed approval history. A separate post-evaluation verdict attestation is not a policy-approval/publication attestation.

6 · Required governance, unavailable automation

Requirement retainedCurrent Engine limit
Tenant/scope and Approved activation stateRegistered checks; not full lifecycle governance.
Reviewer sign-off, separation and unresolved commentsRegistry workflow unregistered; Approved metadata is not evidence.
Shadow, coverage and twin-run determinismRequired for relevant changes; no registered shadow promotion gate validates them here.
Reason/ticket and trusted DSSE for governed publicationRequired when publishing; the compile signature stub does not satisfy them.
Incident rollback, preservation and consumer acceptanceSeparate approved operation, not automatic run/archive choreography.

GET /policy/capabilities requires policy:read and reports simulation.available and shadowMode.available; both source defaults are false. Registered shadow/simulation and promotion-gate compatibility handlers return HTTP 501. Flipping capability flags does not register a backend. Keep future Registry requirements explicit until durable registration and real positive/negative evidence are delivered together.

7 · Offline and air-gap boundary

The lifecycle still requires the local Policy owner service and durable store. Air-gap operation does not enable Registry review/shadow/publication. Preserve exact source, inputs, digests and trusted signatures through the approved offline procedure; this API does not establish automatic Offline Kit export of Registry approval records.

8 · Incident response

Retain current active identity/evidence, stop promotion on failed required checks and follow §3.7. This lifecycle does not implement automatic incident sampling, retention changes or evidence exports; use their actual owner controls and verify separately.

9 · Completion checklist

10 · Source re-verification

From the repository root:

rg -n 'PostgresPolicyPackRepository|MapPolicyPacks|MapPolicyCapabilitiesEndpoints|MapPolicySimulationEndpoints' src/Policy/StellaOps.Policy.Engine/Program.cs
rg -n 'MapPost|RequireStellaOpsScopes|InitialStatus|ActivatePolicyRevisionRequest' src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyPackEndpoints.cs
rg -n 'UpsertRevisionAsync|StoreBundleAsync|RecordActivationAsync|is_published|requires_two_person_approval|ActivationApprovals' src/Policy/StellaOps.Policy.Engine/Services/PostgresPolicyPackRepository.cs
rg -n 'ForceTwoPersonApproval|DefaultRequiresTwoPersonApproval|requested' src/Policy/StellaOps.Policy.Engine/Services/PolicyActivationSettings.cs
rg -n 'signature stub|AttestationRef: null|private static string Sign' src/Policy/StellaOps.Policy.Engine/Services/PolicyBundleService.cs
rg -n 'CompatibilityUnavailable|Status501NotImplemented' src/Policy/StellaOps.Policy.Engine/Endpoints/Gateway/PolicySimulationEndpoints.cs
rg -n 'api/policy/policies/.+versions/.+:activate' src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs

Run the actual transport/repository regression tests after changing the tenant boundary:

pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Policy/__Tests/StellaOps.Policy.Engine.Tests/StellaOps.Policy.Engine.Tests.csproj -Class '*PolicyPackRouterTenantContextTests' -BuildProjectReferences -Restore

These tests include an owner-role database with RLS bypass, signed Router identity, isolated same-name packs, all six lifecycle routes and missing-context rejection. They are local conformance, not a deployed policy activation receipt.

Read the enclosing methods, not only matching lines. Reconfirm live capabilities, mapped routes, persisted requirements and consumer behavior before operational use.