Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c60d2b1
Share redacted DPoP request targets
juliusmarminge Jun 20, 2026
5270126
Share safe URL diagnostics
juliusmarminge Jun 20, 2026
025fd6f
[codex] Redact agent awareness relay diagnostics (#3410)
juliusmarminge Jun 20, 2026
6715d16
Structure advertised endpoint errors (#3289)
juliusmarminge Jun 20, 2026
c861bb8
[codex] add structured Electron shell errors (#3288)
juliusmarminge Jun 20, 2026
1fc31fa
[codex] Structure browser target resolution errors (#3327)
juliusmarminge Jun 20, 2026
1ea4c82
[codex] Structure web local API failures (#3338)
juliusmarminge Jun 20, 2026
cc21e54
[codex] Preserve mobile composer input failures (#3343)
juliusmarminge Jun 20, 2026
77a8eb2
[codex] Structure mobile file-processing failures (#3287)
juliusmarminge Jun 20, 2026
a14abc6
[codex] Structure browser DPoP failures (#3277)
juliusmarminge Jun 20, 2026
f9cf047
[codex] Structure OTLP export diagnostics (#3407)
juliusmarminge Jun 20, 2026
0dd6350
[codex] Structure update manifest failures (#3293)
juliusmarminge Jun 20, 2026
eb8c3f5
[codex] Structure ACP schema generator errors (#3362)
juliusmarminge Jun 20, 2026
15bacdb
[codex] Structure Codex schema generator errors (#3354)
juliusmarminge Jun 20, 2026
252622a
[codex] Structure relay environment connector errors (#3331)
juliusmarminge Jun 20, 2026
65ed5d9
[codex] Structure web cloud-link failures (#3316)
juliusmarminge Jun 20, 2026
7219177
[codex] Preserve client connection error causes (#3242)
juliusmarminge Jun 20, 2026
96e4579
[codex] Structure connection persistence failures (#3255)
juliusmarminge Jun 20, 2026
57cf316
fix: preserve catalog persistence stages
juliusmarminge Jun 20, 2026
babf6f4
[codex] Structure client RPC unavailable errors (#3260)
juliusmarminge Jun 20, 2026
d407d90
[codex] Preserve client connection preparation causes (#3267)
juliusmarminge Jun 20, 2026
249a5ad
[codex] Structure mobile DPoP failures (#3307)
juliusmarminge Jun 20, 2026
71c5fdc
[codex] Preserve supervisor unexpected causes (#3438)
juliusmarminge Jun 20, 2026
973d3c1
[codex] Report preview action failures (#3351)
juliusmarminge Jun 20, 2026
ff76f76
[codex] Structure analytics batch delivery failures (#3311)
juliusmarminge Jun 20, 2026
3b10968
Avoid sensitive persistence paths
juliusmarminge Jun 20, 2026
2efb859
[codex] improve server auth error context (#3240)
juliusmarminge Jun 21, 2026
e4a440b
fix: preserve structured SSH cancellation errors
juliusmarminge Jun 21, 2026
9e0c536
test: assert SSH cancellation cause chain
juliusmarminge Jun 21, 2026
a7d56b3
[codex] Structure mobile cloud-link failures (#3320)
juliusmarminge Jun 21, 2026
8fd276f
fix: keep connection error messages structural
juliusmarminge Jun 21, 2026
761be94
fix(desktop): preserve external-open IPC result
juliusmarminge Jun 21, 2026
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
43 changes: 39 additions & 4 deletions apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,49 @@ describe("ElectronShell", () => {
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("returns false when Electron rejects openExternal", () =>
it.effect("preserves safe URL context and cause when Electron rejects openExternal", () =>
Effect.gen(function* () {
openExternalMock.mockRejectedValue(new Error("open failed"));
const cause = new Error("open failed");
openExternalMock.mockRejectedValue(cause);
const externalUrl =
"HTTPS://user:password@example.com:443/signed-secret-token/path?access_token=secret#fragment";

const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal("https://example.com/path");
const error = yield* Effect.flip(electronShell.openExternal(externalUrl));

assert.equal(result, false);
assert.instanceOf(error, ElectronShell.ElectronShellOpenExternalError);
assert.isTrue(ElectronShell.isElectronShellError(error));
assert.strictEqual(error.urlHostname, "example.com");
assert.strictEqual(error.urlLength, externalUrl.length);
assert.strictEqual(error.urlProtocol, "https:");
assert.strictEqual(error.cause, cause);
assert.notProperty(error, "externalUrl");
assert.notProperty(error, "requestTarget");
assert.notMatch(
error.message,
/user|password|signed-secret-token|path|access_token|secret|fragment/,
);
assert.notInclude(error.message, cause.message);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("preserves non-sensitive clipboard context and cause", () =>
Effect.gen(function* () {
const cause = new Error("clipboard failed");
writeTextMock.mockImplementation(() => {
throw cause;
});

const electronShell = yield* ElectronShell.ElectronShell;
const error = yield* Effect.flip(electronShell.copyText("secret text"));

assert.instanceOf(error, ElectronShell.ElectronShellCopyTextError);
assert.isTrue(ElectronShell.isElectronShellError(error));
assert.strictEqual(error.textLength, 11);
assert.strictEqual(error.cause, cause);
assert.include(error.message, "11 characters");
assert.notInclude(error.message, "secret text");
assert.notInclude(error.message, cause.message);
}).pipe(Effect.provide(ElectronShell.layer)),
);
});
78 changes: 65 additions & 13 deletions apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import { getUrlDiagnostics } from "@t3tools/shared/urlDiagnostics";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

const SAFE_EXTERNAL_PROTOCOLS = new Set(["http:", "https:"]);

export class ElectronShellOpenExternalError extends Schema.TaggedErrorClass<ElectronShellOpenExternalError>()(
"ElectronShellOpenExternalError",
{
urlHostname: Schema.String,
urlLength: Schema.Number,
urlProtocol: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to open external URL for ${this.urlHostname} (${this.urlProtocol}, input length ${this.urlLength}).`;
}
}

export class ElectronShellCopyTextError extends Schema.TaggedErrorClass<ElectronShellCopyTextError>()(
"ElectronShellCopyTextError",
{
textLength: Schema.Number,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to copy ${this.textLength} characters to the clipboard.`;
}
}

export const ElectronShellError = Schema.Union([
ElectronShellOpenExternalError,
ElectronShellCopyTextError,
]);
export type ElectronShellError = typeof ElectronShellError.Type;
export const isElectronShellError = Schema.is(ElectronShellError);

export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
if (typeof rawUrl !== "string") {
return Option.none();
Expand All @@ -20,29 +55,46 @@ export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
}
}

function describeExternalUrl(externalUrl: string, inputLength: number) {
const diagnostics = getUrlDiagnostics(externalUrl);
return {
urlHostname: diagnostics.hostname ?? "",
urlLength: inputLength,
urlProtocol: diagnostics.protocol ?? "",
};
}

export class ElectronShell extends Context.Service<
ElectronShell,
{
readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>;
readonly copyText: (text: string) => Effect.Effect<void>;
readonly openExternal: (
rawUrl: unknown,
) => Effect.Effect<boolean, ElectronShellOpenExternalError>;
readonly copyText: (text: string) => Effect.Effect<void, ElectronShellCopyTextError>;
}
>()("@t3tools/desktop/electron/ElectronShell") {}

export const make = ElectronShell.of({
openExternal: (rawUrl) =>
Option.match(parseSafeExternalUrl(rawUrl), {
openExternal: (rawUrl) => {
const inputLength = typeof rawUrl === "string" ? rawUrl.length : 0;

return Option.match(parseSafeExternalUrl(rawUrl), {
onNone: () => Effect.succeed(false),
onSome: (externalUrl) =>
Effect.promise(() =>
Electron.shell.openExternal(externalUrl).then(
() => true,
() => false,
),
),
}),
Effect.tryPromise({
try: () => Electron.shell.openExternal(externalUrl),
catch: (cause) =>
new ElectronShellOpenExternalError({
...describeExternalUrl(externalUrl, inputLength),
cause,
}),
}).pipe(Effect.as(true)),
Comment thread
cursor[bot] marked this conversation as resolved.
});
},
copyText: (text) =>
Effect.sync(() => {
Electron.clipboard.writeText(text);
Effect.try({
try: () => Electron.clipboard.writeText(text),
catch: (cause) => new ElectronShellCopyTextError({ textLength: text.length, cause }),
}),
});

Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-st
export const SET_SERVER_EXPOSURE_MODE_CHANNEL = "desktop:set-server-exposure-mode";
export const SET_TAILSCALE_SERVE_ENABLED_CHANNEL = "desktop:set-tailscale-serve-enabled";
export const GET_ADVERTISED_ENDPOINTS_CHANNEL = "desktop:get-advertised-endpoints";
export const SSH_PASSWORD_PROMPT_CANCELLED_RESULT = "ssh-password-prompt-cancelled";
export const PREVIEW_CREATE_TAB_CHANNEL = "desktop:preview-create-tab";
export const PREVIEW_CLOSE_TAB_CHANNEL = "desktop:preview-close-tab";
export const PREVIEW_REGISTER_WEBVIEW_CHANNEL = "desktop:preview-register-webview";
Expand Down
52 changes: 51 additions & 1 deletion apps/desktop/src/ipc/methods/sshEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { assert, describe, it } from "@effect/vitest";
import { SshHttpBridgeError } from "@t3tools/ssh/errors";
import {
DesktopSshEnvironmentEnsureResultSchema,
DesktopSshPasswordPromptCancellationError,
} from "@t3tools/contracts";
import { SshHttpBridgeError, SshPasswordPromptError } from "@t3tools/ssh/errors";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";

import {
DesktopSshEnvironmentRequestError,
ensureSshEnvironment,
fetchSshEnvironmentDescriptor,
} from "./sshEnvironment.ts";
import * as DesktopSshEnvironment from "../../ssh/DesktopSshEnvironment.ts";
import * as DesktopSshPasswordPrompts from "../../ssh/DesktopSshPasswordPrompts.ts";

const decodeDesktopSshEnvironmentEnsureResult = Schema.decodeUnknownEffect(
DesktopSshEnvironmentEnsureResultSchema,
);

function jsonResponse(request: HttpClientRequest.HttpClientRequest, body: unknown, status = 200) {
return HttpClientResponse.fromWeb(
Expand All @@ -34,6 +46,44 @@ function makeHttpClientLayer(
}

describe("SSH environment IPC", () => {
it.effect("encodes password prompt cancellations with structured context and their cause", () => {
const promptCause = new DesktopSshPasswordPrompts.DesktopSshPromptWindowClosedError({
requestId: "prompt-1",
destination: "developer@devbox.example.test",
});
const cause = new SshPasswordPromptError({
message: promptCause.message,
cause: promptCause,
});
const layer = Layer.succeed(
DesktopSshEnvironment.DesktopSshEnvironment,
DesktopSshEnvironment.DesktopSshEnvironment.of({
discoverHosts: () => Effect.die("unexpected host discovery"),
ensureEnvironment: () => Effect.fail(cause),
disconnectEnvironment: () => Effect.die("unexpected disconnect"),
}),
);

return Effect.gen(function* () {
const encoded = yield* ensureSshEnvironment.handler({
target: {
alias: "devbox",
hostname: "devbox.example.test",
username: "developer",
port: 22,
},
});
const error = yield* decodeDesktopSshEnvironmentEnsureResult(encoded);

assert.instanceOf(error, DesktopSshPasswordPromptCancellationError);
assert.equal(error.reason, "window-closed");
assert.equal(error.requestId, "prompt-1");
assert.equal(error.destination, "developer@devbox.example.test");
assert.instanceOf(error.cause, Error);
assert.instanceOf(error.cause.cause, Error);
}).pipe(Effect.provide(layer));
});

it.effect("fetches and decodes the remote environment descriptor", () => {
const requestUrls: string[] = [];
const layer = makeHttpClientLayer((request) =>
Expand Down
37 changes: 23 additions & 14 deletions apps/desktop/src/ipc/methods/sshEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
bootstrapRemoteBearerSession,
fetchRemoteSessionState,
issueRemoteWebSocketTicket,
RemoteEnvironmentAuthUndeclaredStatusError,
isRemoteEnvironmentAuthUndeclaredStatusError,
type RemoteEnvironmentAuthError,
} from "@t3tools/client-runtime/authorization";
import { fetchRemoteEnvironmentDescriptor } from "@t3tools/client-runtime/environment";
Expand All @@ -15,7 +15,7 @@ import {
DesktopSshEnvironmentEnsureResultSchema,
DesktopSshEnvironmentTargetSchema,
DesktopSshHttpBaseUrlInputSchema,
DesktopSshPasswordPromptCancelledType,
DesktopSshPasswordPromptCancellationError,
DesktopSshPasswordPromptResolutionInputSchema,
ExecutionEnvironmentDescriptor,
EnvironmentInternalError,
Expand Down Expand Up @@ -45,17 +45,21 @@ type DesktopSshEnvironmentRequestOperation =

type DesktopSshEnvironmentRequestCause = RemoteEnvironmentAuthError | SshHttpBridgeError;

const desktopSshPasswordPromptCancellationReasons = {
DesktopSshPromptCancelledError: "user-cancelled",
DesktopSshPromptWindowClosedError: "window-closed",
DesktopSshPromptServiceStoppedError: "service-stopped",
DesktopSshPromptTimedOutError: "timed-out",
} as const;

const isEnvironmentAuthInvalidError = Schema.is(EnvironmentAuthInvalidError);
const isEnvironmentInternalError = Schema.is(EnvironmentInternalError);
const isEnvironmentOperationForbiddenError = Schema.is(EnvironmentOperationForbiddenError);
const isEnvironmentRequestInvalidError = Schema.is(EnvironmentRequestInvalidError);
const isEnvironmentScopeRequiredError = Schema.is(EnvironmentScopeRequiredError);

function readSshHttpStatus(cause: DesktopSshEnvironmentRequestCause): number | null {
if (
cause instanceof RemoteEnvironmentAuthUndeclaredStatusError ||
cause instanceof SshHttpBridgeError
) {
if (isRemoteEnvironmentAuthUndeclaredStatusError(cause) || cause instanceof SshHttpBridgeError) {
return cause.status ?? null;
}
if (isEnvironmentRequestInvalidError(cause)) {
Expand Down Expand Up @@ -127,14 +131,19 @@ export const ensureSshEnvironment = DesktopIpc.makeIpcMethod({
}) {
const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;
return yield* sshEnvironment.ensureEnvironment(target, options).pipe(
Effect.catch((error) =>
DesktopSshEnvironment.isDesktopSshPasswordPromptCancellation(error)
? Effect.succeed({
type: DesktopSshPasswordPromptCancelledType,
message: error.message,
})
: Effect.fail(error),
),
Effect.catchTags({
SshPasswordPromptError: (error) =>
DesktopSshEnvironment.isDesktopSshPasswordPromptCancellation(error)
? Effect.succeed(
new DesktopSshPasswordPromptCancellationError({
reason: desktopSshPasswordPromptCancellationReasons[error.cause._tag],
requestId: error.cause.requestId,
destination: error.cause.destination,
cause: error,
}),
)
: Effect.fail(error),
}),
);
}),
});
Expand Down
34 changes: 34 additions & 0 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";

import { openExternal } from "./window.ts";
import * as ElectronShell from "../../electron/ElectronShell.ts";

describe("window IPC", () => {
it.effect("returns false when Electron rejects an external URL", () => {
const url = "https://example.com/path";
const error = new ElectronShell.ElectronShellOpenExternalError({
urlHostname: "example.com",
urlLength: url.length,
urlProtocol: "https:",
cause: new Error("open failed"),
});
const layer = Layer.mergeAll(
Layer.succeed(
ElectronShell.ElectronShell,
ElectronShell.ElectronShell.of({
openExternal: () => Effect.fail(error),
copyText: () => Effect.void,
}),
),
Logger.layer([], { mergeWithExisting: false }),
);

return Effect.gen(function* () {
const opened = yield* openExternal.handler(url);
assert.equal(opened, false);
}).pipe(Effect.provide(layer));
});
});
16 changes: 15 additions & 1 deletion apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Redacted from "effect/Redacted";
import * as Schema from "effect/Schema";

import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
Expand Down Expand Up @@ -141,6 +142,19 @@ export const openExternal = DesktopIpc.makeIpcMethod({
result: Schema.Boolean,
handler: Effect.fn("desktop.ipc.window.openExternal")(function* (url) {
const shell = yield* ElectronShell.ElectronShell;
return yield* shell.openExternal(url);
return yield* shell.openExternal(url).pipe(
Effect.catchTag("ElectronShellOpenExternalError", (error) =>
Effect.logWarning(error.message).pipe(
Effect.annotateLogs({
error: Redacted.make(error, { label: error._tag }),
"error.type": error._tag,
"desktop.external_url.hostname": error.urlHostname,
"desktop.external_url.input_length": error.urlLength,
"desktop.external_url.protocol": error.urlProtocol,
}),
Effect.as(false),
),
),
);
}),
});
Loading
Loading