Budget Threshold Attestation

This document describes how unknown budget thresholds are attested in verdict bundles for reproducibility and audit purposes.

Overview

Budget attestation captures the budget configuration applied during policy evaluation, enabling:

Budget Check Predicate

The budget check is included in the verdict predicate:

{
  "_type": "https://stellaops.dev/predicates/policy-verdict@v1",
  "tenantId": "tenant-1",
  "policyId": "default-policy",
  "policyVersion": 1,
  "verdict": { ... },
  "budgetCheck": {
    "environment": "production",
    "config": {
      "maxUnknownCount": 10,
      "maxCumulativeUncertainty": 2.5,
      "action": "warn",
      "reasonLimits": {
        "Reachability": 5,
        "Identity": 3
      }
    },
    "actualCounts": {
      "total": 3,
      "cumulativeUncertainty": 1.2,
      "byReason": {
        "Reachability": 2,
        "Identity": 1
      }
    },
    "result": "pass",
    "configHash": "sha256:abc123...",
    "evaluatedAt": "2025-12-25T12:00:00Z",
    "violations": []
  }
}

Fields

budgetCheck.config

FieldTypeDescription
maxUnknownCountintMaximum total unknowns allowed
maxCumulativeUncertaintydoubleMaximum uncertainty score
actionstringAction when exceeded: warn, block
reasonLimitsobjectPer-reason code limits

budgetCheck.actualCounts

FieldTypeDescription
totalintTotal unknowns observed
cumulativeUncertaintydoubleSum of uncertainty factors
byReasonobjectBreakdown by reason code

budgetCheck.result

Possible values:

budgetCheck.configHash

SHA-256 hash of the budget configuration for determinism verification. Format: sha256:{64 hex characters}

budgetCheck.violations

List of violations when limits are exceeded:

{
  "violations": [
    {
      "type": "total",
      "limit": 10,
      "actual": 15
    },
    {
      "type": "reason",
      "limit": 5,
      "actual": 8,
      "reason": "Reachability"
    }
  ]
}

Usage

Extracting Budget Check from Verdict

using StellaOps.Policy.Engine.Attestation;

// Parse verdict predicate from DSSE envelope
var predicate = VerdictPredicate.Parse(dssePayload);

// Access budget check
if (predicate.BudgetCheck is not null)
{
    var check = predicate.BudgetCheck;
    Console.WriteLine($"Environment: {check.Environment}");
    Console.WriteLine($"Result: {check.Result}");
    Console.WriteLine($"Total: {check.ActualCounts.Total}/{check.Config.MaxUnknownCount}");
    Console.WriteLine($"Config Hash: {check.ConfigHash}");
}

Verifying Configuration Hash

// Compute expected hash from current configuration
var currentConfig = new VerdictBudgetConfig(
    maxUnknownCount: 10,
    maxCumulativeUncertainty: 2.5,
    action: "warn");

var expectedHash = VerdictBudgetCheck.ComputeConfigHash(currentConfig);

// Compare with attested hash
if (predicate.BudgetCheck?.ConfigHash != expectedHash)
{
    Console.WriteLine("Warning: Budget configuration has changed since attestation");
}

Determinism

The config hash ensures reproducibility:

  1. Configuration is serialized to JSON with canonical ordering
  2. SHA-256 is computed over the UTF-8 bytes
  3. Hash is prefixed with sha256: algorithm identifier

This allows verification that the same budget configuration was used across runs.

Integration Points

VerdictPredicateBuilder

Budget check is added when building verdict predicates:

var budgetCheck = new VerdictBudgetCheck(
    environment: context.Environment,
    config: config,
    actualCounts: counts,
    result: budgetResult.Passed ? "pass" : budgetResult.Budget.Action.ToString(),
    configHash: VerdictBudgetCheck.ComputeConfigHash(config),
    evaluatedAt: DateTimeOffset.UtcNow,
    violations: violations);

var predicate = new VerdictPredicate(
    tenantId: trace.TenantId,
    policyId: trace.PolicyId,
    // ... other fields
    budgetCheck: budgetCheck);

UnknownBudgetService

The enhanced BudgetCheckResult includes all data needed for attestation:

var result = await budgetService.CheckBudget(environment, unknowns);

// result.Budget - the configuration applied
// result.CountsByReason - breakdown for attestation
// result.CumulativeUncertainty - total uncertainty score

Risk Budget Enforcement

This section describes the risk budget enforcement system that tracks and controls release risk accumulation over time.

Overview

Risk budgets limit the cumulative risk accepted during a budget window (typically monthly). Each release consumes risk points based on the vulnerabilities it introduces or carries forward. When a budget is exhausted, further high-risk releases are blocked.

Key Concepts

Service Tiers

Services are classified by criticality, which determines their risk budget allocation:

TierNameMonthly AllocationDescription
0Internal300 RPInternal-only, low business impact
1Customer-Facing Non-Critical200 RPCustomer-facing but non-critical
2Customer-Facing Critical120 RPCritical customer-facing services
3Safety-Critical80 RPSafety, financial, or data-critical

Budget Status Thresholds

Budget status transitions based on percentage consumed:

StatusThresholdBehavior
Green< 40% consumedNormal operations
Yellow40-69% consumedIncreased caution, warnings triggered
Red70-99% consumedHigh-risk diffs frozen, only low-risk allowed
Exhausted>= 100% consumedIncident and security fixes only

Budget Windows

API Endpoints

The release risk-budget family is mounted by Policy Engine at /api/v1/policy/budget and requires an authenticated tenant context for every route. Read operations require policy:read; consumption requires policy:operate; allocation adjustment requires policy:edit. The separate unknown-budget configuration/status family is mounted at /api/v1/policy/budgets and applies the same tenant-context endpoint filter. This is required for Router transport dispatch because it invokes the matched endpoint without Policy’s tenant middleware: the filter bridges the already verified signed-envelope identity into Policy’s request-local tenant accessor for the handler invocation. Tenant-sensitive reads then normalize that identity through the canonical tenant directory before data access: UUID claims pass through, active slugs such as default resolve to shared.tenants.id, and missing, inactive, or unknown identifiers return HTTP 400 without constructing the tenant-scoped repository. The same canonical UUID establishes the PostgreSQL RLS session; no synthetic or public tenant fallback is permitted. Both families use the Policy PostgreSQL schema, which is migrated automatically by Policy Engine at startup.

Check Budget Status

GET /api/v1/policy/budget/status/{serviceId}?window={yyyy-MM}

Response:

{
  "budgetId": "budget:my-service:2025-12",
  "serviceId": "my-service",
  "tier": 1,
  "window": "2025-12",
  "allocated": 200,
  "consumed": 85,
  "remaining": 115,
  "percentageUsed": 42.5,
  "status": "Yellow"
}

Record Consumption

POST /api/v1/policy/budget/consume
Content-Type: application/json

{
  "serviceId": "my-service",
  "riskPoints": 25,
  "releaseId": "v1.2.3"
}

Adjust Allocation (Earned Capacity)

POST /api/v1/policy/budget/adjust
Content-Type: application/json

{
  "serviceId": "my-service",
  "adjustment": 40,
  "reason": "MTTR improvement over 2 months"
}

View History

GET /api/v1/policy/budget/history/{serviceId}?window={yyyy-MM}

Check a Proposed Release

POST /api/v1/policy/budget/check
Content-Type: application/json

List Budgets

GET /api/v1/policy/budget/list?status={status}&window={yyyy-MM}&limit={limit}

CLI Commands

Check Status

stella budget status --service my-service

Output:

Service: my-service
Window:  2025-12
Tier:    Customer-Facing Non-Critical (1)
Status:  Yellow

Budget:  85 / 200 RP (42.5%)
         ████████░░░░░░░░░░░░

Remaining: 115 RP

Consume Budget

stella budget consume --service my-service --points 25 --reason "Release v1.2.3"

List All Budgets

stella budget list --status Yellow,Red

Earned Capacity Replenishment

Services demonstrating improved reliability can earn additional budget capacity:

Eligibility Criteria

  1. MTTR Improvement: Mean Time to Remediate must improve for 2 consecutive windows
  2. CFR Improvement: Change Failure Rate must improve for 2 consecutive windows
  3. No Major Incidents: No P1 incidents in the evaluation period

Increase Calculation

Example

Service: payment-api (Tier 2, base 120 RP)
MTTR: 48h → 36h → 24h (50% improvement)
CFR:  15% → 12% → 8%  (47% improvement)

Earned capacity: +20% = 24 RP
New allocation: 144 RP for next window

Notifications

Budget threshold transitions trigger notifications:

Warning (Yellow)

Sent when budget reaches 40% consumption:

Subject: [Warning] Risk Budget at 40% for my-service

Your risk budget for my-service has reached the warning threshold.

Current: 80 / 200 RP (40%)
Status: Yellow

Consider pausing non-critical changes until the next budget window.

Critical (Red/Exhausted)

Sent when budget reaches 70% or 100%:

Subject: [Critical] Risk Budget Exhausted for my-service

Your risk budget for my-service has been exhausted.

Current: 200 / 200 RP (100%)
Status: Exhausted

Only security fixes and incident responses are allowed.
Contact the Platform team for emergency capacity.

Channels

Notifications are sent via:

Database Schema

CREATE TABLE policy.budget_ledger (
    budget_id      TEXT PRIMARY KEY,
    service_id     TEXT NOT NULL,
    tenant_id      TEXT,
    tier           INTEGER NOT NULL,
    window         TEXT NOT NULL,
    allocated      INTEGER NOT NULL,
    consumed       INTEGER NOT NULL DEFAULT 0,
    status         TEXT NOT NULL DEFAULT 'green',
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(service_id, window)
);

CREATE TABLE policy.budget_entries (
    entry_id       TEXT PRIMARY KEY,
    service_id     TEXT NOT NULL,
    window         TEXT NOT NULL,
    release_id     TEXT NOT NULL,
    risk_points    INTEGER NOT NULL,
    consumed_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    FOREIGN KEY (service_id, window) REFERENCES policy.budget_ledger(service_id, window)
);

CREATE INDEX idx_budget_entries_service_window ON policy.budget_entries(service_id, window);

Configuration

# etc/policy.yaml
policy:
  riskBudget:
    enabled: true
    windowCadence: monthly  # monthly | weekly | sprint
    carryOver: false
    defaultTier: 1

    tiers:
      0: { name: Internal, allocation: 300 }
      1: { name: CustomerFacingNonCritical, allocation: 200 }
      2: { name: CustomerFacingCritical, allocation: 120 }
      3: { name: SafetyCritical, allocation: 80 }

    thresholds:
      yellow: 40
      red: 70
      exhausted: 100

    notifications:
      enabled: true
      channels: [email, slack]
      aggregationWindow: 1h  # Debounce rapid transitions

    earnedCapacity:
      enabled: true
      requiredImprovementWindows: 2
      minIncreasePercent: 10
      maxIncreasePercent: 20