From 979aa50c77bb71f7a1e34dc7f4c4396e59f9f27d Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Wed, 21 Oct 2026 20:01:13 -0500 Subject: [PATCH] fix(server): put the updated runtime on the macOS service PATH During `t3 update` the launch agent PATH took the old CLI's runtime directory, and an update run inside the service inherited the old directory from the plist. Swap any PATH entry inside this T3 home's runtime versions directory for the runtime being installed. Fixes #17532. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.test.ts | 28 ++++++++++++++++++-- apps/server/src/cloud/bootService.ts | 31 +++++++++++++++-------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index d3a8af2320c4f..d33215a92dcc8 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -151,6 +151,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( cliVersion = "1.2.3", serviceBaseDir = baseDir, insideService = false, + hostExecPath = "/usr/bin/t3", ) => Effect.gen(function* () { const paths = pinnedRuntimePaths(path, serviceBaseDir, cliVersion, platform); @@ -161,7 +162,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( baseDir: serviceBaseDir, logsDir: path.join(serviceBaseDir, "userdata", "logs"), cliVersion, - host: { execPath: "/usr/bin/t3" }, + host: { execPath: hostExecPath }, }); }).pipe( Effect.provideService(ProcessRunner.ProcessRunner, runner), @@ -169,7 +170,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( Layer.mergeAll( Layer.succeed(HostProcess.Platform, platform), Layer.succeed(HostProcess.UserId, 501), - Layer.succeed(HostProcess.ExecutablePath, "/usr/bin/t3"), + Layer.succeed(HostProcess.ExecutablePath, hostExecPath), Layer.succeed( HttpClient.HttpClient, HttpClient.make(() => Effect.die("no release download expected")), @@ -817,6 +818,29 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + // regression: #17532 + it.effect("puts the runtime an update installs on the launch agent PATH", () => + Effect.gen(function* () { + const { makeService, fs, baseDir, runtime } = yield* makeHarness("darwin"); + const path = yield* Path.Path; + const previous = runtime.versionDir; + const installed = pinnedRuntimePaths(path, baseDir, "1.2.4", "darwin").versionDir; + // The 1.2.3 CLI, or the 1.2.3 service's own PATH, installs 1.2.4. + const updated = yield* makeService( + `/usr/bin:${previous}:/bin`, + "1.2.4", + undefined, + false, + runtime.entryPath, + ); + const plan = yield* updated.install(); + + expect(yield* fs.readFileString(plan.unitPath)).toContain( + ` PATH\n /usr/bin:${installed}:/bin:/opt/homebrew/bin:/usr/local/bin:/usr/sbin:/sbin`, + ); + }), + ); + it.effect("drops PATH directories that cannot be represented in a launch agent plist", () => Effect.gen(function* () { const { service, fs } = yield* makeHarness( diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index c1be1fc2c5ef4..eb28613a46aed 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -17,8 +17,10 @@ import { CLI_RELEASE_BASE_URL_ENV } from "@t3tools/shared/cliRelease"; import * as ProcessRunner from "../processRunner.ts"; import { ensurePinnedRuntimeInstalled, + launcherOwnsVersionsDir, pinnedRuntimeCommand, pinnedRuntimePaths, + pinnedRuntimeVersionsDir, PinnedRuntimeInstallError, } from "./pinnedRuntime.ts"; import { @@ -565,6 +567,8 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const path = yield* Path.Path; const runner = yield* ProcessRunner.ProcessRunner; const host = input.host ?? { execPath: hostExecPath }; + const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion, platform); + const versionsDir = pinnedRuntimeVersionsDir(path, input.baseDir); const xmlSafeInstallerDirectories = installerPath.split(":").filter( (directory) => directory.length > 0 && @@ -573,17 +577,23 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { return code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d; }), ); + // During an update, the running CLI and a PATH inherited from the service name the runtime + // being replaced. The service and its agent shells should find the runtime it starts. const environmentPath = Array.from( - new Set([ - ...xmlSafeInstallerDirectories, - path.dirname(host.execPath), - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - ]), + new Set( + [ + ...xmlSafeInstallerDirectories, + path.dirname(host.execPath), + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + ].map((directory) => + launcherOwnsVersionsDir(path, versionsDir, directory) ? runtimePaths.versionDir : directory, + ), + ), ).join(":"); const detectedManager = selectBootServiceManager({ @@ -597,7 +607,6 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const logPath = path.join(input.logsDir, BOOT_SERVICE_LOG_FILE); const statePath = path.join(input.baseDir, "runtime", SERVICE_STATE_FILE); const restartPendingPath = path.join(input.baseDir, "runtime", SERVICE_RESTART_PENDING_FILE); - const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion, platform); const writeDurably = (filePath: string, contents: string) => Effect.scoped( Effect.gen(function* () {