Exception reports

Source verification: commit 6285fb24cd (2026-10-20), including dedicated reader credentials. Local estate JSON-report acceptance passed with two tenants on 2026-10-20; see the production receipt. Re-verify with pwsh tools/scripts/test-targeted-xunit.ps1 -Project src/OfflineKit/__Tests/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj -Class "*PolicyExceptionReportSourceTests*" -BuildProjectReferences; the explicit simulation and deployment proof are separate.

OfflineKit owns report jobs and rendered contents. Policy owns the exceptions and their history. OfflineKit reads those records through Policy’s authenticated API; it does not open the Policy database or register Policy persistence repositories.

POST /api/offlinekit/v1/exports/exceptions requires export.operator. List, status and download require export.viewer. The server resolves ownership from the canonical stellaops:tenant claim. Request bodies, query parameters and tenant headers cannot select another owner. Another tenant’s job returns the same 404 as an unknown job ID.

The job retains both its local tenant UUID and the authenticated Policy tenant reference. It persists no bearer token. The Policy reader obtains policy:read client credentials for that exact reference from Authority. Its cache key includes the reader client, tenant and scope, and it checks returned and cached token claims before each use. Policy verifies the token’s signature, audience and tenant on every request. A dedicated reader identity has only policy:read, an explicit tenant allowlist and no singular tenant binding. Doctor/catalog credentials and grants remain separate. An identity pinned to one tenant cannot serve a multi-tenant reader even if its metadata lists several allowed tenants.

Enable the reader with these settings:

SettingDefaultMeaning
Export:ExceptionReports:Policy:EnabledfalseEnables Policy reads and requires OfflineKit:ServiceIdentity.
Export:ExceptionReports:Policy:BaseUrlunsetReviewed Policy service or gateway base URL.
Export:ExceptionReports:Policy:ClientIdunsetDedicated confidential reader client.
Export:ExceptionReports:Policy:ClientSecretunsetRuntime secret for that client; required when enabled.
Export:ExceptionReports:Policy:TimeoutSeconds20Deadline for each complete response, including its body.
Export:ExceptionReports:Policy:MaxRecords10000Whole-report exception/history safety limit; exceeding it fails the job.
Export:ExceptionReports:Policy:MaxResponseBytes8388608Maximum bytes in one Policy response.

The existing OfflineKit:ServiceIdentity Authority/token-endpoint settings provide the Auth.Client transport. The read request supplies the dedicated credentials above through that library’s explicit request parameters. Provision secrets through the runtime secret mechanism; preserve the Doctor/catalog client and its settings.

The reader pages ordinary lifecycle exceptions in batches of 100. Supported filters are status, type, vulnerability ID, PURL pattern, environment and owner. It uses page contents to detect completion: Policy’s totalCount counts all tenant exceptions even when a filter is present. includeHistory reads each included exception’s subject-bound history. The withdrawn includeApplications option is absent.

A disabled source, token refusal or binding mismatch, upstream refusal, timeout, malformed response or exceeded limit fails the report with a bounded error code. It never produces a successful empty or partial report for an unreadable source. Existing status and download reads use OfflineKit’s own store and need no Policy request.

The committed local simulation is under src/OfflineKit/__Tests/StellaOps.ExportCenter.Tests/Fixtures/PolicyExceptionReports/. It runs the real Authority token client and Policy reader over HTTP against contract stand-ins. It establishes client behavior, not production authentication or live acceptance. The owning PCD-871 receipt records native controls and the deployed two-tenant journey separately.