Scanner CAS capacity guard procedure
This procedure is the operator and developer response for Scanner’s owned object-store state bucket:scanner-artifacts. It covers the doctor measurement and the P7 pre-write pause introduced by SPRINT_20260722_017 SCN-CAS.
Choose the value using the storage budget sizing guide.
Contract
- Canonical bucket:
scanner-artifacts - Owned-state id:
bucket:scanner-artifacts - Logical budget key:
Doctor:State:bucket:scanner-artifacts:BudgetBytes - Shell-safe deployment variable:
SCANNER_CAS_BUDGET_BYTES - Doctor check id:
doctor.standard.state.size-budget.bucket-scanner-artifacts - Stable write failure:
scanner-cas-write-paused - HTTP write refusal:
507 Insufficient Storage, withreason_code: CAS_WRITE_PAUSED - Stable measurement failure:
scanner-cas-measurement-unavailable
The budget is a positive byte count. An absent budget leaves the existing write path enabled and makes the doctor check report the measured size as unbudgeted. An invalid declared budget, an unmeasurable current size, an unmeasurable payload length, arithmetic overflow, or a projected size above the budget pauses the primary-bucket write before the backing driver’s PutAsync is called.
Scanner-web returns this refusal as application/problem+json, with a trace id and the capacity-pause reason. A Scanner endpoint filter carries the same response through Router/Valkey dispatch, which invokes endpoints without exception middleware. The direct HTTP fallback uses the same response factory. It is an operator-action condition. The CLI’s scan-result uploader preserves the reason and stops after the first 507 response; it does not retry the same write until the store or budget is repaired. Other unexpected failures retain the server-error response.
Scanner-web registers the owned-state doctor check. Both scanner-web and scanner-worker receive the P7 decorator through AddScannerStorage, and both hosts map SCANNER_CAS_BUDGET_BYTES to the canonical logical key before storage is registered.
Do not spell the logical key directly as Doctor__State__bucket__scanner-artifacts__BudgetBytes in Compose. Although that is the usual .NET environment-key transformation, the embedded hyphen makes it an invalid POSIX shell identifier; Scanner’s sh -c image entrypoint drops it before dotnet starts. The shell-safe alias above is the deployment contract.
Source verified against 213da18d06. The initial HTTP/client receipt records the source controls. The live routed verification proves 507/CAS_WRITE_PAUSED through the gateway, unchanged bucket bytes and absent probe artifact, followed by exact budget restoration. The signed Router-frame regression fails with 400 when the endpoint filter is removed. Re-verify with tools/scripts/test-targeted-xunit.ps1 -Project src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj -Class *ScannerCapacityPauseResponseTests* -BuildProjectReferences and the approved live procedure below.
Driver capability
The shipped object store is SeaweedFS. Scanner connects to SeaweedFS’s S3-compatible endpoint through the S3 driver and measures the whole bucket or a prefix with paginated ListObjectsV2. Here, S3 names the protocol/client used to enumerate object sizes; it is not a second product store. Authentication uses the existing SigV4 access-key/secret-key contract (STELLAOPS_S3_ACCESS_KEY_ID and STELLAOPS_S3_SECRET_ACCESS_KEY), not a bearer token. Missing object sizes, a truncated response without a token, token repetition, and byte-count overflow fail measurement; none becomes zero.
The legacy compatibility-only native RustFS client has an evidenced contract only for PUT|GET|DELETE /api/v1/buckets/{bucket}/objects/{key}. It has no evidenced list/size route. Its measurement therefore fails immediately with scanner-cas-measurement-unavailable and performs no speculative HTTP request. Do not add a guessed listing route and do not replace the failure with 0.
For a legacy RustFS-native estate, choose one of these reviewed remediations before declaring the capacity budget:
- move Scanner to the supported S3-compatible driver and grant it bucket-list permission as well as its existing object permissions; or
- add an owner-approved, authenticated server-side list/size contract to RustFS, then implement and test that exact contract in the Scanner driver.
Until one is complete, setting a budget on the native RustFS path deliberately pauses primary CAS writes because the precondition cannot be proven.
Triage a failure
- Capture the stable failure code, service role, configured object-store driver, configured bucket, budget value, and correlation/scan id. Do not remove the guard merely to make a scan proceed.
- Confirm the configured bucket is exactly
scanner-artifacts. A different primary bucket is a different owned state and is rejected by this guard. - For SeaweedFS over S3, verify the Scanner SigV4 credential can list the canonical bucket and that every truncated page returns a new continuation token. A missing or repeated token is a backing endpoint failure, not an empty bucket.
- Compare the doctor evidence fields
state.size.bytesandstate.size.budget-bytes. If the state is legitimately full, apply the approved retention/capacity action; do not delete immutable CAS evidence outside its retention authority. - Restore a positive, reviewed budget and rerun the check. Preserve the before and after doctor results plus the write-pause log in the window evidence.
Red/green acceptance in an approved window
For the OSM-5 HTTP check, devops/compose/docker-compose.local-scanner-capacity-probe.yml is an opt-in local profile setting the web role’s budget to one byte. Append it only for the bounded refusal check after preserving the current chain, image, mounts and budget. Submit one valid CycloneDX 1.6 BYOS document to /api/v1/byos-sboms; require HTTP 507 and reason_code: CAS_WRITE_PAUSED, then restore the exact prior chain even if the request check fails. The ingestion path attempts the guarded object write before the artifact-catalog upsert or scan submission. Compare the bucket measurement and the probe digest’s catalog presence before/after. Do not repeat the request with the normal budget, because that would create durable state.
- Record the current driver, bucket inventory/size, budget, and service image digest.
- On a SeaweedFS-over-S3 non-production or owner-approved disposable path, set
SCANNER_CAS_BUDGET_BYTESbelow the measured current size on both scanner-web and scanner-worker, then restart/reload both roles through the normal configuration procedure. - Verify the doctor check is Critical/unhealthy and reports the real non-zero size.
- Attempt one owner-approved disposable Scanner CAS write. Verify
scanner-cas-write-pausedand prove by before/after object inventory that no object was written. - Restore the exact prior budget, verify the doctor result returns to its expected state, and repeat one authorized write-path smoke test.
The scanner-web path completed this proof in the approved 2026-08-23 window. Image sha256:00a9a0f97b7102e920f429470935786cf4843038de18da70babce04acc03e6a4 measured 142,432,843 bytes with no budget, reported Critical/unhealthy with SCANNER_CAS_BUDGET_BYTES=1, and refused a tenant-valid SBOM upload with scanner-cas-write-paused; the before/after measurement remained exactly 142,432,843 bytes. Removing the override restored Info/healthy with the alias absent. Scanner-worker was not restarted in that window, and the post-restore green write in step 5 was omitted because it would create durable state; shared composition and focused tests cover both roles and the normal write path.
Important limit: this is not a hard global quota
The guard performs measure -> compare -> write. Concurrent scanner-web or scanner-worker replicas can measure the same starting size and both proceed. The check is therefore a fail-closed pre-write safety gate and an observable P7 forcing function, not an atomic cross-replica quota reservation. It can also conservatively count an overwrite as additional bytes.
A hard global invariant requires an owner-approved atomic reservation/commit protocol or an enforceable backend quota with equivalent failure semantics. Until that exists and is tested under concurrent writers, no sprint or runbook may claim hard estate-wide quota enforcement.
