JOB-9 window — JobEngine consolidated route swap
Current routing boundary (2026-08-31): this is the future consolidated-owner proposal, not the RAR-6/7 native-consumer procedure. Console32907 is deployed and its native
/api/v1/packsand/api/v1/schedulerbrowser reads pass. RAR-7 owns retirement of their old Pack Registry and/scheduleraliases, with separate conformance and live POST gates. Scheduler/PacksRegistry hosts remain the current owners; no data or host cutover is implied. Readrar6NativeConsumerRepointin the manifest before applying any historical replaced set.Update 2026-09-04 — the historical swap is RETIRED, not merely deferred. RAR-5/RAR-7 (
81e276c2a3,83bfc9359f,f4b1774cf6) executed the 2026-08-28 owner ruling (“all microservices needs to autopublish no router configuration for microservices endpoints”). All ten rows injob5-jobengine-route-swap.jsonare gone from both route tables, which now hold 22 routes total. Sinceced8a2d286pwsh tools/scripts/job5-swap-jobengine-routes.ps1refuses on both invocations —-Checkand a bare run each report the swap as RETIRED and exit 2, measured 2026-09-04 at HEAD. Step 2’s10 of 10expectation is unsatisfiable and step 3 no longer runs. A static entry out-ranks publication, so reinstating those ten rows — by applying the swap, or by “reconciling” the manifest until-Checkpasses and then applying — would claw the JobEngine plane back off the auto-published layer. Untilced8a2d286a bare run wrote nothing but still reportedSwap applied. Routes replaced: 0and exited 0; that false success signal is fixed underSPRINT_20260904_001RRS-1. Do instead: confirm the auto-published layer serves the native JobEngine/Scheduler/Packs surfaces through the gateway (authorized 2xx, unauthenticated 401/403). Model for this shape:offlinekit-cutover-ok6-route-swap.md.
Sprint: SPRINT_20260722_012 (JOB-5 prepares, JOB-9 applies). Manifest: tools/scripts/job5-jobengine-route-swap.json — the single source for the route set, read by both the swap script and the guard test. Script: tools/scripts/job5-swap-jobengine-routes.ps1. Guard: src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/JobEngineConsolidatedRouteConfigTests.cs.
Nothing in JOB-5 is installed live. scheduler-web and packsregistry-web keep serving until this window opens; the deliverable is the manifest, the script, the guard and this page.
Do not open this window until the consolidated host actually serves the consolidated paths. See “The 404 farm” below — this is the one prerequisite that is not a route-table fact and the one most likely to be assumed.
What changes
Ten routes across two host names collapse into one group:
^/api/jobengine/v1(.*) -> http://jobengine.stella-ops.local/api/jobengine/v1$1
| Replaced route | Current host | New external prefix |
|---|---|---|
^/api/v1/scheduler/doctor(.*) | scheduler | /api/jobengine/v1/scheduler/doctor |
^/api/v1/jobengine/registry/packs(.*) | packsregistry | /api/jobengine/v1/registry/packs |
^/api/v1/jobengine/jobs(.*) | scheduler | /api/jobengine/v1/jobs |
^/api/v1/jobengine/dag(.*) | scheduler | /api/jobengine/v1/dag |
^/api/v1/mirrors(.*) | packsregistry | /api/jobengine/v1/mirrors |
^/api/v1/compliance(.*) | packsregistry | /api/jobengine/v1/compliance |
^/api/v1/jobs(.*) | scheduler | /api/jobengine/v1/jobs |
^/api/v1/scheduler/jobs(.*) | scheduler | /api/jobengine/v1/jobs |
^/scheduler(?=/|$)(.*) | scheduler | (none — removed) |
^/api/scheduler(.*) | scheduler | (none — dead route, removed) |
Route count per config: 203 → 194 (ten removed, one added).
Three facts that are not obvious from the config
1. Ordering is load-bearing — the target namespace is already occupied
^/api/jobengine(.*) (index 175 in both configs) belongs to ReleaseOrchestrator, and it captures /api/jobengine/v1/*. Simulated against the live table, /api/jobengine/v1/schedules resolves today to release-orchestrator. StellaOpsRouteResolver.Resolve is a strict first-match over declaration order, so appending the consolidated group hands this whole plane to the wrong service — and release-orchestrator answers 404/500 rather than failing visibly.
The script inserts the group at the position of the first route it replaces (index 85) and re-reads its own output to prove the group outranks index 175 before writing. It throws rather than write a table where it does not.
2. Most of the family’s routing is IMPLICIT, and no route list shows it
Eight of scheduler-web’s ten endpoint groups have no gateway route at all. /api/v1/scheduler/{schedules,runs,workers,failure-signatures,policy/runs, policies/simulations,vuln/resolver,audit} reach the service only through
^/api/v1/([^/]+)(.*) -> http://$1.stella-ops.local/api/v1/$1$2
which derives the microservice hostname from the first path segment. It works purely because the deployable registers as scheduler (StellaOps.Scheduler.WebService/Program.cs:391). Rename it to jobengine and every one of those breaks, with nothing in the route table to have warned you.
This is why the removal set in the manifest is ten routes plus an impliedRoutes block, and why ImpliedRoutes_AreServedOnlyByTheWildcard_AndTheGroupMustCoverThem asserts both halves. Schedule CRUD and run management — the family’s largest Console surface — are in this set.
3. PreserveAuthHeaders is inert on these routes
Program.cs:368-370 builds JwtPassthroughPrefixes from Routes.Where(r => r.PreserveAuthHeaders && !r.IsRegex). All ten replaced routes are regex, so none can enter the passthrough list and Authorization is stripped on every one of them. Four of the ten set the flag anyway and it changes nothing. Identity reaches the consolidated host through the signed identity envelope, exactly as it reaches scheduler-web and packsregistry-web today. The flag is carried on the group only so a future non-regex split inherits the intent.
The 404 farm — the prerequisite this window must not assume
The group forwards the tail verbatim, so the consolidated host must serve /api/jobengine/v1/** natively. It does not yet. The S4 move was byte-identical, so every carried MapGroup still hardcodes its old prefix — e.g. ResolverJobEndpointExtensions.cs:17 maps /api/v1/scheduler/vuln/resolver.
Re-mapping those groups is JOB-7/JOB-9 host-wiring work, not JOB-5’s. Applying the swap before it lands produces a uniform 404 farm, not a routing error, and the route tests will still be green — they assert routing, not that anyone answers.
Also owed at wiring time: ResolverJobEndpointExtensions.CreateJobAsync returns Results.Created("/api/v1/scheduler/vuln/resolver/jobs/{id}") — an absolute internal path in the Location header. The gateway rewrites request paths, not response headers, so post-swap that Location points at a path the gateway no longer routes.
Window steps
- Confirm the prerequisite. The consolidated host serves
/api/jobengine/v1/**. Not “the image builds” — a real request answered. Dry run, from a clean tree:Retired — the script now refuses and exits 2. Do not reconcile the manifest to make it pass.pwsh tools/scripts/job5-swap-jobengine-routes.ps1 -CheckExpect10 of 10per config andRESULT: manifest and live route configs agree.Apply:A bare run also refuses and exits 2, writing nothing. Untilpwsh tools/scripts/job5-swap-jobengine-routes.ps1ced8a2d286it wrote nothing but still printedSwap applied. Routes replaced: 0. Recreate the gateway so the deployed config reloads.and exited 0 — a false success signal, fixed underSPRINT_20260904_001RRS-1. The remaining regression risk is reinstating the ten manifest rows by some other route — a static entry out-ranks publication.- Update the four route tests that go red (measured, see below).
- Flip the Console registry. Six values in
src/Web/StellaOps.Web/src/app/core/api/api-path-prefixes.ts—jobengineJobs,jobengineDag,jobengineScheduler,jobengineSchedulerDoctor,jobengineSchedulerAudit,jobenginePackRegistry— to the values named in that file’s header. All six together: the gateway swap is atomic. Thenbash tools/scripts/validate/check-console-jobengine-prefix-literals.shmust stay CLEAN, andnpx ng buildmust stay green. - Recreate the gateway so the deployed config reloads (
--force-recreate; a same-tag image will not swap without it). - Forcing function, not “the container is up”: an authorized call to a consolidated path returns the surface’s own payload, and an unauthorized one is rejected by the host reading the identity envelope.
Consumers NOT covered by this swap
- The CLI’s Task Pack commands.
src/Cli/StellaOps.Cli/Services/PackClient.cs(16 call sites) andPackReEnvelopeClientcall/api/v1/packs/*. That prefix has no gateway route and never had: the wildcard resolves it topacks.stella-ops.local, and no service registers aspacks(the name ispacksregistry). The same is true of/api/v1/export/offline-seed. These are pre-existing defects, not swap damage — the consolidated group would give both a real route for the first time. The CLI’s own paths were deliberately not edited at JOB-5: repointing them onto a host that has no compose key until JOB-8 trades one broken state for another. JOB-10 + the CLI owner. - ReleaseOrchestrator’s
/api/v1/jobengine/*surface — quotas, deadletter, runs, pack-runs, stream, audit, sources, slos, and the bare^/api/jobengine(.*). Not this family’s; must survive the window untouched.ConsolidatedGroup_DoesNotCaptureNeighbouringPrefixespins that. JobEngineControlHttpClient.getJobSummary()— reaches scheduler (because/api/v1/jobengine/jobs*is scheduler’s) but no backend serves/jobengine/jobs/summaryand no component calls it. Dead both ends; deletion is JOB-10’s.
Rollback
git checkout -- src/Router/StellaOps.Gateway.WebService/appsettings.json devops/compose/router-gateway-local.json, recreate the gateway, revert the six Console values. The swap is a pure config edit; nothing persists.
Rehearsal record (2026-08-05)
Applied to the working tree and restored. Measured, not predicted:
-Check: 10 of 10 routes matched in both configs; ordering guard PASS.Applied: 203 → 194 routes per config; group at index 85, ReleaseOrchestrator’s
^/api/jobengine(.*)at 167; both files still parse.JobEngineConsolidatedRouteConfigTests: 16/16 green pre-swap AND post-swap.Gateway
Configurationnamespace post-swap: 187 cases, 8 red, all inGatewayRouteSearchMappingsTests— 4 tests × 2 configs:RouteTable_ContainsUnifiedSearchMappingsAndKeepsThemAheadOfApiCatchAll(itsRequiredMappingstable carries four family rows, lines 32/33/34/36)RouteTable_UsesSegmentBoundariesForFrontdoorRootPrefixes(asserts^/scheduler(?=/|$)(.*), line 366)RouteTable_MapsJobEngineJobsConsolePathToScheduler_NotReleaseOrchestratorPlaceholder(line 403)RouteTable_MapsJobEngineDagConsolePathToScheduler_NotReleaseOrchestratorPlaceholder(line 436)
All in ONE file — do not go hunting for more. The last two are worth reading before you edit them: they already exist to stop release-orchestrator answering for
/api/v1/jobengine/jobs, which is independent corroboration that the two “jobengine” planes collide.VulnerabilitiesHubRouteConfigTests,FindingsConsolidatedRouteConfigTests,OperationalApiOwnershipRouteConfigTestsandTopologyOwnershipRouteConfigTestsstay green.
