authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 03:07:18-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 03:07:18-07:00
log2271a27dad1205b403a66af01461f856986e5064
tree4d02aee5070c4a452a16d695d2770100b81e62cd
parent09aac71563c66a4f65f6fbe701aa9596cb677767
signature Signed by PGP key B5690EEEBB952194

fix(server): keep Homebrew mise shims manual-only (#10085)


3 files changed, 184 insertions(+), 14 deletions(-)

apps/server/src/provider/Drivers/CodexDriver.test.ts+151
......@@ -8,7 +8,10 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
88import * as Effect from "effect/Effect";
99import * as FileSystem from "effect/FileSystem";
1010import * as Layer from "effect/Layer";
11import * as Sink from "effect/Sink";
12import * as Stream from "effect/Stream";
1113import { HttpClient } from "effect/unstable/http";
14import * as ChildProcess from "effect/unstable/process/ChildProcess";
1215import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
1316
1417import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
......@@ -17,6 +20,11 @@ import { ServerSettingsService } from "../../serverSettings.ts";
1720import { layerTest as codexResetCreditLayerTest } from "../Layers/codexResetCredit.ts";
1821import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
1922import * as ModelManifest from "../ModelManifest.ts";
23import {
24 createProviderVersionAdvisory,
25 ProviderVersionCache,
26 resolveLatestProviderVersion,
27} from "../providerMaintenance.ts";
2028import { CodexDriver } from "./CodexDriver.ts";
2129
2230const testLayer = ServerConfig.layerTest(process.cwd(), {
......@@ -218,4 +226,147 @@ it.layer(testLayer)("CodexDriver", (it) => {
218226 ),
219227 );
220228 }
229
230 it.effect.each([
231 {
232 name: "conventional shim",
233 dataRoot: "mise",
234 commandName: "codex",
235 version: "0.153.4",
236 nodeFirst: false,
237 },
238 {
239 name: "custom data directory",
240 dataRoot: "custom-tool-data",
241 commandName: "codex",
242 version: "0.153.4",
243 nodeFirst: false,
244 },
245 {
246 name: "renamed configured command",
247 dataRoot: "mise",
248 commandName: "custom-codex",
249 version: "0.153.4",
250 nodeFirst: false,
251 },
252 {
253 name: "outdated provider",
254 dataRoot: "mise",
255 commandName: "codex",
256 version: "0.153.3",
257 nodeFirst: false,
258 },
259 {
260 name: "npm before shim",
261 dataRoot: "mise",
262 commandName: "codex",
263 version: "0.153.4",
264 nodeFirst: true,
265 },
266 ])(
267 "does not mistake Homebrew mise for Codex's installer: $name",
268 (fixture) =>
269 Effect.gen(function* () {
270 const fs = yield* FileSystem.FileSystem;
271 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-mise-shim-" });
272 const brewPrefix = NodePath.join(tempDir, "homebrew");
273 const brewPath = NodePath.join(brewPrefix, "bin", "brew");
274 const misePath = NodePath.join(brewPrefix, "Cellar", "mise", "2026.9.1", "bin", "mise");
275 const shimDir = NodePath.join(tempDir, fixture.dataRoot, "shims");
276 const npmPrefix = NodePath.join(tempDir, "mise", "installs", "node", "24.13.0");
277 const npmBin = NodePath.join(npmPrefix, "bin");
278 const npmEntry = NodePath.join(
279 npmPrefix,
280 "lib",
281 "node_modules",
282 "@openai",
283 "codex",
284 "bin",
285 "codex.js",
286 );
287 for (const file of [brewPath, misePath, npmEntry]) {
288 yield* fs.makeDirectory(NodePath.dirname(file), { recursive: true });
289 yield* fs.writeFileString(file, "#!/bin/sh\n");
290 yield* fs.chmod(file, 0o755);
291 }
292 yield* fs.makeDirectory(shimDir, { recursive: true });
293 yield* fs.makeDirectory(npmBin, { recursive: true });
294 yield* fs.symlink(misePath, NodePath.join(shimDir, fixture.commandName));
295 yield* fs.symlink(npmEntry, NodePath.join(npmBin, fixture.commandName));
296 const lookupPath = [
297 ...(fixture.nodeFirst ? [npmBin, shimDir] : [shimDir, npmBin]),
298 NodePath.dirname(brewPath),
299 ].join(NodePath.delimiter);
300 const probes: Array<ReadonlyArray<string>> = [];
301 const metadataSpawner = ChildProcessSpawner.make((command) => {
302 if (!ChildProcess.isStandardCommand(command) || command.command !== brewPath) {
303 return Effect.die("Provider resolution must not execute a provider or updater");
304 }
305 probes.push(command.args);
306 const stdout =
307 command.args[0] === "--prefix"
308 ? brewPrefix
309 : JSON.stringify({ formulae: [{ versions: { stable: "2026.9.1" } }] });
310 return Effect.succeed(
311 ChildProcessSpawner.makeHandle({
312 pid: ChildProcessSpawner.ProcessId(1),
313 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
314 isRunning: Effect.succeed(false),
315 kill: () => Effect.void,
316 unref: Effect.succeed(Effect.void),
317 stdin: Sink.drain,
318 stdout: Stream.encodeText(Stream.make(stdout)),
319 stderr: Stream.empty,
320 all: Stream.empty,
321 getInputFd: () => Sink.drain,
322 getOutputFd: () => Stream.empty,
323 }),
324 );
325 });
326 const instance = yield* CodexDriver.create({
327 instanceId: ProviderInstanceId.make("codex-mise-shim"),
328 displayName: "Codex shim test",
329 enabled: false,
330 environment: [{ name: "PATH", value: lookupPath, sensitive: false }],
331 config: {
332 ...CodexDriver.defaultConfig(),
333 binaryPath: fixture.commandName,
334 homePath: NodePath.join(tempDir, "codex-home"),
335 },
336 }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, metadataSpawner));
337 const capabilities = yield* instance.snapshot.resolveMaintenance();
338 const latestVersion = yield* resolveLatestProviderVersion(capabilities).pipe(
339 Effect.provideService(
340 ProviderVersionCache,
341 new Map([
342 ["@openai/codex", { expiresAt: Number.MAX_SAFE_INTEGER, version: "0.153.4" }],
343 ]),
344 ),
345 );
346 expect(probes).toEqual([]);
347 expect(latestVersion).toBe("0.153.4");
348 expect(
349 createProviderVersionAdvisory({
350 driver: CodexDriver.driverKind,
351 currentVersion: fixture.version,
352 latestVersion,
353 maintenanceCapabilities: capabilities,
354 }),
355 ).toMatchObject({
356 status: fixture.version === "0.153.4" ? "current" : "behind_latest",
357 currentVersion: fixture.version,
358 latestVersion: "0.153.4",
359 canUpdate: fixture.nodeFirst,
360 });
361 if (fixture.nodeFirst) {
362 expect(capabilities.update).toMatchObject({
363 executable: "npm",
364 args: expect.arrayContaining(["--prefix", npmPrefix, "@openai/codex@latest"]),
365 });
366 } else {
367 expect(capabilities.update).toBeNull();
368 }
369 }).pipe(Effect.scoped),
370 { skip: windowsHost },
371 );
221372});
apps/server/src/provider/providerMaintenance.test.ts+29-14
......@@ -595,25 +595,29 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
595595 }),
596596 );
597597
598 it.effect.skipIf(!symlinksSupported)(
599 "upgrades the Homebrew cask that owns the binary and compares against its version",
600 () =>
598 it.effect.each([
599 { directory: "Caskroom", name: "package-tool", kind: "cask" },
600 { directory: "Cellar", name: "package-tool", kind: "formula" },
601 { directory: "Cellar", name: "package-tool@latest", kind: "formula" },
602 ] as const)(
603 "upgrades the owning Homebrew $kind $name through an executable alias",
604 (fixture) =>
601605 Effect.gen(function* () {
602606 const tempDir = yield* makeTempDir("t3-homebrew-capabilities");
603607 const brewBinDir = NodePath.join(tempDir, "brew-bin");
604608 const brewPath = NodePath.join(brewBinDir, "brew");
605609 writeExecutable(brewPath);
606 const caskBinary = NodePath.join(
610 const ownedBinary = NodePath.join(
607611 tempDir,
608 "Caskroom",
609 "package-tool",
612 fixture.directory,
613 fixture.name,
610614 "0.148.0",
611 "package-tool",
615 "package-tool-0.148.0",
612616 );
613 writeExecutable(caskBinary);
614 const link = NodePath.join(tempDir, "bin", "package-tool");
617 writeExecutable(ownedBinary);
618 const link = NodePath.join(tempDir, "bin", "custom-package-tool");
615619 NodeFS.mkdirSync(NodePath.dirname(link), { recursive: true });
616 NodeFS.symlinkSync(caskBinary, link);
620 NodeFS.symlinkSync(ownedBinary, link);
617621 const spawned: Array<ReadonlyArray<string>> = [];
618622
619623 const capabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(
......@@ -630,27 +634,38 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
630634 spawned.push([command, ...args]);
631635 return args[0] === "--prefix"
632636 ? `${tempDir}\n`
633 : JSON.stringify({ casks: [{ version: "0.148.0,42" }] });
637 : JSON.stringify(
638 fixture.kind === "cask"
639 ? { casks: [{ version: "0.148.0,42" }] }
640 : { formulae: [{ versions: { stable: "0.148.0" } }] },
641 );
634642 }),
635643 ),
636644 );
637645
638646 expect(spawned).toEqual([
639647 [brewPath, "--prefix"],
640 [brewPath, "info", "--json=v2", "package-tool"],
648 [brewPath, "info", "--json=v2", fixture.name],
641649 ]);
642650 expect(capabilities).toEqual({
643651 provider: driver("packageTool"),
644652 packageName: "@example/package-tool",
645653 latestVersion: "0.148.0",
646654 update: {
647 command: "brew upgrade --cask package-tool",
655 command:
656 fixture.kind === "cask"
657 ? `brew upgrade --cask ${fixture.name}`
658 : `brew upgrade ${fixture.name}`,
648659 executable: brewPath,
649 args: ["upgrade", "--cask", "package-tool"],
660 args:
661 fixture.kind === "cask"
662 ? ["upgrade", "--cask", fixture.name]
663 : ["upgrade", fixture.name],
650664 lockKey: "homebrew",
651665 },
652666 });
653667 }),
668 { skip: !symlinksSupported },
654669 );
655670
656671 it.effect.skipIf(windowsHost)(
apps/server/src/provider/providerMaintenance.ts+4
......@@ -432,6 +432,10 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn(
432432
433433 const homebrew = homebrewOwnershipFromCommandPath(context.realCommandPath);
434434 if (homebrew) {
435 // Mise shims resolve to the version manager, not the provider.
436 if (homebrew.kind === "formula" && homebrew.name.toLowerCase() === "mise") {
437 return manual;
438 }
435439 const brewPath = yield* resolveCommandPath("brew", { env: context.env }).pipe(
436440 Effect.catchTags({ CommandResolutionError: () => Effect.succeed(null) }),
437441 );