Exception Approval Workflow
Audience: security and release-governance owners who request, approve, or audit policy exceptions, and developers working on the Policy exception subsystem (
src/Policy).
Reconciliation status (verified against
src/Policy): This flow was rewritten to match the implemented exception subsystem. The implementation provides two related surfaces:
- Exception Objects —
/api/policy/exceptions(lifecycle CRUD + governed state machine). Code:StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs,StellaOps.Policy.Engine/Endpoints/Gateway/ExceptionEndpoints.cs, and theStellaOps.Policy.Exceptionslibrary.- Exception Approval Requests —
/api/v1/policy/exception(gate-level multi-approver workflow). Code:StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs,StellaOps.Policy.Engine/Services/ExceptionApprovalRulesService.cs, and theExceptionApprovalRequestEntity/ExceptionApprovalRuleEntitypersistence models.Both surfaces are mapped by both the Policy Engine and the Policy Gateway (
Program.cs:MapExceptionEndpoints()+MapExceptionApprovalEndpoints()). Org-role concepts such as “Team Lead”, “Security Team”, and “CISO” are not system actors — they exist only as configurable approver role strings in gate-level rules (code-owner,delivery-manager,product-manager,ciso).Automatic expiry and expiry-warning emission are implemented as in-process hosted
BackgroundServiceworkers (ExceptionExpiryWorker,ExceptionLifecycleWorker); lifecycle notifications are emitted to the unified audit/Timeline emitter (IExceptionNotificationService, disabled by default), not the standalonesrc/Notifymodule. See Expiration & Lifecycle. (A prior reconciliation pass incorrectly stated these were absent.)
Overview
The Exception Approval Workflow describes how StellaOps handles policy exception requests, from submission through gate-level approval, time-limited validity, and expiry. Exceptions are first-class auditable entities (not suppression files or UI toggles) with full lifecycle tracking, immutable event history, and deterministic evaluation.
Business Value: Maintain security governance while allowing controlled, time-bounded exceptions for legitimate business needs, with a complete audit trail and enforced separation-of-duty.
Actors
| Actor | Type | Role in the implemented flow |
|---|---|---|
| Requester | Human | Creates an exception object (policy:author) or an approval request (exceptions:request). |
| Approver | Human | Approves/rejects via the gate-level workflow (exceptions:approve) or approves an exception object (policy:operate). Must hold the required role(s) for the gate level. |
| Policy Engine / Policy Gateway | Service | Hosts the exception and approval-request APIs, evaluates approval rules, enforces exceptions during policy evaluation. |
| Expiry / lifecycle workers | Service (background) | Hosted BackgroundService workers in both Policy Gateway and Policy Engine that auto-expire stale exceptions and emit expiry warnings. See Expiration & Lifecycle. |
No external Scheduler/JobEngine dependency: expiry and lifecycle transitions run as in-process hosted workers in the Policy services (not as JobEngine jobs). Lifecycle notifications are emitted to the unified audit/Timeline emitter, not to the standalone
src/Notifychannel-dispatch module (no email/Slack templates exist). See Expiration & Lifecycle and Not Implemented / Roadmap.
Prerequisites
- Caller authenticated with the appropriate scope (see Authorization).
- Tenant context present (all endpoint groups call
RequireTenant()). - For the gate-level workflow: approval rules exist for the tenant, or the built-in per-gate defaults apply.
Authorization
Scopes are sourced from the canonical catalog (StellaOps.Auth.Abstractions/StellaOpsScopes.cs). Note the two surfaces use different scopes.
Exception Objects (/api/policy/exceptions)
| Operation | Required scope |
|---|---|
| List / counts / get / history / expiring | policy:read |
| Create (POST) / update (PUT) | policy:author |
| Approve / activate / extend / revoke (DELETE) | policy:operate |
Exception Approval Requests (/api/v1/policy/exception)
| Operation | Required scope |
|---|---|
| Create request / cancel | exceptions:request |
| Get / list / audit / rules | exceptions:read |
| List pending (approver inbox) / approve / reject | exceptions:approve |
Exception Object Model
An exception object (ExceptionObject) is a versioned, append-only-audited entity.
Status (lifecycle)
Proposed → Approved → Active → Expired
Active → Revoked
Proposed/Approved/Active → Revoked (early termination / rejection)
| Status | Meaning |
|---|---|
proposed | Requested, awaiting approval. |
approved | Approved, awaiting activation. |
active | Currently affecting policy evaluation. |
expired | expires_at reached. |
revoked | Explicitly revoked (or rejected before approval). |
Type
| Type | Meaning |
|---|---|
vulnerability | Exception for a specific CVE/CWE. |
policy | Bypass of a specific policy rule. |
unknown | Allow release despite unknown findings. |
component | Exception for a specific component/package. |
Reason code (ExceptionReason)
false_positive, accepted_risk, compensating_control, test_only, vendor_not_affected, scheduled_fix, deprecation_in_progress, runtime_mitigation, network_isolation, other.
Scope (ExceptionScope)
At least one constraint must be set; multiple constraints combine with AND.
| Field | Notes |
|---|---|
artifact_digest | sha256:..., exact match (most specific). |
purl_pattern | Supports * wildcards (e.g. pkg:npm/lodash@*, pkg:maven/org.apache.logging.log4j/*). |
vulnerability_id | Exact CVE/CWE ID. |
policy_rule_id | Policy rule being bypassed. |
environments | Allowed environments; empty array = all environments. |
tenant_id | RLS tenant binding. |
Gate-Level Approval Rules
The approval-request workflow (/api/v1/policy/exception) routes each request to a gate level (G0–G4). If no tenant-specific ExceptionApprovalRuleEntity is configured (and enabled), these built-in defaults apply (ApprovalRequirements.GetDefault):
| Gate | MinApprovers | Required roles | MaxTtlDays | Self-approval | Evidence req. | Comp. controls req. | MinRationaleLength |
|---|---|---|---|---|---|---|---|
| G0 (informational) | 0 (auto-approve) | — | 90 | yes | no | no | 0 |
| G1 (low) | 1 | — | 60 | yes | no | no | 20 |
| G2 (medium) | 1 | code-owner | 30 | no | yes | no | 50 |
| G3 (high) | 2 | delivery-manager, product-manager | 14 | no | yes | yes | 100 |
| G4 (critical) | 3 | ciso, delivery-manager, product-manager | 7 | no | yes | yes | 200 |
Notes:
- Unparseable gate level defaults to G1; unparseable reason code defaults to other.
- Requested TTL is rejected if it exceeds the gate’s
MaxTtlDays. G0requests withMinApprovers == 0are auto-approved at creation (CanAutoApproveAsync).- A global
MinJustificationLength(default 10) is enforced on every request.
Flow Diagram (gate-level approval request)
┌──────────────────────────────────────────────────────────────────────┐
│ Exception Approval Request Workflow (gate-level) │
└──────────────────────────────────────────────────────────────────────┘
┌───────────┐ ┌──────────────────────┐ ┌──────────────┐
│ Requester │ │ Policy Engine/Gateway │ │ Approver(s) │
└─────┬─────┘ └──────────┬───────────┘ └──────┬───────┘
│ POST /request │ │
│ (justification, gate level, │ │
│ scope, ttl, evidence) │ │
│─────────────────────────────────>│ │
│ │ Resolve requirements │
│ │ (tenant rule || default)│
│ │ Validate TTL/rationale/ │
│ │ evidence/controls │
│ │───┐ │
│ │<──┘ │
│ │ If G0 / MinApprovers==0 │
│ │ → auto-approve │
│ │ Persist + audit │
│ 201 Created (EAR-…, status, │ │
│ warnings) │ │
│<─────────────────────────────────│ │
│ │ │
│ │ GET /pending │
│ │<────────────────────────│
│ │ (approver inbox) │
│ │ POST /{id}/approve │
│ │<────────────────────────│
│ │ Validate action: │
│ │ - not already approved │
│ │ - self-approval rule │
│ │ - in required list │
│ │ - role coverage │
│ │ Record approval; when │
│ │ MinApprovers met → │
│ │ status = Approved │
│ │ │
│ │ POST /{id}/reject │
│ │ (mandatory reason) │
│ │<────────────────────────│
│ │ │
Step-by-Step (gate-level approval request)
1. Create approval request
POST /api/v1/policy/exception/request (scope exceptions:request).
Request body (CreateApprovalRequestDto):
{
"exceptionId": "EXC-… (optional link to an exception object)",
"justification": "Upgrading lodash breaks the auth flow; need time to refactor.",
"rationale": "Vulnerability requires prototype pollution not reachable in our usage.",
"gateLevel": "G3",
"reasonCode": "scheduled_fix",
"evidenceRefs": ["sha256:…"],
"compensatingControls": ["WAF rule blocking exploitation vectors"],
"ticketRef": "JIRA-1234",
"vulnerabilityId": "CVE-2024-1234",
"purlPattern": "pkg:npm/lodash@*",
"artifactDigest": null,
"imagePattern": "docker.io/myorg/auth-service:*",
"environments": ["production"],
"requiredApproverIds": ["alice", "bob"],
"requestedTtlDays": 14,
"metadata": { "team": "auth" }
}
Server behaviour:
- Generates
requestId=EAR-{yyyyMMdd}-{8 hex}. - Resolves requirements (tenant rule if present and enabled, else gate default).
- Validates TTL ≤
MaxTtlDays, rationale length, evidence/compensating-control presence, justification length, and required-approver count. - Sets
requestExpiresAt = now + 7 days(approval window) andexceptionExpiresAt = now + requestedTtlDays. - Auto-approves when the gate requires 0 approvers (G0).
- Records audit entry
requested(sequence 1).
Response: 201 Created with the full ApprovalRequestDto (includes status, gateLevel, requestExpiresAt, exceptionExpiresAt, and any validation warnings, e.g. “No scope constraints specified.”).
2. Approver inbox
GET /api/v1/policy/exception/pending (scope exceptions:approve) returns requests where the caller is a required approver and has not yet approved.
3. Approve
POST /api/v1/policy/exception/{requestId}/approve (scope exceptions:approve).
ValidateApprovalActionAsync enforces:
- Request must be
PendingorPartial. - Caller must not have already approved.
- Self-approval rejected unless the gate allows it.
- If
requiredApproverIdsis non-empty, caller must be in it. - Required roles are tracked toward completion.
When MinApprovers is reached (and outstanding required roles cleared), the request transitions to Approved. Optional comment is captured.
4. Reject
POST /api/v1/policy/exception/{requestId}/reject (scope exceptions:approve). Requires a non-empty reason. Only Pending/Partial requests can be rejected; transitions to Rejected (terminal). The reason is surfaced to the requester.
5. Cancel
POST /api/v1/policy/exception/{requestId}/cancel (scope exceptions:request). Only the original requestor may cancel (returns 403 otherwise); returns 400 if already resolved; 204 No Content on success.
6. Audit & rules
GET /api/v1/policy/exception/{requestId}/audit— ordered audit trail (exceptions:read).GET /api/v1/policy/exception/rules— tenant approval rules (exceptions:read).
Step-by-Step (exception object lifecycle)
The /api/policy/exceptions surface manages the exception object directly.
| Step | Endpoint | Scope | Notes |
|---|---|---|---|
| Create | POST /api/policy/exceptions | policy:author | Status proposed. Validates expiry is in the future and ≤ 1 year. ID = EXC-{guid} (20 chars). |
| Update | PUT /api/policy/exceptions/{id} | policy:author | Mutates rationale/evidence/controls/ticket/metadata; bumps version. Cannot update expired/revoked. |
| Approve | POST /api/policy/exceptions/{id}/approve | policy:operate | Only proposed → approved. Self-approval rejected (approver ≠ requester). Appends approver. |
| Activate | POST /api/policy/exceptions/{id}/activate | policy:operate | Only approved → active. |
| Extend | POST /api/policy/exceptions/{id}/extend | policy:operate | Only active; new expiry must be later than current. |
| Revoke | DELETE /api/policy/exceptions/{id} | policy:operate | Any non-terminal → revoked. Optional reason. |
| List | GET /api/policy/exceptions | policy:read | Filters: status, type, vulnerability ID, PURL pattern, environment, owner; paginated (limit 1–100, default 50). |
| Counts | GET /api/policy/exceptions/counts | policy:read | Totals by status + expiring-soon. |
| Get | GET /api/policy/exceptions/{id} | policy:read | Full detail or 404. |
| History | GET /api/policy/exceptions/{id}/history | policy:read | Ordered ExceptionEvent audit trail. |
| Expiring | GET /api/policy/exceptions/expiring?days=7 | policy:read | Active exceptions expiring within the horizon (default 7 days). |
Example: create exception object
{
"type": "vulnerability",
"scope": {
"vulnerabilityId": "CVE-2024-1234",
"purlPattern": "pkg:npm/lodash@*",
"environments": ["production"]
},
"ownerId": "auth-team",
"expiresAt": "2025-01-28T14:30:00Z",
"reasonCode": "scheduled_fix",
"rationale": "Upgrade breaks the auth flow; WAF mitigation in place while we refactor.",
"evidenceRefs": ["sha256:…"],
"compensatingControls": ["WAF rule deployed"],
"ticketRef": "JIRA-1234",
"metadata": { "team": "auth" }
}
Exception Enforcement (policy evaluation)
IExceptionEvaluator.EvaluateAsync is invoked during policy evaluation with a FindingContext (artifactDigest, purl, vulnerabilityId, policyRuleId, environment, tenantId). It:
- Queries active exceptions for the scope (
GetActiveByScopeAsync). - Filters to those that truly match the context (exact match for digest / vuln ID / rule ID; wildcard PURL match; environment membership; tenant binding) and are
IsEffectiveAt(now)(statusActiveand not pastexpires_at). - Orders matches by a specificity score (artifact digest 100 > exact PURL 50 > vuln ID 40 > policy rule 30 > wildcard PURL 20 > environment 10). The most specific match is the primary.
- Returns
HasException, all matching exceptions, the primary reason/rationale, and the union of evidence references.
Effective verdict: a finding that would otherwise FAIL passes if (and only if) a matching, currently-effective exception applies.
Recheck Policy & Evidence Requirements (implemented, optional)
The StellaOps.Policy.Exceptions library defines additional governance primitives that an exception object may carry:
RecheckPolicy(RecheckConditionType:ReachGraphChange,EPSSAbove,CVSSAbove,UnknownsAbove, …) — conditions that trigger re-evaluation. When triggered, the recommendedRecheckActioncanBlockan exception (IsBlockedByRecheck) orRequireReapproval(RequiresReapproval).EvidenceHook(EvidenceType:FeatureFlagDisabled,BackportMerged,CompensatingControl, …) — mandatory/optional evidence requirements with freshness (MaxAge) and trust-score (MinTrustScore) constraints, validated byEvidenceRequirementValidator.
These are part of the model and library services; surfacing them through the public HTTP API is partial — treat fields like recheckPolicy / evidenceRequirements as advanced/optional rather than required inputs.
Expiration & Lifecycle
- Exceptions are time-bounded by
expires_at(object) /exceptionExpiresAt(approval request). Object expiry is capped at 1 year at creation. - Expiry is enforced lazily at evaluation time via
IsEffectiveAt(now)(a finding is only suppressed if the exception isActiveand not pastexpires_at). The repository exposesGetExpiredActiveAsync()(active rows past expiry) andGetExpiringAsync(horizon)/ the/expiringendpoint (proactive surfacing). - The audit model includes a system-actor
Expiredevent (ExceptionEvent.ForExpired, actor"system").
Background workers (verified)
Expiry is not only lazy: two in-process hosted BackgroundService workers run the status sweep and warnings. Both are registered with AddHostedService<…>():
| Worker | Service / registration | Cadence | What it does |
|---|---|---|---|
ExceptionExpiryWorker | Policy Gateway (Program.cs) and Policy Engine (Program.cs) | hourly (Interval, default 1h; InitialDelay 30s) | Loads GetExpiredActiveAsync() and flips active → expired (system actor system:expiry-worker, with ConcurrencyException retry-next-cycle), then loads GetExpiringAsync(WarningHorizon) (default 7 days) and calls IExceptionNotificationService.NotifyExceptionExpiringSoonAsync at the 7-, 3-, and 1-day thresholds (ShouldSendWarning). Gated by ExceptionExpiryOptions.Enabled (default true; section Policy:Exceptions:Expiry). |
ExceptionLifecycleWorker (+ ExceptionLifecycleService) | Policy Engine (Program.cs) | polling (PollIntervalSeconds, default 60s) | Per-configured-tenant activation/expiry sweep; calls IExceptionRepository.ExpireAsync(tenant) for each tenant in ResourceServer.RequiredTenants. Options: PolicyEngineExceptionLifecycleOptions. |
Earlier drafts claimed “no Scheduler job is wired” and that the “7-day / 1-day reminders” were not implemented. Both claims are incorrect against current source — see the worker table above. What does not exist is an external JobEngine/Scheduler job: this work runs as in-process hosted services in the Policy Gateway and Policy Engine.
Lifecycle notifications (verified)
IExceptionNotificationService (interface in StellaOps.Policy.Gateway/Services/ExceptionService.cs) is invoked on exception-object lifecycle transitions and by the expiry worker:
| Hook | Fired from | Emitted action |
|---|---|---|
NotifyExceptionCreatedAsync | ExceptionService.CreateAsync | exception.created |
NotifyExceptionApprovedAsync | ExceptionService.ApproveAsync | exception.approved (incl. approverId) |
NotifyExceptionActivatedAsync | ExceptionService.ActivateAsync | exception.activated |
NotifyExceptionExtendedAsync | ExceptionService.ExtendAsync | exception.extended (incl. newExpiry) |
NotifyExceptionRevokedAsync | ExceptionService.RevokeAsync | exception.revoked (incl. reason; severity warning) |
NotifyExceptionExpiringSoonAsync | ExceptionExpiryWorker | exception.expiring_soon (incl. timeUntilExpirySeconds) |
The registered implementation is AuditExceptionNotificationService, which emits an AuditEventPayload (module policy, resource type exception) to the unified audit/Timeline emitter (IAuditEventEmitter). It is disabled by default (ExceptionNotificationOptions.Enabled = false; section Policy:Exceptions:Notifications) — when disabled it logs and skips emission. There is no email/Slack/channel dispatch through the standalone src/Notify module; these notifications are audit/Timeline events only.
Note: the
ExceptionServicelifecycle methods above are wrapped by the service layer. The minimal-API handlers documented under exception object lifecycle operate directly onIExceptionRepositoryand therefore do not themselves emit these notifications; the audit history (/history) is still recorded via the repository in both paths.
Data Contracts
Exception object (response — ExceptionResponse)
interface ExceptionResponse {
exceptionId: string; // EXC-…
version: number;
status: 'proposed' | 'approved' | 'active' | 'expired' | 'revoked';
type: 'vulnerability' | 'policy' | 'unknown' | 'component';
scope: {
artifactDigest?: string;
purlPattern?: string;
vulnerabilityId?: string;
policyRuleId?: string;
environments: string[];
};
ownerId: string;
requesterId: string;
approverIds: string[];
createdAt: string;
updatedAt: string;
approvedAt?: string;
expiresAt: string;
reasonCode: 'false_positive' | 'accepted_risk' | 'compensating_control'
| 'test_only' | 'vendor_not_affected' | 'scheduled_fix'
| 'deprecation_in_progress' | 'runtime_mitigation'
| 'network_isolation' | 'other';
rationale: string;
evidenceRefs: string[];
compensatingControls: string[];
ticketRef?: string;
metadata: Record<string, string>;
}
Approval request (response — ApprovalRequestDto)
interface ApprovalRequestDto {
requestId: string; // EAR-yyyyMMdd-XXXXXXXX
id: string; // GUID
tenantId: string;
exceptionId?: string;
requestorId: string;
requiredApproverIds: string[];
approvedByIds: string[];
rejectedById?: string;
status: 'Pending' | 'Partial' | 'Approved' | 'Rejected' | 'Expired' | 'Cancelled';
gateLevel: 'G0' | 'G1' | 'G2' | 'G3' | 'G4';
justification: string;
rationale?: string;
reasonCode: string; // ExceptionReasonCode enum name
evidenceRefs: string[];
compensatingControls: string[];
ticketRef?: string;
vulnerabilityId?: string;
purlPattern?: string;
artifactDigest?: string;
imagePattern?: string;
environments: string[];
requestedTtlDays: number;
createdAt: string;
requestExpiresAt: string;
exceptionExpiresAt?: string;
resolvedAt?: string;
rejectionReason?: string;
version: number;
updatedAt: string;
warnings: string[];
}
Approval rule (response — ApprovalRuleDto)
interface ApprovalRuleDto {
id: string;
name: string;
description?: string;
gateLevel: 'G0' | 'G1' | 'G2' | 'G3' | 'G4';
minApprovers: number;
requiredRoles: string[];
maxTtlDays: number;
allowSelfApproval: boolean;
requireEvidence: boolean;
requireCompensatingControls: boolean;
minRationaleLength: number;
enabled: boolean;
}
Error Handling
| Condition | HTTP | Source |
|---|---|---|
| Missing request body | 400 | both surfaces |
| Object expiry in the past or > 1 year | 400 | ExceptionEndpoints create |
| Update/revoke an already expired/revoked object | 400 | ExceptionEndpoints |
Approve a non-proposed object | 400 | ExceptionEndpoints approve |
| Self-approval of object (approver == requester) | 400 | ExceptionEndpoints approve |
Activate a non-approved object | 400 | ExceptionEndpoints activate |
| Extend with new expiry ≤ current | 400 | ExceptionEndpoints extend |
Request TTL > gate MaxTtlDays | 400 | CreateApprovalRequestAsync / rules service |
| Validation failure (rationale/evidence/controls/justification) | 400 | ValidateRequestAsync |
| Approval not allowed (state, dup, self, not-in-list, roles) | 400 | ValidateApprovalActionAsync |
| Reject without reason | 400 | RejectRequestAsync |
| Cancel by non-owner | 403 | CancelRequestAsync |
| Request/exception not found | 404 | both surfaces |
| Missing tenant / actor | 401 | approval endpoints |
Observability
The specific metric/log-event names below are illustrative and not verified against an emitted-telemetry catalog. The approval-request endpoints are decorated with
.Audited("policy", "<action>", resourceType: "exception_approval_request")(create / approve / reject / cancel), so those actions emit standard audit events; structured information logs (e.g. “Exception request {RequestId} approved by {ApproverId}”) are emitted by the handlers.
Audit actions (verified)
| Endpoint | Audit action | Resource type |
|---|---|---|
POST /request | create | exception_approval_request |
POST /{id}/approve | approve | exception_approval_request |
POST /{id}/reject | reject | exception_approval_request |
POST /{id}/cancel | cancel | exception_approval_request |
Exception-object events (Created, Updated, Approved, Activated, Extended, Revoked, Expired, EvidenceAttached, CompensatingControlAdded, Rejected) are persisted as ExceptionEvent audit entries and exposed via the /history endpoint.
Not Implemented / Roadmap
The following items appeared in earlier drafts of this flow but are not present in src/and should be treated as roadmap, not current behavior:
- Exception types
temporary/extended/permanent/emergencywith fixed max durations (30/90/indefinite/7 days). The implemented object types arevulnerability/policy/unknown/component; duration limits come from gate-levelMaxTtlDays, not a type/duration table. - Severity-driven approval matrix routing to Team Lead → Security Team → CISO. The implemented routing is by gate level (G0–G4) with configurable role strings (
code-owner,delivery-manager,product-manager,ciso). - External Scheduler/JobEngine job for expiry. Implemented as in-process hosted
BackgroundServiceworkers instead (ExceptionExpiryWorker,ExceptionLifecycleWorker) — see Background workers. Automatic deactivation (active → expired) and 7-/3-/1-day expiry warnings are implemented; a distinct “mid-point review” reminder is not. - Notify-module dispatch for approval/expiry emails or Slack (
approval_url,#security-approvals). Exception lifecycle notifications are emitted, but to the unified audit/Timeline emitter (IExceptionNotificationService→AuditExceptionNotificationService, disabled by default) — see Lifecycle notifications. There is no email/Slack channel dispatch through the standalonesrc/Notifymodule. scope.images/scope.tenantsarrays on the exception object. The object scope isartifactDigest/purlPattern/vulnerabilityId/policyRuleId/environments(+tenantId). AnimagePatternfield exists only on the approval-request entity, not on the exception object scope.- Object statuses
pending/rejected. The object state machine usesproposed/approved/active/expired/revoked;pending/rejectedbelong to the separate approval-request status set. - Renewal request as a distinct API. The implemented mechanism is
POST /api/policy/exceptions/{id}/extendon an active object.
Related Flows
- Policy Evaluation Flow — exception enforcement during evaluation
- Multi-Tenant Policy Rollout Flow — policy/tenant context
