authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-10 13:09:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-10 13:09:39-07:00
loge3004ae806d4e9a81e03ff919f50d2d34c37ffe7
treee8ed9f8c631dadae4942d640067ea0b98c2077d8
parent9385314d26aba97d1fed1e05066e4562dbdf56bc
signature Signed by PGP key B5690EEEBB952194

Harden secret store and resolve catalog overrides (#1891)

Co-authored-by: codex <codex@users.noreply.github.com>

16 files changed, 106 insertions(+), 102 deletions(-)

apps/server/scripts/cli.ts+24-2
...@@ -14,6 +14,22 @@ import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog...@@ -14,6 +14,22 @@ import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog
14import rootPackageJson from "../../../package.json" with { type: "json" };14import rootPackageJson from "../../../package.json" with { type: "json" };
15import serverPackageJson from "../package.json" with { type: "json" };15import serverPackageJson from "../package.json" with { type: "json" };
1616
17interface PackageJson {
18 name: string;
19 repository: {
20 type: string;
21 url: string;
22 directory: string;
23 };
24 bin: Record<string, string>;
25 type: string;
26 version: string;
27 engines: Record<string, string>;
28 files: string[];
29 dependencies: Record<string, string>;
30 overrides: Record<string, string>;
31}
32
17class CliError extends Data.TaggedError("CliError")<{33class CliError extends Data.TaggedError("CliError")<{
18 readonly message: string;34 readonly message: string;
19 readonly cause?: unknown;35 readonly cause?: unknown;
...@@ -192,7 +208,7 @@ const publishCmd = Command.make(...@@ -192,7 +208,7 @@ const publishCmd = Command.make(
192 // Resolve catalog dependencies before any file mutations. If this throws,208 // Resolve catalog dependencies before any file mutations. If this throws,
193 // acquire fails and no release hook runs, so filesystem must still be untouched.209 // acquire fails and no release hook runs, so filesystem must still be untouched.
194 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);210 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
195 const pkg = {211 const pkg: PackageJson = {
196 name: serverPackageJson.name,212 name: serverPackageJson.name,
197 repository: serverPackageJson.repository,213 repository: serverPackageJson.repository,
198 bin: serverPackageJson.bin,214 bin: serverPackageJson.bin,
...@@ -200,7 +216,8 @@ const publishCmd = Command.make(...@@ -200,7 +216,8 @@ const publishCmd = Command.make(
200 version,216 version,
201 engines: serverPackageJson.engines,217 engines: serverPackageJson.engines,
202 files: serverPackageJson.files,218 files: serverPackageJson.files,
203 dependencies: serverPackageJson.dependencies as Record<string, unknown>,219 dependencies: serverPackageJson.dependencies,
220 overrides: rootPackageJson.overrides,
204 };221 };
205222
206 pkg.dependencies = resolveCatalogDependencies(223 pkg.dependencies = resolveCatalogDependencies(
...@@ -208,6 +225,11 @@ const publishCmd = Command.make(...@@ -208,6 +225,11 @@ const publishCmd = Command.make(
208 rootPackageJson.workspaces.catalog,225 rootPackageJson.workspaces.catalog,
209 "apps/server dependencies",226 "apps/server dependencies",
210 );227 );
228 pkg.overrides = resolveCatalogDependencies(
229 pkg.overrides,
230 rootPackageJson.workspaces.catalog,
231 "root overrides",
232 );
211233
212 const original = yield* fs.readFileString(packageJsonPath);234 const original = yield* fs.readFileString(packageJsonPath);
213 yield* fs.writeFileString(backupPath, original);235 yield* fs.writeFileString(backupPath, original);
apps/server/src/auth/Layers/ServerSecretStore.ts+6-9
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import * as Crypto from "node:crypto";1import * as Crypto from "node:crypto";
22
3import { Effect, FileSystem, Layer, Path } from "effect";3import { Effect, FileSystem, Layer, Path, Predicate } from "effect";
4import * as PlatformError from "effect/PlatformError";4import * as PlatformError from "effect/PlatformError";
55
6import { ServerConfig } from "../../config.ts";6import { ServerConfig } from "../../config.ts";
...@@ -28,17 +28,14 @@ export const makeServerSecretStore = Effect.gen(function* () {...@@ -28,17 +28,14 @@ export const makeServerSecretStore = Effect.gen(function* () {
2828
29 const resolveSecretPath = (name: string) => path.join(serverConfig.secretsDir, `${name}.bin`);29 const resolveSecretPath = (name: string) => path.join(serverConfig.secretsDir, `${name}.bin`);
3030
31 const isMissingSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>31 const isPlatformError = (u: unknown): u is PlatformError.PlatformError =>
32 cause instanceof PlatformError.PlatformError && cause.reason._tag === "NotFound";32 Predicate.isTagged(u, "PlatformError");
33
34 const isAlreadyExistsSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>
35 cause instanceof PlatformError.PlatformError && cause.reason._tag === "AlreadyExists";
3633
37 const get: ServerSecretStoreShape["get"] = (name) =>34 const get: ServerSecretStoreShape["get"] = (name) =>
38 fileSystem.readFile(resolveSecretPath(name)).pipe(35 fileSystem.readFile(resolveSecretPath(name)).pipe(
39 Effect.map((bytes) => Uint8Array.from(bytes)),36 Effect.map((bytes) => Uint8Array.from(bytes)),
40 Effect.catch((cause) =>37 Effect.catch((cause) =>
41 isMissingSecretFileError(cause)38 cause.reason._tag === "NotFound"
42 ? Effect.succeed(null)39 ? Effect.succeed(null)
43 : Effect.fail(40 : Effect.fail(
44 new SecretStoreError({41 new SecretStoreError({
...@@ -108,7 +105,7 @@ export const makeServerSecretStore = Effect.gen(function* () {...@@ -108,7 +105,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
108 return create(name, generated).pipe(105 return create(name, generated).pipe(
109 Effect.as(Uint8Array.from(generated)),106 Effect.as(Uint8Array.from(generated)),
110 Effect.catchTag("SecretStoreError", (error) =>107 Effect.catchTag("SecretStoreError", (error) =>
111 isAlreadyExistsSecretFileError(error.cause)108 isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists"
112 ? get(name).pipe(109 ? get(name).pipe(
113 Effect.flatMap((created) =>110 Effect.flatMap((created) =>
114 created !== null111 created !== null
...@@ -129,7 +126,7 @@ export const makeServerSecretStore = Effect.gen(function* () {...@@ -129,7 +126,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
129 const remove: ServerSecretStoreShape["remove"] = (name) =>126 const remove: ServerSecretStoreShape["remove"] = (name) =>
130 fileSystem.remove(resolveSecretPath(name)).pipe(127 fileSystem.remove(resolveSecretPath(name)).pipe(
131 Effect.catch((cause) =>128 Effect.catch((cause) =>
132 isMissingSecretFileError(cause)129 cause.reason._tag === "NotFound"
133 ? Effect.void130 ? Effect.void
134 : Effect.fail(131 : Effect.fail(
135 new SecretStoreError({132 new SecretStoreError({
apps/server/src/git/Layers/GitCore.ts+1-1
...@@ -2001,7 +2001,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {...@@ -2001,7 +2001,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
2001 "GitCore.removeWorktree",2001 "GitCore.removeWorktree",
2002 input.cwd,2002 input.cwd,
2003 args,2003 args,
2004 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error instanceof Error ? error.message : String(error)}`,2004 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error.message}`,
2005 error,2005 error,
2006 ),2006 ),
2007 ),2007 ),
apps/server/src/git/Layers/GitHubCli.ts+2-2
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Effect, Layer, Schema } from "effect";1import { Effect, Layer, Schema, SchemaIssue } from "effect";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
33
4import { runProcess } from "../../processRunner";4import { runProcess } from "../../processRunner";
...@@ -154,7 +154,7 @@ function decodeGitHubJson<S extends Schema.Top>(...@@ -154,7 +154,7 @@ function decodeGitHubJson<S extends Schema.Top>(
154 (error) =>154 (error) =>
155 new GitHubCliError({155 new GitHubCliError({
156 operation,156 operation,
157 detail: error instanceof Error ? `${invalidDetail}: ${error.message}` : invalidDetail,157 detail: `${invalidDetail}: ${SchemaIssue.makeFormatterDefault()(error.issue)}`,
158 cause: error,158 cause: error,
159 }),159 }),
160 ),160 ),
apps/server/src/provider/Layers/CodexAdapter.test.ts-7
...@@ -238,14 +238,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {...@@ -238,14 +238,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
238 .pipe(Effect.result);238 .pipe(Effect.result);
239239
240 assert.equal(result._tag, "Failure");240 assert.equal(result._tag, "Failure");
241 if (result._tag !== "Failure") {
242 return;
243 }
244
245 assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");241 assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
246 if (result.failure._tag !== "ProviderAdapterSessionNotFoundError") {
247 return;
248 }
249 assert.equal(result.failure.provider, "codex");242 assert.equal(result.failure.provider, "codex");
250 assert.equal(result.failure.threadId, "sess-missing");243 assert.equal(result.failure.threadId, "sess-missing");
251 assert.equal(result.failure.cause instanceof Error, true);244 assert.equal(result.failure.cause instanceof Error, true);
apps/server/src/provider/Layers/CodexAdapter.ts+4-11
...@@ -51,18 +51,11 @@ export interface CodexAdapterLiveOptions {...@@ -51,18 +51,11 @@ export interface CodexAdapterLiveOptions {
51 readonly nativeEventLogger?: EventNdjsonLogger;51 readonly nativeEventLogger?: EventNdjsonLogger;
52}52}
5353
54function toMessage(cause: unknown, fallback: string): string {
55 if (cause instanceof Error && cause.message.length > 0) {
56 return cause.message;
57 }
58 return fallback;
59}
60
61function toSessionError(54function toSessionError(
62 threadId: ThreadId,55 threadId: ThreadId,
63 cause: unknown,56 cause: unknown,
64): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {57): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
65 const normalized = toMessage(cause, "").toLowerCase();58 const normalized = cause instanceof Error ? cause.message.toLowerCase() : "";
66 if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) {59 if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) {
67 return new ProviderAdapterSessionNotFoundError({60 return new ProviderAdapterSessionNotFoundError({
68 provider: PROVIDER,61 provider: PROVIDER,
...@@ -88,7 +81,7 @@ function toRequestError(threadId: ThreadId, method: string, cause: unknown): Pro...@@ -88,7 +81,7 @@ function toRequestError(threadId: ThreadId, method: string, cause: unknown): Pro
88 return new ProviderAdapterRequestError({81 return new ProviderAdapterRequestError({
89 provider: PROVIDER,82 provider: PROVIDER,
90 method,83 method,
91 detail: toMessage(cause, `${method} failed`),84 detail: cause instanceof Error ? `${method} failed: ${cause.message}` : `${method} failed`,
92 cause,85 cause,
93 });86 });
94}87}
...@@ -1427,7 +1420,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -1427,7 +1420,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
1427 new ProviderAdapterProcessError({1420 new ProviderAdapterProcessError({
1428 provider: PROVIDER,1421 provider: PROVIDER,
1429 threadId: input.threadId,1422 threadId: input.threadId,
1430 detail: toMessage(cause, "Failed to start Codex adapter session."),1423 detail: `Failed to start Codex adapter session: ${cause instanceof Error ? cause.message : String(cause)}.`,
1431 cause,1424 cause,
1432 }),1425 }),
1433 });1426 });
...@@ -1455,7 +1448,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -1455,7 +1448,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
1455 new ProviderAdapterRequestError({1448 new ProviderAdapterRequestError({
1456 provider: PROVIDER,1449 provider: PROVIDER,
1457 method: "turn/start",1450 method: "turn/start",
1458 detail: toMessage(cause, "Failed to read attachment file."),1451 detail: `Failed to read attachment file: ${cause.message}.`,
1459 cause,1452 cause,
1460 }),1453 }),
1461 ),1454 ),
apps/server/src/provider/Layers/CodexProvider.ts+2-5
...@@ -389,7 +389,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu...@@ -389,7 +389,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
389 auth: { status: "unknown" },389 auth: { status: "unknown" },
390 message: isCommandMissingCause(error)390 message: isCommandMissingCause(error)
391 ? "Codex CLI (`codex`) is not installed or not on PATH."391 ? "Codex CLI (`codex`) is not installed or not on PATH."
392 : `Failed to execute Codex CLI health check: ${error instanceof Error ? error.message : String(error)}.`,392 : `Failed to execute Codex CLI health check: ${error.message}.`,
393 },393 },
394 });394 });
395 }395 }
...@@ -489,10 +489,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu...@@ -489,10 +489,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
489 version: parsedVersion,489 version: parsedVersion,
490 status: "warning",490 status: "warning",
491 auth: { status: "unknown" },491 auth: { status: "unknown" },
492 message:492 message: `Could not verify Codex authentication status: ${error.message}.`,
493 error instanceof Error
494 ? `Could not verify Codex authentication status: ${error.message}.`
495 : "Could not verify Codex authentication status.",
496 },493 },
497 });494 });
498 }495 }
apps/server/src/provider/providerSnapshot.ts+1-2
...@@ -32,8 +32,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined {...@@ -32,8 +32,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined {
32 return trimmed.length > 0 ? trimmed : undefined;32 return trimmed.length > 0 ? trimmed : undefined;
33}33}
3434
35export function isCommandMissingCause(error: unknown): boolean {35export function isCommandMissingCause(error: Error): boolean {
36 if (!(error instanceof Error)) return false;
37 const lower = error.message.toLowerCase();36 const lower = error.message.toLowerCase();
38 return lower.includes("enoent") || lower.includes("notfound");37 return lower.includes("enoent") || lower.includes("notfound");
39}38}
apps/server/src/terminal/Layers/Manager.ts+31-26
...@@ -8,7 +8,6 @@ import {...@@ -8,7 +8,6 @@ import {
8} from "@t3tools/contracts";8} from "@t3tools/contracts";
9import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";9import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";
10import {10import {
11 Data,
12 Effect,11 Effect,
13 Encoding,12 Encoding,
14 Equal,13 Equal,
...@@ -17,6 +16,7 @@ import {...@@ -17,6 +16,7 @@ import {
17 FileSystem,16 FileSystem,
18 Layer,17 Layer,
19 Option,18 Option,
19 Schema,
20 Scope,20 Scope,
21 Semaphore,21 Semaphore,
22 SynchronizedRef,22 SynchronizedRef,
...@@ -54,22 +54,28 @@ const DEFAULT_OPEN_COLS = 120;...@@ -54,22 +54,28 @@ const DEFAULT_OPEN_COLS = 120;
54const DEFAULT_OPEN_ROWS = 30;54const DEFAULT_OPEN_ROWS = 30;
55const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);55const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);
5656
57type TerminalSubprocessChecker = (57class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubprocessCheckError>()(
58 terminalPid: number,58 "TerminalSubprocessCheckError",
59) => Effect.Effect<boolean, TerminalSubprocessCheckError>;59 {
6060 message: Schema.String,
61class TerminalSubprocessCheckError extends Data.TaggedError("TerminalSubprocessCheckError")<{61 cause: Schema.optional(Schema.Defect),
62 readonly message: string;62 terminalPid: Schema.Number,
63 readonly cause?: unknown;63 command: Schema.Literals(["powershell", "pgrep", "ps"]),
64 readonly terminalPid: number;64 },
65 readonly command: "powershell" | "pgrep" | "ps";65) {}
66}> {}66
67class TerminalProcessSignalError extends Schema.TaggedErrorClass<TerminalProcessSignalError>()(
68 "TerminalProcessSignalError",
69 {
70 message: Schema.String,
71 cause: Schema.optional(Schema.Defect),
72 signal: Schema.Literals(["SIGTERM", "SIGKILL"]),
73 },
74) {}
6775
68class TerminalProcessSignalError extends Data.TaggedError("TerminalProcessSignalError")<{76interface TerminalSubprocessChecker {
69 readonly message: string;77 (terminalPid: number): Effect.Effect<boolean, TerminalSubprocessCheckError>;
70 readonly cause?: unknown;78}
71 readonly signal: "SIGTERM" | "SIGKILL";
72}> {}
7379
74interface ShellCandidate {80interface ShellCandidate {
75 shell: string;81 shell: string;
...@@ -271,9 +277,8 @@ function isRetryableShellSpawnError(error: PtySpawnError): boolean {...@@ -271,9 +277,8 @@ function isRetryableShellSpawnError(error: PtySpawnError): boolean {
271277
272 if (current instanceof Error) {278 if (current instanceof Error) {
273 messages.push(current.message);279 messages.push(current.message);
274 const cause = (current as { cause?: unknown }).cause;280 if (current.cause) {
275 if (cause) {281 queue.push(current.cause);
276 queue.push(cause);
277 }282 }
278 continue;283 continue;
279 }284 }
...@@ -876,7 +881,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -876,7 +881,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
876 Effect.logWarning("failed to persist terminal history", {881 Effect.logWarning("failed to persist terminal history", {
877 threadId,882 threadId,
878 terminalId,883 terminalId,
879 error: error instanceof Error ? error.message : String(error),884 error,
880 }),885 }),
881 ),886 ),
882 );887 );
...@@ -959,7 +964,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -959,7 +964,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
959 Effect.catch((cleanupError) =>964 Effect.catch((cleanupError) =>
960 Effect.logWarning("failed to remove legacy terminal history", {965 Effect.logWarning("failed to remove legacy terminal history", {
961 threadId,966 threadId,
962 error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),967 error: cleanupError,
963 }),968 }),
964 ),969 ),
965 );970 );
...@@ -975,7 +980,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -975,7 +980,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
975 Effect.logWarning("failed to delete terminal history", {980 Effect.logWarning("failed to delete terminal history", {
976 threadId,981 threadId,
977 terminalId,982 terminalId,
978 error: error instanceof Error ? error.message : String(error),983 error,
979 }),984 }),
980 ),985 ),
981 );986 );
...@@ -985,7 +990,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -985,7 +990,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
985 Effect.logWarning("failed to delete terminal history", {990 Effect.logWarning("failed to delete terminal history", {
986 threadId,991 threadId,
987 terminalId,992 terminalId,
988 error: error instanceof Error ? error.message : String(error),993 error,
989 }),994 }),
990 ),995 ),
991 );996 );
...@@ -1011,7 +1016,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1011,7 +1016,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1011 Effect.catch((error) =>1016 Effect.catch((error) =>
1012 Effect.logWarning("failed to delete terminal histories for thread", {1017 Effect.logWarning("failed to delete terminal histories for thread", {
1013 threadId,1018 threadId,
1014 error: error instanceof Error ? error.message : String(error),1019 error,
1015 }),1020 }),
1016 ),1021 ),
1017 ),1022 ),
...@@ -1463,12 +1468,12 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1463,12 +1468,12 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1463 const terminalPid = session.pid;1468 const terminalPid = session.pid;
1464 const hasRunningSubprocess = yield* subprocessChecker(terminalPid).pipe(1469 const hasRunningSubprocess = yield* subprocessChecker(terminalPid).pipe(
1465 Effect.map(Option.some),1470 Effect.map(Option.some),
1466 Effect.catch((error) =>1471 Effect.catch((reason) =>
1467 Effect.logWarning("failed to check terminal subprocess activity", {1472 Effect.logWarning("failed to check terminal subprocess activity", {
1468 threadId: session.threadId,1473 threadId: session.threadId,
1469 terminalId: session.terminalId,1474 terminalId: session.terminalId,
1470 terminalPid,1475 terminalPid,
1471 error: error instanceof Error ? error.message : String(error),1476 reason,
1472 }).pipe(Effect.as(Option.none<boolean>())),1477 }).pipe(Effect.as(Option.none<boolean>())),
1473 ),1478 ),
1474 );1479 );
apps/server/src/workspace/Layers/WorkspaceEntries.ts+1-4
...@@ -214,9 +214,6 @@ function directoryAncestorsOf(relativePath: string): string[] {...@@ -214,9 +214,6 @@ function directoryAncestorsOf(relativePath: string): string[] {
214 return directories;214 return directories;
215}215}
216216
217const processErrorDetail = (cause: unknown): string =>
218 cause instanceof Error ? cause.message : String(cause);
219
220export const makeWorkspaceEntries = Effect.gen(function* () {217export const makeWorkspaceEntries = Effect.gen(function* () {
221 const path = yield* Path.Path;218 const path = yield* Path.Path;
222 const gitOption = yield* Effect.serviceOption(GitCore);219 const gitOption = yield* Effect.serviceOption(GitCore);
...@@ -319,7 +316,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () {...@@ -319,7 +316,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
319 new WorkspaceEntriesError({316 new WorkspaceEntriesError({
320 cwd,317 cwd,
321 operation: "workspaceEntries.readDirectoryEntries",318 operation: "workspaceEntries.readDirectoryEntries",
322 detail: processErrorDetail(cause),319 detail: cause instanceof Error ? cause.message : String(cause),
323 cause,320 cause,
324 }),321 }),
325 }).pipe(322 }).pipe(
apps/server/src/ws.ts+1-4
...@@ -313,10 +313,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -313,10 +313,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
313 worktreePath: input.worktreePath,313 worktreePath: input.worktreePath,
314 scriptId: input.scriptId,314 scriptId: input.scriptId,
315 terminalId: input.terminalId,315 terminalId: input.terminalId,
316 detail:316 detail: error.message,
317 error instanceof Error
318 ? error.message
319 : "Unknown setup activity dispatch failure.",
320 },317 },
321 ),318 ),
322 ),319 ),
apps/web/src/environments/primary/auth.ts+17-19
...@@ -16,6 +16,14 @@ import {...@@ -16,6 +16,14 @@ import {
16} from "../../pairingUrl";16} from "../../pairingUrl";
1717
18import { resolvePrimaryEnvironmentHttpUrl } from "./target";18import { resolvePrimaryEnvironmentHttpUrl } from "./target";
19import { Data, Predicate } from "effect";
20
21export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{
22 readonly message: string;
23 readonly status: number;
24}> {}
25const isBootstrapHttpError = (u: unknown): u is BootstrapHttpError =>
26 Predicate.isTagged(u, "BootstrapHttpError");
1927
20export interface ServerPairingLinkRecord {28export interface ServerPairingLinkRecord {
21 readonly id: string;29 readonly id: string;
...@@ -87,10 +95,10 @@ export async function fetchSessionState(): Promise<AuthSessionState> {...@@ -87,10 +95,10 @@ export async function fetchSessionState(): Promise<AuthSessionState> {
87 credentials: "include",95 credentials: "include",
88 });96 });
89 if (!response.ok) {97 if (!response.ok) {
90 throw new BootstrapHttpError(98 throw new BootstrapHttpError({
91 `Failed to load server auth session state (${response.status}).`,99 message: `Failed to load server auth session state (${response.status}).`,
92 response.status,100 status: response.status,
93 );101 });
94 }102 }
95 return (await response.json()) as AuthSessionState;103 return (await response.json()) as AuthSessionState;
96 });104 });
...@@ -115,10 +123,10 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBoot...@@ -115,10 +123,10 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBoot
115123
116 if (!response.ok) {124 if (!response.ok) {
117 const message = await response.text();125 const message = await response.text();
118 throw new BootstrapHttpError(126 throw new BootstrapHttpError({
119 message || `Failed to bootstrap auth session (${response.status}).`,127 message: message || `Failed to bootstrap auth session (${response.status}).`,
120 response.status,128 status: response.status,
121 );129 });
122 }130 }
123131
124 return (await response.json()) as AuthBootstrapResult;132 return (await response.json()) as AuthBootstrapResult;
...@@ -146,16 +154,6 @@ const TRANSIENT_BOOTSTRAP_STATUS_CODES = new Set([502, 503, 504]);...@@ -146,16 +154,6 @@ const TRANSIENT_BOOTSTRAP_STATUS_CODES = new Set([502, 503, 504]);
146const BOOTSTRAP_RETRY_TIMEOUT_MS = 15_000;154const BOOTSTRAP_RETRY_TIMEOUT_MS = 15_000;
147const BOOTSTRAP_RETRY_STEP_MS = 500;155const BOOTSTRAP_RETRY_STEP_MS = 500;
148156
149export class BootstrapHttpError extends Error {
150 readonly status: number;
151
152 constructor(message: string, status: number) {
153 super(message);
154 this.name = "BootstrapHttpError";
155 this.status = status;
156 }
157}
158
159export async function retryTransientBootstrap<T>(operation: () => Promise<T>): Promise<T> {157export async function retryTransientBootstrap<T>(operation: () => Promise<T>): Promise<T> {
160 const startedAt = Date.now();158 const startedAt = Date.now();
161 while (true) {159 while (true) {
...@@ -182,7 +180,7 @@ function waitForBootstrapRetry(delayMs: number): Promise<void> {...@@ -182,7 +180,7 @@ function waitForBootstrapRetry(delayMs: number): Promise<void> {
182}180}
183181
184function isTransientBootstrapError(error: unknown): boolean {182function isTransientBootstrapError(error: unknown): boolean {
185 if (error instanceof BootstrapHttpError) {183 if (isBootstrapHttpError(error)) {
186 return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status);184 return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status);
187 }185 }
188186
apps/web/src/environments/primary/context.ts+4-4
...@@ -52,10 +52,10 @@ async function fetchPrimaryEnvironmentDescriptor(): Promise<ExecutionEnvironment...@@ -52,10 +52,10 @@ async function fetchPrimaryEnvironmentDescriptor(): Promise<ExecutionEnvironment
52 resolvePrimaryEnvironmentHttpUrl(SERVER_ENVIRONMENT_DESCRIPTOR_PATH),52 resolvePrimaryEnvironmentHttpUrl(SERVER_ENVIRONMENT_DESCRIPTOR_PATH),
53 );53 );
54 if (!response.ok) {54 if (!response.ok) {
55 throw new BootstrapHttpError(55 throw new BootstrapHttpError({
56 `Failed to load server environment descriptor (${response.status}).`,56 message: `Failed to load server environment descriptor (${response.status}).`,
57 response.status,57 status: response.status,
58 );58 });
59 }59 }
6060
61 const descriptor = (await response.json()) as ExecutionEnvironmentDescriptor;61 const descriptor = (await response.json()) as ExecutionEnvironmentDescriptor;
package.json+6
...@@ -62,7 +62,13 @@...@@ -62,7 +62,13 @@
62 "vitest": "catalog:"62 "vitest": "catalog:"
63 },63 },
64 "overrides": {64 "overrides": {
65 "@effect/atom-react": "catalog:",
66 "@effect/platform-bun": "catalog:",
67 "@effect/platform-node": "catalog:",
65 "@effect/platform-node-shared": "catalog:",68 "@effect/platform-node-shared": "catalog:",
69 "@effect/sql-sqlite-bun": "catalog:",
70 "@effect/vitest": "catalog:",
71 "effect": "catalog:",
66 "vite": "^8.0.0"72 "vite": "^8.0.0"
67 },73 },
68 "engines": {74 "engines": {
scripts/build-desktop-artifact.ts+3-3
...@@ -427,9 +427,9 @@ function validateBundledClientAssets(clientDir: string) {...@@ -427,9 +427,9 @@ function validateBundledClientAssets(clientDir: string) {
427}427}
428428
429function resolveDesktopRuntimeDependencies(429function resolveDesktopRuntimeDependencies(
430 dependencies: Record<string, unknown> | undefined,430 dependencies: Record<string, string> | undefined,
431 catalog: Record<string, unknown>,431 catalog: Record<string, string>,
432): Record<string, unknown> {432): Record<string, string> {
433 if (!dependencies || Object.keys(dependencies).length === 0) {433 if (!dependencies || Object.keys(dependencies).length === 0) {
434 return {};434 return {};
435 }435 }
scripts/lib/resolve-catalog.ts+3-3
...@@ -5,10 +5,10 @@...@@ -5,10 +5,10 @@
5 * the concrete version string found in `catalog`. Throws on missing entries.5 * the concrete version string found in `catalog`. Throws on missing entries.
6 */6 */
7export function resolveCatalogDependencies(7export function resolveCatalogDependencies(
8 dependencies: Record<string, unknown>,8 dependencies: Record<string, string>,
9 catalog: Record<string, unknown>,9 catalog: Record<string, string>,
10 label: string,10 label: string,
11): Record<string, unknown> {11): Record<string, string> {
12 return Object.fromEntries(12 return Object.fromEntries(
13 Object.entries(dependencies).map(([name, spec]) => {13 Object.entries(dependencies).map(([name, spec]) => {
14 if (typeof spec !== "string" || !spec.startsWith("catalog:")) {14 if (typeof spec !== "string" || !spec.startsWith("catalog:")) {