authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-10-09 23:08:57-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 23:08:57-05:00
log03bd839554c9da27aae37b027bcdb2fa9896af09
tree25da84173d3f061de3b8cdf2180438ecb244af18
parentaacefcd7e92a784c179b3ca2473a7608f783784e
signature Signed by PGP key B5690EEEBB952194

perf(desktop): reuse the prepared shell environment in the local backend (#17384)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

11 files changed, 304 insertions(+), 66 deletions(-)

apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+15
......@@ -325,6 +325,21 @@ describe("DesktopBackendConfiguration", () => {
325325 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
326326 );
327327
328 it.effect("only the local primary bootstrap claims the prepared shell environment", () =>
329 withHarness(
330 Effect.gen(function* () {
331 const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
332
333 const primary = yield* configuration.resolvePrimary;
334 const wsl = yield* configuration.resolveWsl({ port: 5000, distro: null });
335
336 assert.equal(primary.bootstrap.shellEnvironmentPrepared, true);
337 // A WSL server runs on its own Linux user environment and must hydrate it.
338 assert.isUndefined(wsl.bootstrap.shellEnvironmentPrepared);
339 }),
340 ),
341 );
342
328343 it.effect("resolveWsl reuses the primary's bootstrap token", () =>
329344 withHarness(
330345 Effect.gen(function* () {
apps/desktop/src/backend/DesktopBackendConfiguration.ts+4
......@@ -567,6 +567,10 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv
567567 host: backendExposure.bindHost,
568568 desktopBootstrapToken: input.bootstrapToken,
569569 desktopBootstrapSecret: input.bootstrapSecret,
570 // DesktopApp installs the shell environment into process.env before any
571 // backend starts, and this child extends it (extendEnv below). WSL gets
572 // a different environment, so its bootstrap never sets this.
573 shellEnvironmentPrepared: true,
570574 tailscaleServeEnabled: backendExposure.tailscaleServeEnabled,
571575 tailscaleServePort: backendExposure.tailscaleServePort,
572576 desktopTelemetryFd: 4,
apps/desktop/src/shell/DesktopShellEnvironment.test.ts+98-9
......@@ -9,10 +9,14 @@ import * as Sink from "effect/Sink";
99import * as Stream from "effect/Stream";
1010import * as ChildProcess from "effect/process/ChildProcess";
1111import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
12import * as NodeOS from "node:os";
13import { vi } from "vite-plus/test";
1214
1315import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
1416import * as DesktopShellEnvironment from "./DesktopShellEnvironment.ts";
1517
18vi.mock("node:os", { spy: true });
19
1620const textEncoder = new TextEncoder();
1721
1822const isDesktopShellEnvironmentCommandError = Schema.is(
......@@ -29,14 +33,14 @@ function envOutput(values: Readonly<Record<string, string>>): string {
2933 .join("\n");
3034}
3135
32function makeProcess(output: string): ChildProcessSpawner.ChildProcessHandle {
36function makeProcess(output: string, exitCode = 0): ChildProcessSpawner.ChildProcessHandle {
3337 const stdout = output.length === 0 ? Stream.empty : Stream.make(textEncoder.encode(output));
3438 return ChildProcessSpawner.makeHandle({
3539 pid: ChildProcessSpawner.ProcessId(123),
3640 stdout,
3741 stderr: Stream.empty,
3842 all: stdout,
39 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
43 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
4044 isRunning: Effect.succeed(false),
4145 kill: () => Effect.void,
4246 stdin: Sink.drain,
......@@ -67,7 +71,10 @@ function withProcessEnv<A, E, R>(
6771function runShellEnvironment(input: {
6872 readonly env: NodeJS.ProcessEnv;
6973 readonly platform: NodeJS.Platform;
70 readonly handler: (command: ChildProcess.Command) => string;
74 readonly userShell?: string;
75 readonly handler: (
76 command: ChildProcess.Command,
77 ) => string | { output: string; exitCode: number };
7178 readonly failure?: PlatformError.PlatformError;
7279}) {
7380 const layerEnvironment = Layer.succeed(
......@@ -78,11 +85,15 @@ function runShellEnvironment(input: {
7885 );
7986 const layerSpawner = Layer.succeed(
8087 ChildProcessSpawner.ChildProcessSpawner,
81 ChildProcessSpawner.make((command) =>
82 input.failure === undefined
83 ? Effect.succeed(makeProcess(input.handler(command)))
84 : Effect.fail(input.failure),
85 ),
88 ChildProcessSpawner.make((command) => {
89 if (input.failure !== undefined) return Effect.fail(input.failure);
90 const result = input.handler(command);
91 return Effect.succeed(
92 typeof result === "string"
93 ? makeProcess(result)
94 : makeProcess(result.output, result.exitCode),
95 );
96 }),
8697 );
8798
8899 const program = Effect.gen(function* () {
......@@ -96,10 +107,88 @@ function runShellEnvironment(input: {
96107 ),
97108 );
98109
99 return withProcessEnv(input.env, program);
110 return Effect.acquireUseRelease(
111 Effect.sync(() =>
112 vi.spyOn(NodeOS, "userInfo").mockReturnValue({
113 ...NodeOS.userInfo(),
114 shell: input.userShell ?? null,
115 }),
116 ),
117 () => withProcessEnv(input.env, program),
118 (spy) => Effect.sync(() => spy.mockRestore()),
119 );
100120}
101121
102122describe("DesktopShellEnvironment", () => {
123 it.effect("rejects PATH output from a failed shell and tries the next candidate", () =>
124 Effect.gen(function* () {
125 const env: NodeJS.ProcessEnv = { SHELL: "/bin/failing", PATH: "/usr/bin" };
126 const commands: string[] = [];
127
128 yield* runShellEnvironment({
129 env,
130 platform: "linux",
131 userShell: "/usr/local/bin/fish",
132 handler: (command) => {
133 if (command._tag !== "StandardCommand") return "";
134 commands.push(command.command);
135 return command.command === "/bin/failing"
136 ? { output: envOutput({ PATH: "/failed/bin" }), exitCode: 7 }
137 : envOutput({ PATH: "/home/test/.local/bin:/usr/bin" });
138 },
139 }).pipe(Effect.provide(Logger.layer([])));
140
141 assert.deepEqual(commands, ["/bin/failing", "/usr/local/bin/fish"]);
142 assert.equal(env.PATH, "/home/test/.local/bin:/usr/bin");
143 }),
144 );
145
146 it.effect.each(["darwin", "linux"] as const)(
147 "uses the account shell when SHELL is missing on %s",
148 (platform) =>
149 Effect.gen(function* () {
150 const env: NodeJS.ProcessEnv = { PATH: "/usr/bin" };
151 const commands: string[] = [];
152
153 yield* runShellEnvironment({
154 env,
155 platform,
156 userShell: "/usr/local/bin/fish",
157 handler: (command) => {
158 if (command._tag !== "StandardCommand") return "";
159 commands.push(command.command);
160 return envOutput({ PATH: "/home/test/.local/bin:/usr/bin" });
161 },
162 });
163
164 assert.deepEqual(commands, ["/usr/local/bin/fish"]);
165 assert.equal(env.PATH, "/home/test/.local/bin:/usr/bin");
166 }),
167 );
168
169 it.effect("tries the account shell before the platform fallback when SHELL has no PATH", () =>
170 Effect.gen(function* () {
171 const env: NodeJS.ProcessEnv = { SHELL: "/bin/missing", PATH: "/usr/bin" };
172 const commands: string[] = [];
173
174 yield* runShellEnvironment({
175 env,
176 platform: "linux",
177 userShell: "/usr/local/bin/fish",
178 handler: (command) => {
179 if (command._tag !== "StandardCommand") return "";
180 commands.push(command.command);
181 return command.command === "/usr/local/bin/fish"
182 ? envOutput({ PATH: "/home/test/.local/bin:/usr/bin" })
183 : "";
184 },
185 });
186
187 assert.deepEqual(commands, ["/bin/missing", "/usr/local/bin/fish"]);
188 assert.equal(env.PATH, "/home/test/.local/bin:/usr/bin");
189 }),
190 );
191
103192 it.effect("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on macOS", () =>
104193 Effect.gen(function* () {
105194 const env: NodeJS.ProcessEnv = {
apps/desktop/src/shell/DesktopShellEnvironment.ts+46-49
......@@ -1,3 +1,4 @@
1import { listLoginShellCandidates } from "@t3tools/shared/shell";
12import * as Context from "effect/Context";
23import * as Duration from "effect/Duration";
34import * as Effect from "effect/Effect";
......@@ -5,6 +6,7 @@ import * as FileSystem from "effect/FileSystem";
56import * as Layer from "effect/Layer";
67import * as Option from "effect/Option";
78import * as Schema from "effect/Schema";
9import * as Stream from "effect/Stream";
810import * as ChildProcess from "effect/process/ChildProcess";
911import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1012
......@@ -15,7 +17,6 @@ type EnvironmentPatch = Record<string, string>;
1517interface ShellEnvironmentConfig {
1618 readonly env: NodeJS.ProcessEnv;
1719 readonly platform: NodeJS.Platform;
18 readonly userShell: Option.Option<string>;
1920}
2021
2122interface WindowsProbeOptions {
......@@ -157,25 +158,6 @@ const mergePaths = (
157158 return entries.length > 0 ? Option.some(entries.join(delimiter)) : Option.none();
158159};
159160
160const listLoginShellCandidates = (config: ShellEnvironmentConfig): ReadonlyArray<string> => {
161 const fallback =
162 config.platform === "darwin" ? "/bin/zsh" : config.platform === "linux" ? "/bin/bash" : "";
163 const seen = new Set<string>();
164 const candidates: string[] = [];
165
166 for (const candidate of [
167 trimNonEmpty(config.env.SHELL),
168 config.userShell,
169 trimNonEmpty(fallback),
170 ]) {
171 if (Option.isNone(candidate) || seen.has(candidate.value)) continue;
172 seen.add(candidate.value);
173 candidates.push(candidate.value);
174 }
175
176 return candidates;
177};
178
179161const knownWindowsCliDirs = (env: NodeJS.ProcessEnv): ReadonlyArray<string> => [
180162 ...trimNonEmpty(env.APPDATA).pipe(
181163 Option.match({
......@@ -267,33 +249,49 @@ const runCommandOutput = Effect.fn("desktop.shellEnvironment.runCommandOutput")(
267249 readonly shell?: boolean;
268250}): Effect.fn.Return<string, never, ChildProcessSpawner.ChildProcessSpawner> {
269251 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
270 const output = yield* spawner
271 .string(
272 ChildProcess.make(input.command, input.args, {
273 shell: input.shell ?? false,
274 stdin: "ignore",
275 stdout: "pipe",
276 stderr: "pipe",
277 killSignal: "SIGTERM",
278 forceKillAfter: PROCESS_TERMINATE_GRACE,
279 }),
280 )
281 .pipe(
282 Effect.mapError(
283 (cause) =>
284 new DesktopShellEnvironmentCommandError({
285 probe: input.probe,
286 executable: executableName(input.command),
287 argumentCount: input.args.length,
288 cause,
289 }),
290 ),
291 Effect.catchTags({
292 DesktopShellEnvironmentCommandError: (error) =>
293 logShellEnvironmentCommandError(error).pipe(Effect.as("")),
294 }),
295 Effect.timeoutOption(input.timeout),
296 );
252 const output = yield* Effect.scoped(
253 Effect.gen(function* () {
254 const child = yield* spawner.spawn(
255 ChildProcess.make(input.command, input.args, {
256 shell: input.shell ?? false,
257 stdin: "ignore",
258 stdout: "pipe",
259 stderr: "pipe",
260 killSignal: "SIGTERM",
261 forceKillAfter: PROCESS_TERMINATE_GRACE,
262 }),
263 );
264 return yield* Effect.all([Stream.mkString(Stream.decodeText(child.stdout)), child.exitCode], {
265 concurrency: 2,
266 });
267 }),
268 ).pipe(
269 Effect.mapError(
270 (cause) =>
271 new DesktopShellEnvironmentCommandError({
272 probe: input.probe,
273 executable: executableName(input.command),
274 argumentCount: input.args.length,
275 cause,
276 }),
277 ),
278 Effect.filterOrFail(
279 ([, exitCode]) => exitCode === 0,
280 ([, exitCode]) =>
281 new DesktopShellEnvironmentCommandError({
282 probe: input.probe,
283 executable: executableName(input.command),
284 argumentCount: input.args.length,
285 cause: { exitCode },
286 }),
287 ),
288 Effect.map(([stdout]) => stdout),
289 Effect.catchTags({
290 DesktopShellEnvironmentCommandError: (error) =>
291 logShellEnvironmentCommandError(error).pipe(Effect.as("")),
292 }),
293 Effect.timeoutOption(input.timeout),
294 );
297295 if (Option.isSome(output)) {
298296 return output.value;
299297 }
......@@ -406,7 +404,7 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix
406404 const fileSystem = yield* FileSystem.FileSystem;
407405 const shellEnvironment: EnvironmentPatch = {};
408406
409 for (const shell of listLoginShellCandidates(config)) {
407 for (const shell of listLoginShellCandidates(config.platform, config.env.SHELL)) {
410408 Object.assign(
411409 shellEnvironment,
412410 yield* readLoginShellEnvironment(shell, LOGIN_SHELL_ENV_NAMES),
......@@ -521,7 +519,6 @@ export const make = Effect.gen(function* () {
521519 installShellEnvironment({
522520 env: process.env,
523521 platform: environment.platform,
524 userShell: Option.none(),
525522 }).pipe(
526523 Effect.provideService(FileSystem.FileSystem, fileSystem),
527524 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
apps/server/src/cli/config.test.ts+45
......@@ -26,6 +26,21 @@ import { resolveServerConfig } from "./config.ts";
2626const deriveExplicitServerPaths = (baseDir: string, devUrl: URL | undefined) =>
2727 deriveServerPaths(baseDir, devUrl, { baseDirIsExplicit: true });
2828
29const minimalDesktopFlags = (baseDir: string) => ({
30 mode: Option.some("desktop" as const),
31 port: Option.some(4888),
32 host: Option.none<string>(),
33 baseDir: Option.some(baseDir),
34 cwd: Option.none<string>(),
35 devUrl: Option.none<URL>(),
36 noBrowser: Option.none<boolean>(),
37 bootstrapFd: Option.none<number>(),
38 autoBootstrapProjectFromCwd: Option.none<boolean>(),
39 logWebSocketEvents: Option.none<boolean>(),
40 tailscaleServeEnabled: Option.none<boolean>(),
41 tailscaleServePort: Option.none<number>(),
42});
43
2944const encodeDesktopBootstrap = Schema.encodeEffect(Schema.fromJsonString(DesktopBackendBootstrap));
3045const encodeUnknownJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
3146
......@@ -515,6 +530,36 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
515530 }),
516531 );
517532
533 it.effect("carries the desktop's shell environment handoff only when the envelope sets it", () =>
534 Effect.gen(function* () {
535 const fs = yield* FileSystem.FileSystem;
536 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-config-shell-env-" });
537 const resolveWith = Effect.fn(function* (overrides: Partial<DesktopBackendBootstrapValue>) {
538 const fd = yield* openBootstrapFd(makeDesktopBootstrap(overrides));
539 return yield* resolveServerConfig(
540 {
541 ...minimalDesktopFlags(baseDir),
542 bootstrapFd: Option.some(fd),
543 },
544 Option.none(),
545 ).pipe(
546 Effect.provide(
547 Layer.mergeAll(
548 ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })),
549 NetService.layer,
550 ),
551 ),
552 );
553 });
554
555 assert.equal((yield* resolveWith({})).shellEnvironmentPrepared, undefined);
556 assert.equal(
557 (yield* resolveWith({ shellEnvironmentPrepared: true })).shellEnvironmentPrepared,
558 true,
559 );
560 }),
561 );
562
518563 it.effect("creates derived runtime directories during config resolution", () =>
519564 Effect.gen(function* () {
520565 const fs = yield* FileSystem.FileSystem;
apps/server/src/cli/config.ts+2
......@@ -360,6 +360,7 @@ export const resolveServerConfig = (
360360 );
361361 const desktopBootstrapToken = bootstrap?.desktopBootstrapToken;
362362 const desktopBootstrapSecret = bootstrap?.desktopBootstrapSecret;
363 const shellEnvironmentPrepared = bootstrap?.shellEnvironmentPrepared;
363364 const desktopTelemetryFd = bootstrap?.desktopTelemetryFd;
364365 const desktopTelemetryControlFd = bootstrap?.desktopTelemetryControlFd;
365366 const desktopBrowserFd = bootstrap?.desktopBrowserFd;
......@@ -468,6 +469,7 @@ export const resolveServerConfig = (
468469 startupPresentation,
469470 desktopBootstrapToken,
470471 ...(desktopBootstrapSecret === undefined ? {} : { desktopBootstrapSecret }),
472 ...(shellEnvironmentPrepared === undefined ? {} : { shellEnvironmentPrepared }),
471473 desktopTelemetryFd,
472474 desktopTelemetryControlFd,
473475 desktopBrowserFd,
apps/server/src/config.ts+1
......@@ -95,6 +95,7 @@ export class ServerConfig extends Context.Service<
9595 readonly startupPresentation: StartupPresentation;
9696 readonly desktopBootstrapToken: string | undefined;
9797 readonly desktopBootstrapSecret?: string | undefined;
98 readonly shellEnvironmentPrepared?: boolean | undefined;
9899 readonly desktopTelemetryFd?: number | undefined;
99100 readonly desktopTelemetryControlFd?: number | undefined;
100101 readonly desktopBrowserFd?: number | undefined;
apps/server/src/os-jank.test.ts+84-2
......@@ -1,7 +1,16 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it as effectIt } from "@effect/vitest";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import { CommandAvailability, WindowsShellEnvironment } from "@t3tools/shared/shell";
5import * as Effect from "effect/Effect";
6// @effect-diagnostics-next-line nodeBuiltinImport:off -- Observe the existing synchronous shell probe without launching the user's login shell.
7import * as NodeChildProcess from "node:child_process";
18import * as NodeOS from "node:os";
2import { assert, it } from "vite-plus/test";
9import { assert, it, vi } from "vite-plus/test";
310
4import { hydratePosixHome } from "./os-jank.ts";
11import { fixPath, hydratePosixHome } from "./os-jank.ts";
12
13vi.mock("node:child_process", { spy: true });
514
615it("hydrates HOME for minimal service environments from the user account", () => {
716 const env: NodeJS.ProcessEnv = {};
......@@ -38,3 +47,76 @@ it("preserves an explicitly configured HOME", () => {
3847
3948 assert.equal(env.HOME, "/custom/home");
4049});
50
51effectIt.effect.each(["darwin", "linux"] as const)(
52 "keeps the prepared PATH and still hydrates HOME on %s",
53 (platform) =>
54 Effect.acquireUseRelease(
55 Effect.sync(() => vi.spyOn(NodeChildProcess, "execFileSync")),
56 (probe) =>
57 Effect.gen(function* () {
58 probe.mockImplementation(() => {
59 throw new Error("The prepared environment must not launch another shell");
60 });
61 const env: NodeJS.ProcessEnv = { PATH: "/prepared/bin:/usr/bin" };
62
63 yield* fixPath({ shellEnvironmentPrepared: true }).pipe(
64 Effect.provideService(HostProcess.Platform, platform),
65 Effect.provideService(HostProcess.Environment, env),
66 Effect.provide(NodeServices.layer),
67 );
68
69 assert.equal(probe.mock.calls.length, 0);
70 assert.equal(env.PATH, "/prepared/bin:/usr/bin");
71 assert.equal(env.HOME, NodeOS.userInfo().homedir);
72 }),
73 (probe) => Effect.sync(() => probe.mockRestore()),
74 ),
75);
76
77effectIt.effect("hydrates PATH when the desktop handoff is absent", () =>
78 Effect.acquireUseRelease(
79 Effect.sync(() =>
80 vi
81 .spyOn(NodeChildProcess, "execFileSync")
82 .mockReturnValue("__T3CODE_ENV_PATH_START__\n/opt/tools/bin\n__T3CODE_ENV_PATH_END__\n"),
83 ),
84 (probe) =>
85 Effect.gen(function* () {
86 const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", PATH: "/usr/bin" };
87
88 yield* fixPath().pipe(
89 Effect.provideService(HostProcess.Platform, "linux"),
90 Effect.provideService(HostProcess.Environment, env),
91 Effect.provide(NodeServices.layer),
92 );
93
94 assert.equal(probe.mock.calls.length, 1);
95 assert.equal(env.PATH, "/opt/tools/bin:/usr/bin");
96 }),
97 (probe) => Effect.sync(() => probe.mockRestore()),
98 ),
99);
100
101effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () =>
102 Effect.gen(function* () {
103 const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows" };
104 const commands: string[] = [];
105
106 yield* fixPath({ shellEnvironmentPrepared: true }).pipe(
107 Effect.provideService(HostProcess.Platform, "win32"),
108 Effect.provideService(HostProcess.Environment, env),
109 Effect.provideService(WindowsShellEnvironment, () => ({ PATH: "C:\\Tools" })),
110 Effect.provideService(CommandAvailability, (command) =>
111 Effect.sync(() => {
112 commands.push(command);
113 return true;
114 }),
115 ),
116 Effect.provide(NodeServices.layer),
117 );
118
119 assert.deepEqual(commands, ["node"]);
120 assert.equal(env.PATH, "C:\\Tools;C:\\Windows");
121 }),
122);
apps/server/src/os-jank.ts+4-5
......@@ -48,11 +48,9 @@ export function hydratePosixHome(
4848 }
4949}
5050
51export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return<
52 void,
53 never,
54 FileSystem.FileSystem | Path.Path
55> {
51export const fixPath = Effect.fn("fixPath")(function* (options?: {
52 readonly shellEnvironmentPrepared?: boolean | undefined;
53}): Effect.fn.Return<void, never, FileSystem.FileSystem | Path.Path> {
5654 const platform = yield* HostProcess.Platform;
5755 const env = yield* HostProcess.Environment;
5856
......@@ -82,6 +80,7 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return<
8280 }),
8381 ),
8482 );
83 if (options?.shellEnvironmentPrepared === true) return;
8584 yield* Effect.sync(() => hydratePosixPath(env, platform)).pipe(
8685 Effect.catchDefect((defect) =>
8786 Effect.sync(() => {
apps/server/src/server.ts+1-1
......@@ -741,7 +741,7 @@ const layerMakeServer = Layer.unwrap(
741741 const routesReady = yield* Deferred.make<void>();
742742 const layerLauncher = ServiceLauncherClient.layer;
743743
744 yield* fixPath();
744 yield* fixPath({ shellEnvironmentPrepared: config.shellEnvironmentPrepared });
745745
746746 const layerHttpListening = Layer.effectDiscard(
747747 Effect.gen(function* () {
packages/contracts/src/desktopBootstrap.ts+4
......@@ -17,6 +17,10 @@ export const DesktopBackendBootstrap = Schema.Struct({
1717 // `desktopBootstrapToken` for its whole run. See
1818 // `@t3tools/shared/desktopBootstrapToken`.
1919 desktopBootstrapSecret: Schema.optionalKey(Schema.String),
20 // Set only when the desktop already merged the user's shell environment into
21 // the environment it hands this backend. Absent means the backend hydrates
22 // PATH itself, which WSL and older desktops rely on.
23 shellEnvironmentPrepared: Schema.optionalKey(Schema.Boolean),
2024 tailscaleServeEnabled: Schema.Boolean,
2125 tailscaleServePort: PortSchema,
2226 otlpTracesUrl: Schema.optional(Schema.String),