Skip to content
Closed
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
114 changes: 114 additions & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,116 @@ describe("DesktopBackendConfiguration", () => {
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("resolveWsl forwards the standard OTEL variables into the distro", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-config-test-",
});

const previousWslEnv = process.env.WSLENV;
const previousEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
const previousHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS;
const previousTemporality = process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE;
try {
// The bootstrap carries the resolved URLs but nothing else these
// variables say, and it is the lowest-priority source. Without the
// names crossing too, a Windows machine would reach the collector
// inside the distro unauthenticated, in the wrong wire format, and
// with an aggregation the receiver drops.
delete process.env.WSLENV;
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://collector.example.com";
process.env.OTEL_EXPORTER_OTLP_HEADERS = "authorization=Bearer%20ambient";
process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "delta";

yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolveWsl({ port: 5050, distro: null });

assert.equal(config.env.OTEL_EXPORTER_OTLP_ENDPOINT, "https://collector.example.com");
const declared = (config.env.WSLENV ?? "").split(":");
assert.include(declared, "OTEL_EXPORTER_OTLP_ENDPOINT");
assert.include(declared, "OTEL_EXPORTER_OTLP_HEADERS");
assert.include(declared, "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE");
// A bare entry crosses verbatim. A path flag would rewrite a URL.
assert.notInclude(config.env.WSLENV ?? "", "OTEL_EXPORTER_OTLP_ENDPOINT/");
}).pipe(
Effect.provide(
DesktopBackendConfiguration.layer.pipe(
Layer.provideMerge(serverExposureLayer),
Layer.provideMerge(DesktopAppSettings.layerTest()),
Layer.provideMerge(DesktopWslServerTree.layerTest()),
Layer.provideMerge(
DesktopWslEnvironment.layerTest({
isAvailable: true,
windowsToWslPath: () => Option.some("/mnt/c/repo/apps/server/src/index.ts"),
getDistroIp: () => Option.some("172.27.0.99"),
}),
),
Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),
),
),
);
} finally {
restoreEnv("WSLENV", previousWslEnv);
restoreEnv("OTEL_EXPORTER_OTLP_ENDPOINT", previousEndpoint);
restoreEnv("OTEL_EXPORTER_OTLP_HEADERS", previousHeaders);
restoreEnv("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", previousTemporality);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("resolveWsl forwards T3 Code's own endpoint under its own name", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-config-test-",
});

const previousWslEnv = process.env.WSLENV;
const previousTracesUrl = process.env.T3CODE_OTLP_TRACES_URL;
const previousEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
try {
// The bootstrap carries this URL too, but it cannot say which variable
// put it there, and the bootstrap is the lowest-priority source. Only
// the name crossing keeps T3 Code's own variable outranking an ambient
// endpoint inside the distro the way it does everywhere else.
delete process.env.WSLENV;
process.env.T3CODE_OTLP_TRACES_URL = "http://localhost:4318/v1/traces";
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://collector.example.com";

yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolveWsl({ port: 5050, distro: null });

assert.equal(config.env.T3CODE_OTLP_TRACES_URL, "http://localhost:4318/v1/traces");
assert.include((config.env.WSLENV ?? "").split(":"), "T3CODE_OTLP_TRACES_URL");
assert.notInclude(config.env.WSLENV ?? "", "T3CODE_OTLP_TRACES_URL/");
}).pipe(
Effect.provide(
DesktopBackendConfiguration.layer.pipe(
Layer.provideMerge(serverExposureLayer),
Layer.provideMerge(DesktopAppSettings.layerTest()),
Layer.provideMerge(DesktopWslServerTree.layerTest()),
Layer.provideMerge(
DesktopWslEnvironment.layerTest({
isAvailable: true,
windowsToWslPath: () => Option.some("/mnt/c/repo/apps/server/src/index.ts"),
getDistroIp: () => Option.some("172.27.0.99"),
}),
),
Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),
),
),
);
} finally {
restoreEnv("WSLENV", previousWslEnv);
restoreEnv("T3CODE_OTLP_TRACES_URL", previousTracesUrl);
restoreEnv("OTEL_EXPORTER_OTLP_ENDPOINT", previousEndpoint);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("resolveWsl preserves existing WSLENV entries when forwarding backend secrets", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand All @@ -1001,7 +1111,10 @@ describe("DesktopBackendConfiguration", () => {
const previousAnthropicKey = process.env.ANTHROPIC_API_KEY;
const previousOtlpHeaders = process.env.T3CODE_OTLP_HEADERS;
const previousOtlpProtocol = process.env.T3CODE_OTLP_PROTOCOL;
// A developer's own OTEL_* variables would be forwarded too.
const ambientOtel = Object.entries(process.env).filter(([name]) => name.startsWith("OTEL_"));
try {
for (const [name] of ambientOtel) delete process.env[name];
process.env.WSLENV = "GOPATH/p:OPENAI_API_KEY/u:EMPTY::AZURE_DEVOPS_EXT_PAT/u";
process.env.OPENAI_API_KEY = "openai-key";
process.env.ANTHROPIC_API_KEY = "anthropic-key";
Expand Down Expand Up @@ -1058,6 +1171,7 @@ describe("DesktopBackendConfiguration", () => {
restoreEnv("ANTHROPIC_API_KEY", previousAnthropicKey);
restoreEnv("T3CODE_OTLP_HEADERS", previousOtlpHeaders);
restoreEnv("T3CODE_OTLP_PROTOCOL", previousOtlpProtocol);
for (const [name, value] of ambientOtel) restoreEnv(name, value);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand Down
69 changes: 52 additions & 17 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,55 @@ const DESKTOP_BACKEND_ENV_NAMES = [
"T3CODE_TAILSCALE_SERVE_PORT",
] as const;

// Every name the server reads to decide what it exports and where. Forwarded
// under their own names, not folded into the bootstrap envelope, so precedence
// inside a WSL distro is the same as on every other platform and a collector's
// headers and wire format travel with its endpoint. Declared in WSLENV without
// a flag, which is what makes URL-shaped values safe: only a `/p`, `/l`, `/u`,
// or `/w` entry is path-translated.
const OBSERVABILITY_FORWARDED_ENV_NAMES = [
"T3CODE_OTEL_SDK_DISABLED",
"T3CODE_OTLP_TRACES_URL",
"T3CODE_OTLP_METRICS_URL",
"T3CODE_OTLP_LOGS_URL",
"T3CODE_OTLP_HEADERS",
"T3CODE_OTLP_PROTOCOL",
"T3CODE_OTLP_EXPORT_INTERVAL_MS",
"T3CODE_OTLP_SERVICE_NAME",
"OTEL_SDK_DISABLED",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
"OTEL_EXPORTER_OTLP_METRICS_HEADERS",
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL",
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL",
"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE",
"OTEL_TRACES_EXPORTER",
"OTEL_METRICS_EXPORTER",
"OTEL_LOGS_EXPORTER",
"OTEL_BSP_SCHEDULE_DELAY",
"OTEL_BSP_MAX_EXPORT_BATCH_SIZE",
"OTEL_BLRP_SCHEDULE_DELAY",
"OTEL_BLRP_MAX_EXPORT_BATCH_SIZE",
"OTEL_METRIC_EXPORT_INTERVAL",
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
"OTEL_RESOURCE_ATTRIBUTES",
] as const;

// Env vars that the WSL backend needs but Windows process.env won't forward
// across the wsl.exe boundary without WSLENV. The dev-server URL is handled
// separately via a `--dev-url` CLI flag because WSLENV translation of
// URL-shaped values (colons / slashes) is unreliable.
// separately via a `--dev-url` CLI flag.
const WSL_FORWARDED_ENV_NAMES = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
// Otherwise the WSL server keeps exporting to endpoints from the bootstrap.
"T3CODE_OTEL_SDK_DISABLED",
"OTEL_SDK_DISABLED",
"T3CODE_OTLP_HEADERS",
"T3CODE_OTLP_PROTOCOL",
...OBSERVABILITY_FORWARDED_ENV_NAMES,
] as const;

const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
Expand Down Expand Up @@ -221,12 +258,12 @@ const readPersistedBackendObservabilitySettings = Effect.gen(function* () {
};
});

// The bootstrap is the only channel that carries an OTLP endpoint to every
// backend. A Windows-native child inherits the desktop process's env, but a
// WSL child gets nothing across wsl.exe that WSLENV does not declare, and
// WSLENV translation of URL-shaped values is unreliable, so the endpoints are
// deliberately not forwarded that way. Env beats the persisted settings file,
// matching the precedence resolveServerConfig and DesktopObservability apply.
// The bootstrap envelope is the channel that carries an endpoint to every
// backend whatever its platform, and it is the lowest-priority source the
// server consults. Env beats the persisted settings file here, matching the
// precedence resolveServerConfig and DesktopObservability apply. The variables
// themselves reach a WSL backend under their own names through WSLENV, which is
// what keeps that precedence identical inside the distro.
const readBackendObservabilitySettings = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const persisted = yield* readPersistedBackendObservabilitySettings;
Expand Down Expand Up @@ -733,10 +770,8 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
};

// Forward the dev-server URL as an explicit CLI flag so the WSL backend's
// config resolution lands in dev/ instead of userdata/. Inheriting through
// WSLENV is unreliable in practice (URL-shaped values with colons /
// slashes get translated unpredictably depending on flags), and the
// packaged build leaves devServerUrl as None anyway.
// config resolution lands in dev/ instead of userdata/. The packaged build
// leaves devServerUrl as None anyway.
const devUrlArgs = Option.match(environment.devServerUrl, {
onNone: () => [] as ReadonlyArray<string>,
onSome: (url) => ["--dev-url", url.href],
Expand Down
Loading
Loading