From 7db5fca413dc0d282d8d387ca2dcb1faa2474447 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:42:57 +0100 Subject: [PATCH 1/3] feat(supervisor): make the runner seccomp profile path configurable --- apps/supervisor/src/env.ts | 1 + .../src/workloadManager/kubernetes.test.ts | 37 +++++++++---------- .../src/workloadManager/kubernetes.ts | 4 +- .../src/workloadManager/kubernetesPodSpec.ts | 21 ++++------- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index c5aaf70e1fe..bb03dbd51f5 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -210,6 +210,7 @@ export const Env = z KUBERNETES_MEMORY_OVERHEAD_GB: z.coerce.number().min(0).optional(), // Optional memory overhead to add to the limit in GB KUBERNETES_SCHEDULER_NAME: z.string().optional(), // Custom scheduler name for pods + KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z.string().default("profiles/block-io-uring.json"), // Pod DNS config — override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`. // Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked diff --git a/apps/supervisor/src/workloadManager/kubernetes.test.ts b/apps/supervisor/src/workloadManager/kubernetes.test.ts index 3c6419e9c6f..b012f032187 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.test.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.test.ts @@ -1,9 +1,8 @@ import { describe, expect, it } from "vitest"; import { - BLOCK_IO_URING_SECCOMP_PROFILE, nodetypeNodeSelector, runPodTolerations, - withBlockIoUringSeccompProfile, + withRunnerSeccompProfile, withNodeSelector, } from "./kubernetesPodSpec.js"; @@ -100,27 +99,25 @@ describe("withNodeSelector", () => { }); }); -describe("withBlockIoUringSeccompProfile", () => { - it("adds the Localhost io_uring profile for node-24 and above, preserving pod security defaults", () => { - for (const runtime of ["node-24", "node-26", "node-30", "experimental-node-24"]) { - const podSpec = withBlockIoUringSeccompProfile(basePodSpec, runtime); - - expect(podSpec).toMatchObject({ - ...basePodSpec, - securityContext: { - ...basePodSpec.securityContext, - seccompProfile: { - type: "Localhost", - localhostProfile: BLOCK_IO_URING_SECCOMP_PROFILE, - }, +describe("withRunnerSeccompProfile", () => { + it("applies the profile for every runtime, preserving pod security defaults", () => { + const podSpec = withRunnerSeccompProfile(basePodSpec, "profiles/example.json"); + + expect(podSpec).toMatchObject({ + ...basePodSpec, + securityContext: { + ...basePodSpec.securityContext, + seccompProfile: { + type: "Localhost", + localhostProfile: "profiles/example.json", }, - }); - } + }, + }); }); - it("leaves the pod spec unchanged for runtimes that do not create io_uring fds", () => { - for (const runtime of ["node", "node-22", "bun", undefined, null, ""]) { - expect(withBlockIoUringSeccompProfile(basePodSpec, runtime)).toEqual(basePodSpec); + it("leaves the pod spec untouched when no profile is configured", () => { + for (const profilePath of [undefined, ""]) { + expect(withRunnerSeccompProfile(basePodSpec, profilePath)).toBe(basePodSpec); } }); }); diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index e0bf01a050f..43b65f6bda1 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -17,7 +17,7 @@ import { getRunnerId } from "../util.js"; import { nodetypeNodeSelector, runPodTolerations, - withBlockIoUringSeccompProfile, + withRunnerSeccompProfile, withNodeSelector, } from "./kubernetesPodSpec.js"; @@ -136,7 +136,7 @@ export class KubernetesWorkloadManager implements WorkloadManager { } } const podSpec = this.opts.checkpointsEnabled - ? withBlockIoUringSeccompProfile(basePodSpec, opts.runtime) + ? withRunnerSeccompProfile(basePodSpec, env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH) : basePodSpec; await this.k8s.core.createNamespacedPod({ diff --git a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts index f521369f082..8ed7edad87e 100644 --- a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts +++ b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts @@ -1,11 +1,5 @@ import type { k8s } from "../clients/kubernetes.js"; -/** - * Relative path (kubelet seccomp root) of the profile blocking only io_uring - * syscalls. Must match the profile deployed to worker nodes. - */ -export const BLOCK_IO_URING_SECCOMP_PROFILE = "profiles/block-io-uring.json"; - /** * An empty label is the documented off-switch, leaving the pod unpinned. The Helm * chart ships an empty value, so don't collapse this into a fallback default - @@ -61,16 +55,15 @@ export function withNodeSelector( } /** - * Node >= 24 always creates io_uring fds, which can't be checkpointed. Blocking - * io_uring_setup makes libuv fall back to epoll. Other runtimes don't need this, - * so the profile is only applied for node-24+. Tolerates an "experimental-" prefix. + * Applies the runner seccomp profile. The profile is a node-local file installed + * outside this repo, so an empty path leaves the pod on the runtime default - + * pointing at a profile the nodes don't have fails pod creation. */ -export function withBlockIoUringSeccompProfile( +export function withRunnerSeccompProfile( podSpec: Omit, - runtime: string | null | undefined + profilePath: string | undefined ): Omit { - const match = runtime ? /^(?:experimental-)?node-(\d+)$/.exec(runtime) : null; - if (!match || Number(match[1]) < 24) { + if (!profilePath) { return podSpec; } @@ -80,7 +73,7 @@ export function withBlockIoUringSeccompProfile( ...podSpec.securityContext, seccompProfile: { type: "Localhost", - localhostProfile: BLOCK_IO_URING_SECCOMP_PROFILE, + localhostProfile: profilePath, }, }, }; From 84c4a91553395bf82ce60c6ad58837cf91dbced6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:46:55 +0100 Subject: [PATCH 2/3] refactor(supervisor): drop the unused runtime create option --- apps/supervisor/src/index.ts | 1 - apps/supervisor/src/workloadManager/types.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index 542841bd6e5..a2e95ab7926 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -653,7 +653,6 @@ class ManagedSupervisor { projectId: message.project.id, deploymentFriendlyId: message.deployment.friendlyId, deploymentVersion: message.backgroundWorker.version, - runtime: message.backgroundWorker.runtime, deploymentToken, runId: message.run.id, runFriendlyId: message.run.friendlyId, diff --git a/apps/supervisor/src/workloadManager/types.ts b/apps/supervisor/src/workloadManager/types.ts index eda9162c7d6..d0ca48d7a96 100644 --- a/apps/supervisor/src/workloadManager/types.ts +++ b/apps/supervisor/src/workloadManager/types.ts @@ -42,8 +42,6 @@ export interface WorkloadManagerCreateOptions { projectId: string; deploymentFriendlyId: string; deploymentVersion: string; - // Canonical runtime identifier (e.g. "node", "node-22", "node-24") - runtime?: string; // When set, overrides the TRIGGER_DEPLOYMENT_ID value the runner forwards as its identity header. deploymentToken?: string; runId: string; From 1b71ae70b9d6849d3bb3142be09d04fd8a6006a9 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:57:18 +0100 Subject: [PATCH 3/3] feat(supervisor): gate the runner seccomp profile by runtime scope --- apps/supervisor/src/env.ts | 9 ++- apps/supervisor/src/index.ts | 1 + .../src/workloadManager/kubernetes.test.ts | 62 ++++++++++++++----- .../src/workloadManager/kubernetes.ts | 9 ++- .../src/workloadManager/kubernetesPodSpec.ts | 30 +++++++-- apps/supervisor/src/workloadManager/types.ts | 2 + 6 files changed, 88 insertions(+), 25 deletions(-) diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index bb03dbd51f5..fc18b7f09d8 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -210,7 +210,14 @@ export const Env = z KUBERNETES_MEMORY_OVERHEAD_GB: z.coerce.number().min(0).optional(), // Optional memory overhead to add to the limit in GB KUBERNETES_SCHEDULER_NAME: z.string().optional(), // Custom scheduler name for pods - KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z.string().default("profiles/block-io-uring.json"), + KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z + .string() + .trim() + .min(1) + .default("profiles/block-io-uring.json"), + KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES: z + .enum(["none", "node-24-plus", "all"]) + .default("node-24-plus"), // Pod DNS config — override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`. // Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index a2e95ab7926..542841bd6e5 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -653,6 +653,7 @@ class ManagedSupervisor { projectId: message.project.id, deploymentFriendlyId: message.deployment.friendlyId, deploymentVersion: message.backgroundWorker.version, + runtime: message.backgroundWorker.runtime, deploymentToken, runId: message.run.id, runFriendlyId: message.run.friendlyId, diff --git a/apps/supervisor/src/workloadManager/kubernetes.test.ts b/apps/supervisor/src/workloadManager/kubernetes.test.ts index b012f032187..e3023bc5d84 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.test.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.test.ts @@ -100,24 +100,56 @@ describe("withNodeSelector", () => { }); describe("withRunnerSeccompProfile", () => { - it("applies the profile for every runtime, preserving pod security defaults", () => { - const podSpec = withRunnerSeccompProfile(basePodSpec, "profiles/example.json"); + const base = { + profilePath: "profiles/example.json", + runtimes: "node-24-plus" as const, + runtime: "node-24", + checkpointsEnabled: true, + }; + + const withProfile = { + ...basePodSpec, + securityContext: { + ...basePodSpec.securityContext, + seccompProfile: { type: "Localhost", localhostProfile: "profiles/example.json" }, + }, + }; + + it("applies the profile to node-24 and above under the default scope", () => { + for (const runtime of ["node-24", "node-26", "node-30", "experimental-node-24"]) { + expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toMatchObject( + withProfile + ); + } + }); - expect(podSpec).toMatchObject({ - ...basePodSpec, - securityContext: { - ...basePodSpec.securityContext, - seccompProfile: { - type: "Localhost", - localhostProfile: "profiles/example.json", - }, - }, - }); + it("skips older runtimes under the default scope", () => { + for (const runtime of ["node", "node-22", "bun", undefined, null, ""]) { + expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toBe(basePodSpec); + } + }); + + it("applies the profile to every runtime under the all scope", () => { + for (const runtime of ["node", "node-22", "bun", "node-24", undefined]) { + expect( + withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "all", runtime }) + ).toMatchObject(withProfile); + } + }); + + it("applies nothing under the none scope, whatever the runtime", () => { + for (const runtime of ["node-24", "bun", "node-22"]) { + expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "none", runtime })).toBe( + basePodSpec + ); + } }); - it("leaves the pod spec untouched when no profile is configured", () => { - for (const profilePath of [undefined, ""]) { - expect(withRunnerSeccompProfile(basePodSpec, profilePath)).toBe(basePodSpec); + it("applies nothing when checkpoints are disabled", () => { + for (const runtimes of ["none", "node-24-plus", "all"] as const) { + expect( + withRunnerSeccompProfile(basePodSpec, { ...base, runtimes, checkpointsEnabled: false }) + ).toBe(basePodSpec); } }); }); diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 43b65f6bda1..d09c86cf9cc 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -135,9 +135,12 @@ export class KubernetesWorkloadManager implements WorkloadManager { ); } } - const podSpec = this.opts.checkpointsEnabled - ? withRunnerSeccompProfile(basePodSpec, env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH) - : basePodSpec; + const podSpec = withRunnerSeccompProfile(basePodSpec, { + profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH, + runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES, + runtime: opts.runtime, + checkpointsEnabled: this.opts.checkpointsEnabled, + }); await this.k8s.core.createNamespacedPod({ namespace: this.namespace, diff --git a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts index 8ed7edad87e..6c6ddfc09b0 100644 --- a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts +++ b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts @@ -54,26 +54,44 @@ export function withNodeSelector( }; } +export type RunnerSeccompProfileOptions = { + profilePath: string; + runtimes: "none" | "node-24-plus" | "all"; + runtime: string | null | undefined; + checkpointsEnabled: boolean | undefined; +}; + /** - * Applies the runner seccomp profile. The profile is a node-local file installed - * outside this repo, so an empty path leaves the pod on the runtime default - - * pointing at a profile the nodes don't have fails pod creation. + * Applies the runner seccomp profile, which is a node-local file installed outside + * this repo - pointing a pod at a profile its node doesn't have fails pod creation, + * so every condition for skipping it lives here. + * + * "node-24-plus" matches the original rollout: node >= 24 always creates io_uring + * fds, which can't be checkpointed, and blocking io_uring_setup makes libuv fall + * back to epoll. Tolerates an "experimental-" prefix. "bun" matches only under "all". */ export function withRunnerSeccompProfile( podSpec: Omit, - profilePath: string | undefined + options: RunnerSeccompProfileOptions ): Omit { - if (!profilePath) { + if (!options.checkpointsEnabled || options.runtimes === "none") { return podSpec; } + if (options.runtimes === "node-24-plus") { + const match = options.runtime ? /^(?:experimental-)?node-(\d+)$/.exec(options.runtime) : null; + if (!match || Number(match[1]) < 24) { + return podSpec; + } + } + return { ...podSpec, securityContext: { ...podSpec.securityContext, seccompProfile: { type: "Localhost", - localhostProfile: profilePath, + localhostProfile: options.profilePath, }, }, }; diff --git a/apps/supervisor/src/workloadManager/types.ts b/apps/supervisor/src/workloadManager/types.ts index d0ca48d7a96..eda9162c7d6 100644 --- a/apps/supervisor/src/workloadManager/types.ts +++ b/apps/supervisor/src/workloadManager/types.ts @@ -42,6 +42,8 @@ export interface WorkloadManagerCreateOptions { projectId: string; deploymentFriendlyId: string; deploymentVersion: string; + // Canonical runtime identifier (e.g. "node", "node-22", "node-24") + runtime?: string; // When set, overrides the TRIGGER_DEPLOYMENT_ID value the runner forwards as its identity header. deploymentToken?: string; runId: string;