From 4223d3c3200c920dc80e4cfa765fd970c73cd975 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Tue, 23 Jun 2026 19:42:45 +0200 Subject: [PATCH] feat(enabled): runtime honoring for disabled scripts and routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the `enabled` flag bite at the data plane (§4.3). - Routes: `compile_routes` drops disabled rows from the match table, so a request to a disabled route 404s indistinguishably from an absent one (no info leak). Rebuilt on every route write + at startup, as today. - Scripts: the orchestrator's two execute paths — `/execute/{id}` bypass and the user-route handler — 404 when the resolved script is disabled. The route-handler check also covers an enabled-route-bound-to-disabled- script (§4.7): the binding is unreachable rather than 500/executing. Still pending: the dispatcher fire-time re-check for already-enqueued outbox rows (next commit). Tested: route_admin disabled-route-dropped unit test + orchestrator suite (75) green; clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/manager-core/src/route_admin.rs | 15 +++++++++++++++ crates/orchestrator-core/src/api.rs | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/crates/manager-core/src/route_admin.rs b/crates/manager-core/src/route_admin.rs index 6edd32b..f34cc93 100644 --- a/crates/manager-core/src/route_admin.rs +++ b/crates/manager-core/src/route_admin.rs @@ -396,6 +396,9 @@ async fn refresh_table( #[must_use] pub fn compile_routes(rows: &[Route]) -> Vec { rows.iter() + // A disabled route (§4.3) is dropped from the match table entirely, so + // a request to it 404s indistinguishably from an absent route. + .filter(|r| r.enabled) .filter_map(|r| match compile_route(r) { Ok(compiled) => Some(compiled), Err(e) => { @@ -654,4 +657,16 @@ mod tests { "a reserved-path route must be skipped, never abort the compile" ); } + + #[test] + fn disabled_route_is_dropped_from_compiled_table() { + // §4.3: a disabled route is excluded from the match table, so a + // request to it 404s indistinguishably from an absent route. + let active = route_with_path("/on"); + let mut disabled = route_with_path("/off"); + disabled.enabled = false; + let compiled = compile_routes(&[active.clone(), disabled.clone()]); + let ids: Vec = compiled.iter().map(|c| c.route_id).collect(); + assert_eq!(ids, vec![active.id], "only the enabled route compiles"); + } } diff --git a/crates/orchestrator-core/src/api.rs b/crates/orchestrator-core/src/api.rs index a6073cc..4e49588 100644 --- a/crates/orchestrator-core/src/api.rs +++ b/crates/orchestrator-core/src/api.rs @@ -121,6 +121,11 @@ where .resolve(id) .await? .ok_or(ApiError::NotFound(id))?; + // A disabled script (§4.3) is not invocable — 404, indistinguishable + // from an absent one (no info leak that the id exists but is off). + if !script.enabled { + return Err(ApiError::NotFound(id)); + } let mut req = build_exec_request(id, &script.name, &headers, &body, script.app_id, principal)?; req.sandbox_overrides = script.sandbox; @@ -164,6 +169,7 @@ where Ok(exec_response_to_http(outcome?)) } +#[allow(clippy::too_many_lines)] async fn user_route_handler( State(state): State>, Extension(principal): Extension>, @@ -221,6 +227,12 @@ where .resolve(matched.matched.script_id) .await? .ok_or(ApiError::NotFound(matched.matched.script_id))?; + // An enabled route bound to a disabled script (§4.3, §4.7) is + // unreachable: 404 as if no route matched (the disabled route is already + // dropped from the match table; this covers the route-on/script-off case). + if !script.enabled { + return Err(ApiError::NotFound(matched.matched.script_id)); + } // Drain the body now that we know we'll execute. 10 MiB cap matches // the conservative default response/request size in the blueprint.