From b0a8b4d4f611aac35a553bb5789c6f34b5af344c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 20:07:27 -0700 Subject: [PATCH 1/2] test(hub): process provisioner forwards the sidecar credential key (CL-8420) --- apps/hub/src/provisioners/process.test.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/hub/src/provisioners/process.test.ts b/apps/hub/src/provisioners/process.test.ts index 7961524d8c..fda9dfbf82 100644 --- a/apps/hub/src/provisioners/process.test.ts +++ b/apps/hub/src/provisioners/process.test.ts @@ -18,17 +18,19 @@ import { const HUB_WS_URL = "ws://127.0.0.1:3000/api/sidecars/ws"; const SIDECAR_ENTRY = "/srv/workbench/apps/sidecar/src/index.ts"; +const SIDECAR_KEY = "22".repeat(32); describe("readProcessProvisionerConfig", () => { test("an unconfigured environment resolves this repository's sidecar entry and the running bun", () => { const config = readProcessProvisionerConfig({ - env: {}, + env: { SIDECAR_CREDENTIAL_ENCRYPTION_KEY: SIDECAR_KEY }, dataDir: "/srv/hub-data/process-provisioner", hubWebSocketUrl: HUB_WS_URL, }); expect(config.sidecarEntryPath).toEndWith("/apps/sidecar/src/index.ts"); expect(config.runtimePath).toBe(process.execPath); + expect(config.sidecarCredentialEncryptionKey).toBe(SIDECAR_KEY); expect(config.allocationsDir).toBe("/srv/hub-data/process-provisioner/allocations"); expect(config.stateFilePath).toBe("/srv/hub-data/process-provisioner/state.json"); expect(config.hubWebSocketUrl).toBe(HUB_WS_URL); @@ -39,6 +41,7 @@ describe("readProcessProvisionerConfig", () => { env: { PROCESS_PROVISIONER_SIDECAR_ENTRY: "/opt/sidecar/index.js", PROCESS_PROVISIONER_RUNTIME: "/usr/bin/node", + SIDECAR_CREDENTIAL_ENCRYPTION_KEY: SIDECAR_KEY, }, dataDir: "/srv/hub-data/process-provisioner", hubWebSocketUrl: HUB_WS_URL, @@ -51,7 +54,7 @@ describe("readProcessProvisionerConfig", () => { test("a relative data dir fails loudly instead of resolving against the cwd", () => { expect(() => readProcessProvisionerConfig({ - env: {}, + env: { SIDECAR_CREDENTIAL_ENCRYPTION_KEY: SIDECAR_KEY }, dataDir: ".data/hub/process-provisioner", hubWebSocketUrl: HUB_WS_URL, }), @@ -61,7 +64,17 @@ describe("readProcessProvisionerConfig", () => { test("an empty override is a misconfiguration, not an unset key", () => { expect(() => readProcessProvisionerConfig({ - env: { PROCESS_PROVISIONER_RUNTIME: "" }, + env: { PROCESS_PROVISIONER_RUNTIME: "", SIDECAR_CREDENTIAL_ENCRYPTION_KEY: SIDECAR_KEY }, + dataDir: "/srv/hub-data/process-provisioner", + hubWebSocketUrl: HUB_WS_URL, + }), + ).toThrow("invalid process provisioner environment"); + }); + + test("a hub without the sidecar key cannot provision at all", () => { + expect(() => + readProcessProvisionerConfig({ + env: {}, dataDir: "/srv/hub-data/process-provisioner", hubWebSocketUrl: HUB_WS_URL, }), @@ -76,6 +89,7 @@ async function configIn(dataDir: string): Promise { allocationsDir: resolve(dataDir, "allocations"), stateFilePath: resolve(dataDir, "state.json"), hubWebSocketUrl: HUB_WS_URL, + sidecarCredentialEncryptionKey: SIDECAR_KEY, }; } @@ -131,6 +145,7 @@ describe("createProcessSidecarProvisioner", () => { resolve(config.allocationsDir, "alloc-1", "gen-1", "data"), ); expect(spawn?.env["PATH"]).toBe(process.env["PATH"]); + expect(spawn?.env["SIDECAR_CREDENTIAL_ENCRYPTION_KEY"]).toBe(SIDECAR_KEY); }); test("ensure records the pid so a restarted hub can still find the unit", async () => { From 0a8c8cc0790017b3cf925212d82c58e55776f550 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 20:07:27 -0700 Subject: [PATCH 2/2] fix(hub): process provisioner forwards SIDECAR_CREDENTIAL_ENCRYPTION_KEY to spawned sidecars (CL-8420) --- apps/hub/src/provisioners/process.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/hub/src/provisioners/process.ts b/apps/hub/src/provisioners/process.ts index 5d9c4beb59..310322838e 100644 --- a/apps/hub/src/provisioners/process.ts +++ b/apps/hub/src/provisioners/process.ts @@ -34,6 +34,7 @@ const DEFAULT_SIDECAR_ENTRY = resolve( const Environment = type({ "PROCESS_PROVISIONER_SIDECAR_ENTRY?": "string > 0", "PROCESS_PROVISIONER_RUNTIME?": "string > 0", + SIDECAR_CREDENTIAL_ENCRYPTION_KEY: "string > 0", }); export type ProcessProvisionerConfig = { @@ -54,6 +55,8 @@ export type ProcessProvisionerConfig = { * binding fingerprint, since a hub moved to a new address is a * different backend binding even with the same entry point. */ readonly hubWebSocketUrl: string; + /** The operator key every spawned sidecar seals its at-rest credentials under. */ + readonly sidecarCredentialEncryptionKey: string; }; export type ReadProcessProvisionerConfigArgs = { @@ -67,7 +70,7 @@ export type ReadProcessProvisionerConfigArgs = { readonly hubWebSocketUrl: string; }; -/** Both env keys are optional: unconfigured, this spawns the repo's own apps/sidecar with the running bun. */ +/** The entry and runtime keys are optional: unconfigured, this spawns the repo's own apps/sidecar with the running bun. */ export function readProcessProvisionerConfig( args: ReadProcessProvisionerConfigArgs, ): ProcessProvisionerConfig { @@ -89,6 +92,7 @@ export function readProcessProvisionerConfig( allocationsDir: resolve(args.dataDir, "allocations"), stateFilePath: resolve(args.dataDir, "state.json"), hubWebSocketUrl: args.hubWebSocketUrl, + sidecarCredentialEncryptionKey: parsed.SIDECAR_CREDENTIAL_ENCRYPTION_KEY, }; } @@ -184,7 +188,7 @@ export function createProcessBackend( async startUnit(args: StartUnitArgs): Promise { const unitDir = unitDirOf(args.allocationId, args.generation); const sidecarDataDir = resolve(unitDir, SIDECAR_DATA_DIR_NAME); - const env = sidecarEnvFor(args, sidecarDataDir); + const env = sidecarEnvFor(args, sidecarDataDir, config.sidecarCredentialEncryptionKey); try { await mkdir(sidecarDataDir, { recursive: true, mode: 0o700 }); } catch (error) { @@ -293,7 +297,11 @@ export function createProcessBackend( } /** A sidecar learns everything else over the wire; nothing else of the hub's env is inherited. */ -function sidecarEnvFor(args: StartUnitArgs, sidecarDataDir: string): Record { +function sidecarEnvFor( + args: StartUnitArgs, + sidecarDataDir: string, + sidecarCredentialEncryptionKey: string, +): Record { const path = process.env["PATH"]; if (path === undefined || path === "") { const error = new Error( @@ -312,6 +320,7 @@ function sidecarEnvFor(args: StartUnitArgs, sidecarDataDir: string): Record