From cc95ff7b19a3b93b020dcb6c5a11b65f8e5ff3ab Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 2 Nov 2026 13:13:59 +0000 Subject: [PATCH 1/2] test(providers): reproduce ~/ binaryPath not expanded for OpenCode, Grok and Pi These tests reproduce #29302: a binaryPath starting with ~/ reaches the spawned command unexpanded for the OpenCode, Grok and Pi drivers, so the status probe fails with ENOENT and the provider shows "Not found". Co-Authored-By: Claude Opus 5.5 --- .../provider-grok/src/server/driver.test.ts | 26 +++++++++++++++++++ .../src/server/driver.test.ts | 24 +++++++++++++++++ .../provider-pi/src/server/driver.test.ts | 19 ++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/packages/provider-grok/src/server/driver.test.ts b/packages/provider-grok/src/server/driver.test.ts index 3a080ec60582a..9f37d88b70be8 100644 --- a/packages/provider-grok/src/server/driver.test.ts +++ b/packages/provider-grok/src/server/driver.test.ts @@ -172,6 +172,32 @@ it.layer(layerTest)("GrokDriver", (it) => { }); } + it.effect("probes a ~/ binary path from the home directory", () => { + const spawned: Array = []; + return Effect.gen(function* () { + const path = yield* Path.Path; + const instance = yield* GrokDriver.create({ + instanceId: ProviderInstanceId.make("grok-home"), + displayName: "Grok test", + enabled: true, + environment: [], + config: { ...GrokDriver.defaultConfig(), binaryPath: "~/bin/grok" }, + }).pipe(Effect.provideService(HostProcess.HomeDirectory, "/test-home")); + yield* instance.snapshot.refresh; + expect(spawned).not.toHaveLength(0); + expect(new Set(spawned)).toEqual(new Set([path.join("/test-home", "bin", "grok")])); + }).pipe( + Effect.provideService( + ChildProcessSpawner.ChildProcessSpawner, + stdoutSpawner((command) => { + spawned.push(command); + return "grok 1.0.0\n"; + }), + ), + Effect.scoped, + ); + }); + it.effect("stays manual-only when the configured executable does not exist", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index fe0826e7a0026..296fd6ecd09d9 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -88,6 +88,30 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => { }).pipe(Effect.scoped), ); + it.effect("probes and starts a ~/ binary path from the home directory", () => + Effect.gen(function* () { + const binaryPaths: Array = []; + const record = ({ binaryPath }: { readonly binaryPath: string }) => + Effect.sync(() => binaryPaths.push(binaryPath)); + const instance = yield* create({ binaryPath: "~/bin/opencode" }, noHttp).pipe( + Effect.provideService(HostProcess.HomeDirectory, "/test-home"), + Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, { + ...openCode2Runtime, + runOpenCodeCommand: (input) => + record(input).pipe(Effect.andThen(openCode2Runtime.runOpenCodeCommand(input))), + startOpenCodeServerProcess: (input) => + record(input).pipe(Effect.andThen(reachedServer("start"))), + }), + ); + yield* instance.snapshot.refresh; + assert.isNotEmpty(binaryPaths); + assert.deepStrictEqual( + [...new Set(binaryPaths)], + [NodePath.join("/test-home", "bin", "opencode")], + ); + }).pipe(Effect.scoped), + ); + it.effect("answers capability reads without waiting on an unreachable server", () => Effect.gen(function* () { const hang = HttpClient.make(() => Effect.never); diff --git a/packages/provider-pi/src/server/driver.test.ts b/packages/provider-pi/src/server/driver.test.ts index 087a8089aaee8..6e2e8dec432af 100644 --- a/packages/provider-pi/src/server/driver.test.ts +++ b/packages/provider-pi/src/server/driver.test.ts @@ -1,9 +1,11 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { DEFAULT_SERVER_SETTINGS, ProviderInstanceId } from "@t3tools/contracts"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; @@ -172,6 +174,23 @@ it.layer(layerTest)("PiDriver workspace discovery", (it) => { }).pipe(Effect.scoped), ); + it.effect("launches a ~/ binary path from the home directory", () => + Effect.gen(function* () { + const { spawner, launches } = yield* makePiSpawner(); + const instance = yield* create({ binaryPath: "~/bin/pi" }).pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(HostProcess.HomeDirectory, "/test-home"), + ); + yield* instance.snapshot.refresh; + const path = yield* Path.Path; + assert.isNotEmpty(launches); + assert.deepEqual( + [...new Set(launches.map((launch) => launch.command))], + [path.join("/test-home", "bin", "pi")], + ); + }).pipe(Effect.scoped), + ); + it.effect("does not run a disabled provider's workspace probe", () => Effect.gen(function* () { const instance = yield* create({}, false).pipe( From 5d33f3eb906d406d58c804ac8b6b8c674458c81e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 2 Nov 2026 13:20:28 +0000 Subject: [PATCH 2/2] fix(providers): expand ~/ binary paths for OpenCode, Grok and Pi A Binary path such as ~/.local/bin/opencode reached spawn unexpanded for the OpenCode, Grok and Pi drivers, so the status probe failed with ENOENT and Settings showed "Not found". These drivers now expand the path against the home directory like Codex, Claude and Muse, and hand the expanded config to their orchestration adapters so sessions launch the same binary. The Grok reproduction test now checks every argv element, since agent launches can be wrapped in /bin/sh and the maintenance lookup may spawn npm. Fixes pingdotgg/t3code#29302 Co-Authored-By: Claude Opus 5.5 --- packages/provider-grok/src/server/driver.test.ts | 11 ++++++----- packages/provider-grok/src/server/driver.ts | 10 ++++++++-- packages/provider-opencode/src/server/driver.ts | 10 ++++++++-- packages/provider-pi/src/server/driver.ts | 10 ++++++++-- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/provider-grok/src/server/driver.test.ts b/packages/provider-grok/src/server/driver.test.ts index 9f37d88b70be8..87f60732b65e5 100644 --- a/packages/provider-grok/src/server/driver.test.ts +++ b/packages/provider-grok/src/server/driver.test.ts @@ -173,7 +173,7 @@ it.layer(layerTest)("GrokDriver", (it) => { } it.effect("probes a ~/ binary path from the home directory", () => { - const spawned: Array = []; + const argv: Array = []; return Effect.gen(function* () { const path = yield* Path.Path; const instance = yield* GrokDriver.create({ @@ -184,13 +184,14 @@ it.layer(layerTest)("GrokDriver", (it) => { config: { ...GrokDriver.defaultConfig(), binaryPath: "~/bin/grok" }, }).pipe(Effect.provideService(HostProcess.HomeDirectory, "/test-home")); yield* instance.snapshot.refresh; - expect(spawned).not.toHaveLength(0); - expect(new Set(spawned)).toEqual(new Set([path.join("/test-home", "bin", "grok")])); + // Agent launches may be wrapped in /bin/sh, so look at every argv element. + expect(argv).toContain(path.join("/test-home", "bin", "grok")); + expect(argv).not.toContain("~/bin/grok"); }).pipe( Effect.provideService( ChildProcessSpawner.ChildProcessSpawner, - stdoutSpawner((command) => { - spawned.push(command); + stdoutSpawner((command, args) => { + argv.push(command, ...args); return "grok 1.0.0\n"; }), ), diff --git a/packages/provider-grok/src/server/driver.ts b/packages/provider-grok/src/server/driver.ts index acb8690509e2d..0ff0707e83609 100644 --- a/packages/provider-grok/src/server/driver.ts +++ b/packages/provider-grok/src/server/driver.ts @@ -29,6 +29,8 @@ import { } from "@t3tools/provider-core/server/driver"; import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { discoverGrokSkills } from "./skills.ts"; import { homebrewOwnershipFromCommandPath, @@ -137,7 +139,11 @@ export const GrokDriver: ProviderDriver accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); - const effectiveConfig = { ...config, enabled } satisfies GrokSettings; + const effectiveConfig = { + ...config, + enabled, + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), + } satisfies GrokSettings; const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution( resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, @@ -154,7 +160,7 @@ export const GrokDriver: ProviderDriver accentColor, environment, enabled, - config, + config: effectiveConfig, }).pipe( Effect.mapError( (cause) => diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 95da30946e138..fbfcc0fcfbcfd 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -50,6 +50,8 @@ import { } from "@t3tools/provider-core/server/driver"; import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { enrichProviderSnapshotWithVersionAdvisory, makeCachedProviderMaintenanceResolution, @@ -207,7 +209,11 @@ export const OpenCodeDriver: ProviderDriver< accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); - const effectiveConfig = { ...config, enabled } satisfies OpenCodeSettings; + const effectiveConfig = { + ...config, + enabled, + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), + } satisfies OpenCodeSettings; const runtimeProbe = yield* makeOpenCodeRuntimeProbe( probeOpenCodeRuntime(effectiveConfig, processEnv).pipe( Effect.provideService(HttpClient.HttpClient, httpClient), @@ -252,7 +258,7 @@ export const OpenCodeDriver: ProviderDriver< accentColor, environment, enabled, - config, + config: effectiveConfig, }).pipe( Effect.mapError( (cause) => diff --git a/packages/provider-pi/src/server/driver.ts b/packages/provider-pi/src/server/driver.ts index c37e5d757f382..5bd999dd131b4 100644 --- a/packages/provider-pi/src/server/driver.ts +++ b/packages/provider-pi/src/server/driver.ts @@ -28,6 +28,8 @@ import { } from "@t3tools/provider-core/server/driver"; import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { makeCachedProviderMaintenanceResolution, makePackageManagedProviderMaintenanceResolver, @@ -108,7 +110,11 @@ export const PiDriver: ProviderDriver = { accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); - const effectiveConfig = { ...config, enabled } satisfies PiSettings; + const effectiveConfig = { + ...config, + enabled, + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), + } satisfies PiSettings; const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution( resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, @@ -126,7 +132,7 @@ export const PiDriver: ProviderDriver = { accentColor, environment, enabled, - config, + config: effectiveConfig, }).pipe( Effect.mapError( (cause) =>