# BG-001 tenant-safe worker foundation verification

Updated: 2026-08-15

Status: `IMPLEMENTED — NOT VERIFIED`. The owner-scoping foundation and default-off activation contract pass local tests. Browser, production canary, durable notification/idempotency and multi-replica gates remain.

## Revalidated root cause and scope

Rules, follow-up reminders, daily export and job enrichment created request-scoped `JobTrackerContext` instances without an HTTP user. Deny-on-null global filters therefore returned no owned rows. Rules swallowed every exception; the other loops appeared healthy while doing empty work. CV processing already uses explicit owner predicates on each unfiltered query and was not changed; backup and AI health probe are tenant-neutral.

`BackgroundTenantRunner` now performs the sole worker bypass: it enumerates distinct non-empty job owners with `IgnoreQueryFilters`, opens a new dependency-injection scope per owner, sets `CurrentUserService`, and then executes all work through the normal tenant filters. It processes owners sequentially, isolates failures, and logs only worker/failure categories and aggregate counts. It refuses to override any HTTP context.

The four repaired workers are deny-by-default through new switches. Old email/export settings alone cannot activate them. Real email, external AI and production services were not called.

## Automated evidence

| Check | Result |
|---|---|
| `dotnet test ... --filter "FullyQualifiedName~BackgroundWorkerTenantTests"` | PASS — 9/9, including fixed-clock boundary and restart cases |
| `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore` | PASS — 532/532 after the final trust-boundary test |
| `docker compose config --quiet` | PASS; only unset optional/local environment warnings |
| `git diff --check` | PASS — no whitespace errors; repository line-ending notices only |

Real SQLite tests prove two-owner/no-HTTP filtering, owner-failure isolation, per-owner rules at an exact fixed-clock threshold with an idempotent post-restart pass, atomic per-owner daily exports that overwrite the same date file after restart, fake-AI enrichment for both owners, fake-email reminders that do not resend after restart, default-off behavior for all four workers and HTTP-context override refusal. All four worker loops and business-date decisions now use the injected `TimeProvider` rather than hidden system clocks.

## Runtime evidence

An isolated app using disposable data under `docs/audits/evidence/bg-001-runtime` listened on `127.0.0.1:5306`. Health returned 200, no daily-export directory was created with all four default-off switches, and the exact `JobTrackerApi` PID 44980 was stopped; the port and process were then confirmed closed.

## Remaining gates and rollback

- Reminder delivery is not exactly-once across an email-success/database-failure boundary. Keep it off until OPS-001 supplies a persistent notification/outbox operation.
- AI enrichment must remain off until POL-001/POL-002 and durable AI operations are enforced server-side.
- Rules and export remain off pending notification/audit and retention/operator rollout respectively.
- No lease, heartbeat, distributed scheduler, browser surface or production canary was added here. Local restart and clock-boundary coverage is complete; multi-replica coordination remains an activation concern rather than default-off foundation work.
- Rollback is setting all four worker switches false, then reverting the runner/service changes. Do not delete export files or undo user-visible mutations without a separate reviewed procedure. No schema migration was introduced.
