Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
import {
configureCuaDriverServerEnvironment,
disableCuaDriverServerEnvironment,
} from "../cua/CuaDriverServerEnvironment.ts";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
const MAX_TCP_PORT = 65_535;
Expand Down Expand Up @@ -222,11 +227,13 @@ const startup = Effect.gen(function* () {
const appIdentity = yield* DesktopAppIdentity.DesktopAppIdentity;
const applicationMenu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
const electronApp = yield* ElectronApp.ElectronApp;
const electronDialog = yield* ElectronDialog.ElectronDialog;
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
const linuxUrlHandler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler;
const clerk = yield* DesktopClerk.DesktopClerk;
const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment;
const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
const clientSettings = yield* DesktopClientSettings.DesktopClientSettings;
const preReadyElectronOptions = yield* DesktopPreReadyPlatform.DesktopPreReadyElectronOptions;
const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
const updates = yield* DesktopUpdates.DesktopUpdates;
Expand Down Expand Up @@ -283,6 +290,41 @@ const startup = Effect.gen(function* () {
backend: Option.getOrElse(selectedBackend, () => "unknown"),
});
}
const cuaEnabled = Option.match(yield* clientSettings.get, {
onNone: () => false,
onSome: (settings) => settings.enableCua,
});
if (cuaEnabled) {
const continueWithoutCua = Effect.fn("desktop.continueWithoutCua")(function* (
context: Readonly<Record<string, unknown>>,
) {
yield* disableCuaDriverServerEnvironment();
yield* logStartupError("embedded cua-driver failed to configure", context);
yield* electronDialog
.showMessageBox({
type: "error",
title: "Computer use unavailable",
message: "Cua could not start.",
detail:
"T3 Code will continue without computer use. Check the logs, then restart to try again.",
buttons: ["Continue"],
})
.pipe(Effect.ignore);
});
yield* configureCuaDriverServerEnvironment(
environment.appUserModelId,
environment.platform,
environment.isPackaged ? environment.resourcesPath : undefined,
).pipe(
Effect.catchTags({
CuaDriverConfigurationError: (error) =>
continueWithoutCua({ modulePath: error.modulePath, cause: error.cause }),
CuaDriverNotConfiguredError: () => continueWithoutCua({}),
}),
);
} else {
yield* disableCuaDriverServerEnvironment();
}
yield* appIdentity.configure;
yield* applicationMenu.configure;
yield* updates.configure;
Expand Down
139 changes: 139 additions & 0 deletions apps/desktop/src/cua/CuaDriverServerEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, expect, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";

import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Path from "effect/Path";

import {
configureCuaDriverServerEnvironment,
CuaDriverNotConfiguredError,
disableCuaDriverServerEnvironment,
resolveEmbeddedDriverPath,
T3CODE_CUA_DRIVER_HOST_BUNDLE_ID_ENV,
T3CODE_CUA_DRIVER_MODULE_URL_ENV,
T3CODE_CUA_DRIVER_PATH_ENV,
} from "./CuaDriverServerEnvironment.ts";

describe("cua-driver server environment", () => {
it("uses a configured driver path", () => {
expect(
Option.getOrUndefined(
resolveEmbeddedDriverPath({ T3CODE_CUA_DRIVER_PATH: "/Applications/T3 Code/cua-driver" }),
),
).toBe("/Applications/T3 Code/cua-driver");
});

it("uses the packaged resource when no override is configured", () => {
expect(
Option.getOrUndefined(
resolveEmbeddedDriverPath({}, "/Applications/T3 Code.app/Contents/Resources/cua-driver"),
),
).toBe("/Applications/T3 Code.app/Contents/Resources/cua-driver");
});

it("ignores the development override in packaged builds", () => {
expect(
Option.getOrUndefined(
resolveEmbeddedDriverPath(
{ T3CODE_CUA_DRIVER_PATH: "/tmp/untrusted-cua-driver" },
"/Applications/T3 Code.app/Contents/Resources/cua-driver",
),
),
).toBe("/Applications/T3 Code.app/Contents/Resources/cua-driver");
});

it("ignores missing and empty paths", () => {
expect(Option.isNone(resolveEmbeddedDriverPath({}))).toBe(true);
expect(Option.isNone(resolveEmbeddedDriverPath({ T3CODE_CUA_DRIVER_PATH: " " }))).toBe(true);
});

it.effect("reports missing configuration without a fabricated path", () => {
const previous = process.env[T3CODE_CUA_DRIVER_PATH_ENV];
delete process.env[T3CODE_CUA_DRIVER_PATH_ENV];
return Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
if (previous === undefined) delete process.env[T3CODE_CUA_DRIVER_PATH_ENV];
else process.env[T3CODE_CUA_DRIVER_PATH_ENV] = previous;
}),
);
const error = yield* configureCuaDriverServerEnvironment(
"com.t3tools.t3code",
"linux",
).pipe(Effect.flip);
expect(error).toBeInstanceOf(CuaDriverNotConfiguredError);
}),
).pipe(Effect.provide(NodeServices.layer));
});

it.effect("loads the packaged module from the asar archive", () =>
Effect.scoped(
Effect.gen(function* () {
const path = yield* Path.Path;
const resourcesPath = "/Applications/T3 Code.app/Contents/Resources";
yield* configureCuaDriverServerEnvironment("com.t3tools.t3code", "darwin", resourcesPath);

const moduleUrl = yield* path.toFileUrl(
path.join(resourcesPath, "app.asar/node_modules/@trycua/cua-driver/dist/embedded.js"),
);
expect(process.env[T3CODE_CUA_DRIVER_MODULE_URL_ENV]).toBe(moduleUrl.href);
}),
).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("uses packaged executables outside macOS", () =>
Effect.scoped(
Effect.gen(function* () {
const path = yield* Path.Path;
const resourcesPath = "/opt/T3 Code/resources";
for (const [platform, executable] of [
["linux", "cua-driver"],
["win32", "cua-driver.exe"],
] as const) {
yield* configureCuaDriverServerEnvironment("com.t3tools.t3code", platform, resourcesPath);
expect(process.env[T3CODE_CUA_DRIVER_PATH_ENV]).toBe(
path.join(resourcesPath, "cua-driver", executable),
);
expect(process.env[T3CODE_CUA_DRIVER_HOST_BUNDLE_ID_ENV]).toBe("com.t3tools.t3code");
expect(process.env[T3CODE_CUA_DRIVER_MODULE_URL_ENV]).toContain(
"app.asar/node_modules/@trycua/cua-driver/dist/embedded.js",
);
}
}),
).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("prevents inherited configuration from bypassing the desktop opt-in", () => {
const names = [
T3CODE_CUA_DRIVER_PATH_ENV,
T3CODE_CUA_DRIVER_HOST_BUNDLE_ID_ENV,
T3CODE_CUA_DRIVER_MODULE_URL_ENV,
] as const;
const previous = Object.fromEntries(names.map((name) => [name, process.env[name]] as const));
for (const name of names) process.env[name] = `inherited-${name}`;

return Effect.scoped(
Effect.gen(function* () {
yield* disableCuaDriverServerEnvironment();
for (const name of names) expect(process.env[name]).toBeUndefined();
}),
).pipe(
Effect.tap(() =>
Effect.sync(() => {
for (const name of names) expect(process.env[name]).toBe(`inherited-${name}`);
}),
),
Effect.ensuring(
Effect.sync(() => {
for (const name of names) {
const value = previous[name];
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
}),
),
);
});
});
114 changes: 114 additions & 0 deletions apps/desktop/src/cua/CuaDriverServerEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";

export const T3CODE_CUA_DRIVER_PATH_ENV = "T3CODE_CUA_DRIVER_PATH";
export const T3CODE_CUA_DRIVER_HOST_BUNDLE_ID_ENV = "T3CODE_CUA_DRIVER_HOST_BUNDLE_ID";
export const T3CODE_CUA_DRIVER_MODULE_URL_ENV = "T3CODE_CUA_DRIVER_MODULE_URL";

export class CuaDriverConfigurationError extends Schema.TaggedErrorClass<CuaDriverConfigurationError>()(
"CuaDriverConfigurationError",
{
modulePath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Could not configure embedded cua-driver module at '${this.modulePath}'.`;
}
}

export class CuaDriverNotConfiguredError extends Schema.TaggedErrorClass<CuaDriverNotConfiguredError>()(
"CuaDriverNotConfiguredError",
{},
) {
override get message(): string {
return "Cua Driver is not configured.";
}
}

export const resolveEmbeddedDriverPath = (
environment: NodeJS.ProcessEnv = process.env,
packagedDriverPath?: string,
): Option.Option<string> => {
if (packagedDriverPath !== undefined) return Option.some(packagedDriverPath);
return Option.fromNullishOr(environment[T3CODE_CUA_DRIVER_PATH_ENV]).pipe(
Option.map((value) => value.trim()),
Option.filter((value) => value.length > 0),
);
};

const cuaEnvironmentNames = [
T3CODE_CUA_DRIVER_PATH_ENV,
T3CODE_CUA_DRIVER_HOST_BUNDLE_ID_ENV,
T3CODE_CUA_DRIVER_MODULE_URL_ENV,
] as const;

const replaceCuaDriverServerEnvironment = Effect.fn("replaceCuaDriverServerEnvironment")(function* (
updates: Readonly<Record<string, string>>,
) {
const previous = Object.fromEntries(
cuaEnvironmentNames.map((name) => [name, process.env[name]] as const),
);
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
for (const [name, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
}),
);
for (const name of cuaEnvironmentNames) delete process.env[name];
Object.assign(process.env, updates);
});

/** Prevents inherited development variables from bypassing the desktop opt-in. */
export const disableCuaDriverServerEnvironment = Effect.fn("disableCuaDriverServerEnvironment")(
function* () {
yield* replaceCuaDriverServerEnvironment({});
},
);

/**
* Configures the local Node server to own cua-driver. The backend inherits
* these values when Electron starts it; remote servers can set the same
* variables in their own environment.
*/
export const configureCuaDriverServerEnvironment = Effect.fn("configureCuaDriverServerEnvironment")(
function* (hostBundleId: string, platform: NodeJS.Platform, resourcesPath?: string) {
const path = yield* Path.Path;
const packagedDriverPath =
resourcesPath === undefined
? undefined
: platform === "darwin"
? path.join(resourcesPath, "cua-driver")
: path.join(
resourcesPath,
"cua-driver",
platform === "win32" ? "cua-driver.exe" : "cua-driver",
);
const driverPath = resolveEmbeddedDriverPath(process.env, packagedDriverPath);
if (Option.isNone(driverPath)) {
return yield* new CuaDriverNotConfiguredError();
}

const updates: Record<string, string> = {
[T3CODE_CUA_DRIVER_PATH_ENV]: driverPath.value,
[T3CODE_CUA_DRIVER_HOST_BUNDLE_ID_ENV]: hostBundleId,
};
if (resourcesPath !== undefined) {
const modulePath = path.join(
resourcesPath,
"app.asar/node_modules/@trycua/cua-driver/dist/embedded.js",
);
const moduleUrl = yield* path.toFileUrl(modulePath).pipe(
Effect.map((url) => url.href),
Effect.mapError((cause) => new CuaDriverConfigurationError({ modulePath, cause })),
);
updates[T3CODE_CUA_DRIVER_MODULE_URL_ENV] = moduleUrl;
}

yield* replaceCuaDriverServerEnvironment(updates);
},
);
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const clientSettings: ClientSettings = {
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
environmentIdentificationMode: "artwork",
enableCua: false,
favorites: [],
fontFamilyCode: "",
fontFamilyComposer: "",
Expand Down
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@ff-labs/fff-node": "0.9.4",
"@opencode-ai/sdk": "^1.3.15",
"@pierre/diffs": "catalog:",
"@trycua/cua-driver": "0.12.2",
"effect": "catalog:",
"node-pty": "^1.1.0",
"yaml": "catalog:"
Expand Down
Loading
Loading