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:
- Auditability: Verify what thresholds were enforced at decision time
- Reproducibility: Include all inputs for deterministic verification
- Compliance: Demonstrate policy enforcement for regulatory requirements
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
| Field | Type | Description |
|---|---|---|
maxUnknownCount | int | Maximum total unknowns allowed |
maxCumulativeUncertainty | double | Maximum uncertainty score |
action | string | Action when exceeded: warn, block |
reasonLimits | object | Per-reason code limits |
budgetCheck.actualCounts
| Field | Type | Description |
|---|---|---|
total | int | Total unknowns observed |
cumulativeUncertainty | double | Sum of uncertainty factors |
byReason | object | Breakdown by reason code |
budgetCheck.result
Possible values:
pass- All limits satisfiedwarn- Limits exceeded but action is warnfail- Limits exceeded and action is block
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:
- Configuration is serialized to JSON with canonical ordering
- SHA-256 is computed over the UTF-8 bytes
- 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
Inventory and tenant ownership were verified against source commit 6a8e9f038d on 2026-10-07. Re-run PolicyEngineRiskBudgetInventoryTests, PostgresBudgetStoreTests and BudgetInventoryTests; the owner-path receipt is docs/implplan/_evidence/20261007-policy-budget-owner/pcd-451.json. Reason/history and atomic consumption were verified against d2a4147aee on 2026-10-07. Re-run PolicyEngineRiskBudgetReasonTests, BudgetReasonTests and the storage/CLI budget tests; the companion owner-path receipt is pcd-452.json in that evidence directory.
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). Release checks derive risk points from the service tier, change category, operational context, and mitigation factors. Consumption records explicitly supplied risk points against a release ID and fails when the remaining budget is insufficient.
Key Concepts
Service Tiers
Services are classified by criticality, which determines their risk budget allocation:
| Tier | Name | Monthly Allocation | Description |
|---|---|---|---|
| 0 | Internal | 300 RP | Internal-only, low business impact |
| 1 | Customer-Facing Non-Critical | 200 RP | Customer-facing but non-critical |
| 2 | Customer-Facing Critical | 120 RP | Critical customer-facing services |
| 3 | Safety-Critical | 80 RP | Safety, financial, or data-critical |
Budget Status Thresholds
Budget status transitions based on percentage consumed:
| Status | Threshold | Behavior |
|---|---|---|
| Green | < 40% consumed | Normal operations |
| Yellow | 40-69% consumed | Increased caution, warnings triggered |
| Red | 70-99% consumed | High-risk diffs frozen, only low-risk allowed |
| Exhausted | >= 100% consumed | Incident and security fixes only |
Budget Windows
- Default cadence: Monthly (YYYY-MM format)
- Reset behavior: No carry-over; unused budget expires
- Window boundary: UTC midnight on the 1st of each month
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:11111111-1111-1111-1111-111111111111:my-service:2025-12",
"serviceId": "my-service",
"tier": "CustomerFacingNonCritical",
"window": "2025-12",
"allocated": 200,
"consumed": 85,
"remaining": 115,
"percentageUsed": 42.5,
"status": "yellow",
"updatedAt": "2025-12-15T10:30:00Z"
}
Record Consumption
POST /api/v1/policy/budget/consume
Content-Type: application/json
{
"serviceId": "my-service",
"riskPoints": 25,
"releaseId": "v1.2.3",
"reason": "Release approved after the maintenance review"
}
Consumption preserves an optional reason of at most 512 UTF-16 code units in the tenant’s history entry. Omission stores SQL NULL. Operator text, including Unicode, is retained unchanged in PostgreSQL and exported as UTF-8 JSON. A successful consumption atomically deducts points and appends its entry; an entry failure rolls back the debit. Concurrent requests check headroom against the locked current row. Each request appends separately, including repeated release IDs; release ID is not an idempotency key. The owner consumption request requires positive risk points and nonblank service/release identifiers of at most 128 UTF-16 code units each. Invalid inputs return HTTP 400 without an entry or debit. Internal calculated zero-risk releases can retain a reasoned entry without changing the balance.
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}
Each entry includes entryId, releaseId, riskPoints, consumedAt and optional reason. The reason reflects the persisted consumption entry, including after a new owner host reads it. History is scoped to the admitted tenant and requested or active window, ordered by consumed time descending and entry ID as an ordinal tie-breaker.
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}
The inventory queries persisted budgets owned by the admitted tenant. status accepts green, yellow, red or exhausted (case-insensitive). An explicit window must be yyyy-MM; omission includes all persisted windows. limit defaults to 50 and accepts 1 through 1000. totalCount is the number of matching rows before applying the limit. Rows are ordered by update time descending, then service, window and budget ID.
A successful empty inventory means no persisted budgets match those tenant-bound filters. The read does not create default budgets. Invalid inputs return HTTP 400; unavailable PostgreSQL returns HTTP 503 with error_code: policy.budget.persistence_unavailable. An inactive or unresolved tenant returns HTTP 400 with policy.budget.tenant_invalid before budget data access. Missing tenant admission is rejected by the tenant filter.
Forward migration 021_budget_tenant_inventory.sql keys service/window budgets by tenant and scopes consumption history to the same owner. Existing history inherits ownership only from its actual ledger row. Unattributed historical rows remain stored and are excluded by the strict tenant policies; the migration assigns no fallback tenant.
CLI Commands
The budget command guide describes all options, scopes, owner response fields and failure behavior. These commands require an authenticated tenant and a reachable Policy Engine through the configured backend.
Check Status
stella budget status --service my-service --window 2026-10 --output json
Status retains the owner’s budget ID, named string tier, window, allocated/consumed/remaining points, percentage used, status and update timestamp.
Consume Budget
stella budget consume --service my-service --points 25 --release-id v1.2.3 --reason "Release v1.2.3"
--points becomes riskPoints in the owner request. The release ID is required. The optional reason is preserved in the tenant’s consumption entry, with a maximum of 512 UTF-16 code units.
Check a Proposed Release
stella budget check --service my-service --tier CustomerFacingNonCritical --diff-category ApiBackwardCompatible --rollback-plan --output json
Policy computes the risk points from category, operational context and mitigation inputs. Blocked decisions exit nonzero by default (--fail-on-block true); a check does not consume points.
View History
stella budget history --service my-service --window 2026-10 --limit 20 --output json
The owner returns the full window. --limit only limits text output; JSON retains all returned entries and their recorded reasons. History has neither server paging nor a total count.
List Budgets
stella budget list --status yellow --window 2026-10 --limit 50
The status filter accepts a single state. The CLI preserves authoritative empty inventory and exits zero. Persistence, transport and malformed-result failures exit nonzero without emitting a budget result.
Earned Capacity Replenishment
Services demonstrating improved reliability can earn additional budget capacity:
Eligibility Criteria
- MTTR Improvement: Mean Time to Remediate must improve for 2 consecutive windows
- CFR Improvement: Change Failure Rate must improve for 2 consecutive windows
- No Major Incidents: No P1 incidents in the evaluation period
Increase Calculation
- Minimum increase: 10% of base allocation
- Maximum increase: 20% of base allocation
- Scale: Proportional to improvement magnitude
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:
- Email (to service owners)
- Slack (to designated channel)
- Microsoft Teams (to designated channel)
- Webhooks (for integration)
Database Schema
Relevant owner columns and keys after startup convergence (additional audit fields omitted):
CREATE TABLE policy.budget_ledger (
budget_id VARCHAR(256) PRIMARY KEY,
service_id VARCHAR(128) NOT NULL,
tenant_id VARCHAR(64),
tier INTEGER NOT NULL,
"window" VARCHAR(16) NOT NULL,
allocated INTEGER NOT NULL,
consumed INTEGER NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'green',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, service_id, "window")
);
CREATE TABLE policy.budget_entries (
entry_id VARCHAR(64) PRIMARY KEY,
tenant_id VARCHAR(64),
service_id VARCHAR(128) NOT NULL,
"window" VARCHAR(16) NOT NULL,
release_id VARCHAR(128) NOT NULL,
risk_points INTEGER NOT NULL,
reason VARCHAR(512),
consumed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (tenant_id, service_id, "window")
REFERENCES policy.budget_ledger(tenant_id, service_id, "window") ON DELETE CASCADE
);
CREATE INDEX idx_budget_entries_tenant_service_window_consumed
ON policy.budget_entries(tenant_id, service_id, "window", consumed_at DESC, entry_id);
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
Related Documentation
- Unknown Budget Gates (not published)
- Verdict Attestations (not published)
- BudgetCheckPredicate Model (not published)
- Risk Point Scoring (not published)
- Diff-Aware Release Gates (not published)
