The internal machine routes (`quality/bridge-run`, `canonical/facts`,
`internal/evidence-outbox/dispatch`, `internal/er/candidates`,
`search/definitions`) trusted a shared `X-Internal-Token` with NO tenant
authority: middleware let it through without setting any verified tenant, so
`assertTenantMatchesVerified` was a no-op and a caller-chosen `tenant_id` in
the body was authoritative.
This closes that gap while keeping the engine working during the transition
to CL0UD workload tokens:
- `src/auth/internal-service-tenant.ts` — the server-configured
`INTERNAL_SERVICE_TENANTS` scope, a scope header that only middleware may
set, and a parser that strips/ignores client values.
- `src/auth/cl0ud-workload.ts` — a real `jose`/JWKS verifier for CL0UD
workload JWTs (`iss`/`aud`/`exp`), deriving `tenant_id` and `capabilities`
from verified claims only. Fail-closed when a token is presented but the
issuer/JWKS/audience/tenant claim does not verify.
- `middleware.ts` — strips every `x-0pt-verified-*` header on all forwarded
paths. Legacy internal tokens get the single configured tenant as scope
(an ambiguous multi-tenant list is refused with 401 instead of widened);
a Bearer CL0UD workload token is verified and its claims become the
verified tenant/subject/capabilities.
- `src/http/tenant-guard.ts` — enforces the service scope on tenant-scoped
writes (deny when the tenant is outside the scope) and adds
`assertWorkloadCapabilities` for capability-scoped principals.
- `quality/bridge-run` now calls `assertTenantMatchesVerified` like
`canonical/facts` already did.
Tests: `tests/auth/workload-identity-dual-accept.test.ts` (10 cases) covers
forged-header stripping, ambiguous-scope refusal, wrong audience, missing
tenant claim, cross-tenant denial and missing-capability denial.
Real CL0UD provisioning (service accounts + published JWKS) is still
required before a workload token exists in production; until then the
legacy token path is the only one that authenticates, now tenant-scoped.