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
1414import rootPackageJson from "../../../package.json" with { type: "json" };
1515import 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
1733class CliError extends Data.TaggedError("CliError")<{
1834 readonly message: string;
1935 readonly cause?: unknown;
......@@ -192,7 +208,7 @@ const publishCmd = Command.make(
192208 // Resolve catalog dependencies before any file mutations. If this throws,
193209 // acquire fails and no release hook runs, so filesystem must still be untouched.
194210 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
195 const pkg = {
211 const pkg: PackageJson = {
196212 name: serverPackageJson.name,
197213 repository: serverPackageJson.repository,
198214 bin: serverPackageJson.bin,
......@@ -200,7 +216,8 @@ const publishCmd = Command.make(
200216 version,
201217 engines: serverPackageJson.engines,
202218 files: serverPackageJson.files,
203 dependencies: serverPackageJson.dependencies as Record<string, unknown>,
219 dependencies: serverPackageJson.dependencies,
220 overrides: rootPackageJson.overrides,
204221 };
205222
206223 pkg.dependencies = resolveCatalogDependencies(
......@@ -208,6 +225,11 @@ const publishCmd = Command.make(
208225 rootPackageJson.workspaces.catalog,
209226 "apps/server dependencies",
210227 );
228 pkg.overrides = resolveCatalogDependencies(
229 pkg.overrides,
230 rootPackageJson.workspaces.catalog,
231 "root overrides",
232 );
211233
212234 const original = yield* fs.readFileString(packageJsonPath);
213235 yield* fs.writeFileString(backupPath, original);
apps/server/src/auth/Layers/ServerSecretStore.ts+6-9
......@@ -1,6 +1,6 @@
11import * as Crypto from "node:crypto";
22
3import { Effect, FileSystem, Layer, Path } from "effect";
3import { Effect, FileSystem, Layer, Path, Predicate } from "effect";
44import * as PlatformError from "effect/PlatformError";
55
66import { ServerConfig } from "../../config.ts";
......@@ -28,17 +28,14 @@ export const makeServerSecretStore = Effect.gen(function* () {
2828
2929 const resolveSecretPath = (name: string) => path.join(serverConfig.secretsDir, `${name}.bin`);
3030
31 const isMissingSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>
32 cause instanceof PlatformError.PlatformError && cause.reason._tag === "NotFound";
33
34 const isAlreadyExistsSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>
35 cause instanceof PlatformError.PlatformError && cause.reason._tag === "AlreadyExists";
31 const isPlatformError = (u: unknown): u is PlatformError.PlatformError =>
32 Predicate.isTagged(u, "PlatformError");
3633
3734 const get: ServerSecretStoreShape["get"] = (name) =>
3835 fileSystem.readFile(resolveSecretPath(name)).pipe(
3936 Effect.map((bytes) => Uint8Array.from(bytes)),
4037 Effect.catch((cause) =>
41 isMissingSecretFileError(cause)
38 cause.reason._tag === "NotFound"
4239 ? Effect.succeed(null)
4340 : Effect.fail(
4441 new SecretStoreError({
......@@ -108,7 +105,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
108105 return create(name, generated).pipe(
109106 Effect.as(Uint8Array.from(generated)),
110107 Effect.catchTag("SecretStoreError", (error) =>
111 isAlreadyExistsSecretFileError(error.cause)
108 isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists"
112109 ? get(name).pipe(
113110 Effect.flatMap((created) =>
114111 created !== null
......@@ -129,7 +126,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
129126 const remove: ServerSecretStoreShape["remove"] = (name) =>
130127 fileSystem.remove(resolveSecretPath(name)).pipe(
131128 Effect.catch((cause) =>
132 isMissingSecretFileError(cause)
129 cause.reason._tag === "NotFound"
133130 ? Effect.void
134131 : Effect.fail(
135132 new SecretStoreError({
apps/server/src/git/Layers/GitCore.ts+1-1
......@@ -2001,7 +2001,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
20012001 "GitCore.removeWorktree",
20022002 input.cwd,
20032003 args,
2004 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error instanceof Error ? error.message : String(error)}`,
2004 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error.message}`,
20052005 error,
20062006 ),
20072007 ),
apps/server/src/git/Layers/GitHubCli.ts+2-2
......@@ -1,4 +1,4 @@
1import { Effect, Layer, Schema } from "effect";
1import { Effect, Layer, Schema, SchemaIssue } from "effect";
22import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
33
44import { runProcess } from "../../processRunner";
......@@ -154,7 +154,7 @@ function decodeGitHubJson<S extends Schema.Top>(
154154 (error) =>
155155 new GitHubCliError({
156156 operation,
157 detail: error instanceof Error ? `${invalidDetail}: ${error.message}` : invalidDetail,
157 detail: `${invalidDetail}: ${SchemaIssue.makeFormatterDefault()(error.issue)}`,
158158 cause: error,
159159 }),
160160 ),
apps/server/src/provider/Layers/CodexAdapter.test.ts-7
......@@ -238,14 +238,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
238238 .pipe(Effect.result);
239239
240240 assert.equal(result._tag, "Failure");
241 if (result._tag !== "Failure") {
242 return;
243 }
244
245241 assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
246 if (result.failure._tag !== "ProviderAdapterSessionNotFoundError") {
247 return;
248 }
249242 assert.equal(result.failure.provider, "codex");
250243 assert.equal(result.failure.threadId, "sess-missing");
251244 assert.equal(result.failure.cause instanceof Error, true);
apps/server/src/provider/Layers/CodexAdapter.ts+4-11
......@@ -51,18 +51,11 @@ export interface CodexAdapterLiveOptions {
5151 readonly nativeEventLogger?: EventNdjsonLogger;
5252}
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
6154function toSessionError(
6255 threadId: ThreadId,
6356 cause: unknown,
6457): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
65 const normalized = toMessage(cause, "").toLowerCase();
58 const normalized = cause instanceof Error ? cause.message.toLowerCase() : "";
6659 if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) {
6760 return new ProviderAdapterSessionNotFoundError({
6861 provider: PROVIDER,
......@@ -88,7 +81,7 @@ function toRequestError(threadId: ThreadId, method: string, cause: unknown): Pro
8881 return new ProviderAdapterRequestError({
8982 provider: PROVIDER,
9083 method,
91 detail: toMessage(cause, `${method} failed`),
84 detail: cause instanceof Error ? `${method} failed: ${cause.message}` : `${method} failed`,
9285 cause,
9386 });
9487}
......@@ -1427,7 +1420,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
14271420 new ProviderAdapterProcessError({
14281421 provider: PROVIDER,
14291422 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)}.`,
14311424 cause,
14321425 }),
14331426 });
......@@ -1455,7 +1448,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
14551448 new ProviderAdapterRequestError({
14561449 provider: PROVIDER,
14571450 method: "turn/start",
1458 detail: toMessage(cause, "Failed to read attachment file."),
1451 detail: `Failed to read attachment file: ${cause.message}.`,
14591452 cause,
14601453 }),
14611454 ),
apps/server/src/provider/Layers/CodexProvider.ts+2-5
......@@ -389,7 +389,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
389389 auth: { status: "unknown" },
390390 message: isCommandMissingCause(error)
391391 ? "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}.`,
393393 },
394394 });
395395 }
......@@ -489,10 +489,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
489489 version: parsedVersion,
490490 status: "warning",
491491 auth: { status: "unknown" },
492 message:
493 error instanceof Error
494 ? `Could not verify Codex authentication status: ${error.message}.`
495 : "Could not verify Codex authentication status.",
492 message: `Could not verify Codex authentication status: ${error.message}.`,
496493 },
497494 });
498495 }
apps/server/src/provider/providerSnapshot.ts+1-2
......@@ -32,8 +32,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined {
3232 return trimmed.length > 0 ? trimmed : undefined;
3333}
3434
35export function isCommandMissingCause(error: unknown): boolean {
36 if (!(error instanceof Error)) return false;
35export function isCommandMissingCause(error: Error): boolean {
3736 const lower = error.message.toLowerCase();
3837 return lower.includes("enoent") || lower.includes("notfound");
3938}
apps/server/src/terminal/Layers/Manager.ts+31-26
......@@ -8,7 +8,6 @@ import {
88} from "@t3tools/contracts";
99import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";
1010import {
11 Data,
1211 Effect,
1312 Encoding,
1413 Equal,
......@@ -17,6 +16,7 @@ import {
1716 FileSystem,
1817 Layer,
1918 Option,
19 Schema,
2020 Scope,
2121 Semaphore,
2222 SynchronizedRef,
......@@ -54,22 +54,28 @@ const DEFAULT_OPEN_COLS = 120;
5454const DEFAULT_OPEN_ROWS = 30;
5555const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);
5656
57type TerminalSubprocessChecker = (
58 terminalPid: number,
59) => Effect.Effect<boolean, TerminalSubprocessCheckError>;
60
61class TerminalSubprocessCheckError extends Data.TaggedError("TerminalSubprocessCheckError")<{
62 readonly message: string;
63 readonly cause?: unknown;
64 readonly terminalPid: number;
65 readonly command: "powershell" | "pgrep" | "ps";
66}> {}
57class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubprocessCheckError>()(
58 "TerminalSubprocessCheckError",
59 {
60 message: Schema.String,
61 cause: Schema.optional(Schema.Defect),
62 terminalPid: Schema.Number,
63 command: Schema.Literals(["powershell", "pgrep", "ps"]),
64 },
65) {}
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")<{
69 readonly message: string;
70 readonly cause?: unknown;
71 readonly signal: "SIGTERM" | "SIGKILL";
72}> {}
76interface TerminalSubprocessChecker {
77 (terminalPid: number): Effect.Effect<boolean, TerminalSubprocessCheckError>;
78}
7379
7480interface ShellCandidate {
7581 shell: string;
......@@ -271,9 +277,8 @@ function isRetryableShellSpawnError(error: PtySpawnError): boolean {
271277
272278 if (current instanceof Error) {
273279 messages.push(current.message);
274 const cause = (current as { cause?: unknown }).cause;
275 if (cause) {
276 queue.push(cause);
280 if (current.cause) {
281 queue.push(current.cause);
277282 }
278283 continue;
279284 }
......@@ -876,7 +881,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
876881 Effect.logWarning("failed to persist terminal history", {
877882 threadId,
878883 terminalId,
879 error: error instanceof Error ? error.message : String(error),
884 error,
880885 }),
881886 ),
882887 );
......@@ -959,7 +964,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
959964 Effect.catch((cleanupError) =>
960965 Effect.logWarning("failed to remove legacy terminal history", {
961966 threadId,
962 error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
967 error: cleanupError,
963968 }),
964969 ),
965970 );
......@@ -975,7 +980,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
975980 Effect.logWarning("failed to delete terminal history", {
976981 threadId,
977982 terminalId,
978 error: error instanceof Error ? error.message : String(error),
983 error,
979984 }),
980985 ),
981986 );
......@@ -985,7 +990,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
985990 Effect.logWarning("failed to delete terminal history", {
986991 threadId,
987992 terminalId,
988 error: error instanceof Error ? error.message : String(error),
993 error,
989994 }),
990995 ),
991996 );
......@@ -1011,7 +1016,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
10111016 Effect.catch((error) =>
10121017 Effect.logWarning("failed to delete terminal histories for thread", {
10131018 threadId,
1014 error: error instanceof Error ? error.message : String(error),
1019 error,
10151020 }),
10161021 ),
10171022 ),
......@@ -1463,12 +1468,12 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
14631468 const terminalPid = session.pid;
14641469 const hasRunningSubprocess = yield* subprocessChecker(terminalPid).pipe(
14651470 Effect.map(Option.some),
1466 Effect.catch((error) =>
1471 Effect.catch((reason) =>
14671472 Effect.logWarning("failed to check terminal subprocess activity", {
14681473 threadId: session.threadId,
14691474 terminalId: session.terminalId,
14701475 terminalPid,
1471 error: error instanceof Error ? error.message : String(error),
1476 reason,
14721477 }).pipe(Effect.as(Option.none<boolean>())),
14731478 ),
14741479 );
apps/server/src/workspace/Layers/WorkspaceEntries.ts+1-4
......@@ -214,9 +214,6 @@ function directoryAncestorsOf(relativePath: string): string[] {
214214 return directories;
215215}
216216
217const processErrorDetail = (cause: unknown): string =>
218 cause instanceof Error ? cause.message : String(cause);
219
220217export const makeWorkspaceEntries = Effect.gen(function* () {
221218 const path = yield* Path.Path;
222219 const gitOption = yield* Effect.serviceOption(GitCore);
......@@ -319,7 +316,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
319316 new WorkspaceEntriesError({
320317 cwd,
321318 operation: "workspaceEntries.readDirectoryEntries",
322 detail: processErrorDetail(cause),
319 detail: cause instanceof Error ? cause.message : String(cause),
323320 cause,
324321 }),
325322 }).pipe(
apps/server/src/ws.ts+1-4
......@@ -313,10 +313,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
313313 worktreePath: input.worktreePath,
314314 scriptId: input.scriptId,
315315 terminalId: input.terminalId,
316 detail:
317 error instanceof Error
318 ? error.message
319 : "Unknown setup activity dispatch failure.",
316 detail: error.message,
320317 },
321318 ),
322319 ),
apps/web/src/environments/primary/auth.ts+17-19
......@@ -16,6 +16,14 @@ import {
1616} from "../../pairingUrl";
1717
1818import { 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
2028export interface ServerPairingLinkRecord {
2129 readonly id: string;
......@@ -87,10 +95,10 @@ export async function fetchSessionState(): Promise<AuthSessionState> {
8795 credentials: "include",
8896 });
8997 if (!response.ok) {
90 throw new BootstrapHttpError(
91 `Failed to load server auth session state (${response.status}).`,
92 response.status,
93 );
98 throw new BootstrapHttpError({
99 message: `Failed to load server auth session state (${response.status}).`,
100 status: response.status,
101 });
94102 }
95103 return (await response.json()) as AuthSessionState;
96104 });
......@@ -115,10 +123,10 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBoot
115123
116124 if (!response.ok) {
117125 const message = await response.text();
118 throw new BootstrapHttpError(
119 message || `Failed to bootstrap auth session (${response.status}).`,
120 response.status,
121 );
126 throw new BootstrapHttpError({
127 message: message || `Failed to bootstrap auth session (${response.status}).`,
128 status: response.status,
129 });
122130 }
123131
124132 return (await response.json()) as AuthBootstrapResult;
......@@ -146,16 +154,6 @@ const TRANSIENT_BOOTSTRAP_STATUS_CODES = new Set([502, 503, 504]);
146154const BOOTSTRAP_RETRY_TIMEOUT_MS = 15_000;
147155const 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
159157export async function retryTransientBootstrap<T>(operation: () => Promise<T>): Promise<T> {
160158 const startedAt = Date.now();
161159 while (true) {
......@@ -182,7 +180,7 @@ function waitForBootstrapRetry(delayMs: number): Promise<void> {
182180}
183181
184182function isTransientBootstrapError(error: unknown): boolean {
185 if (error instanceof BootstrapHttpError) {
183 if (isBootstrapHttpError(error)) {
186184 return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status);
187185 }
188186
apps/web/src/environments/primary/context.ts+4-4
......@@ -52,10 +52,10 @@ async function fetchPrimaryEnvironmentDescriptor(): Promise<ExecutionEnvironment
5252 resolvePrimaryEnvironmentHttpUrl(SERVER_ENVIRONMENT_DESCRIPTOR_PATH),
5353 );
5454 if (!response.ok) {
55 throw new BootstrapHttpError(
56 `Failed to load server environment descriptor (${response.status}).`,
57 response.status,
58 );
55 throw new BootstrapHttpError({
56 message: `Failed to load server environment descriptor (${response.status}).`,
57 status: response.status,
58 });
5959 }
6060
6161 const descriptor = (await response.json()) as ExecutionEnvironmentDescriptor;
package.json+6
......@@ -62,7 +62,13 @@
6262 "vitest": "catalog:"
6363 },
6464 "overrides": {
65 "@effect/atom-react": "catalog:",
66 "@effect/platform-bun": "catalog:",
67 "@effect/platform-node": "catalog:",
6568 "@effect/platform-node-shared": "catalog:",
69 "@effect/sql-sqlite-bun": "catalog:",
70 "@effect/vitest": "catalog:",
71 "effect": "catalog:",
6672 "vite": "^8.0.0"
6773 },
6874 "engines": {
scripts/build-desktop-artifact.ts+3-3
......@@ -427,9 +427,9 @@ function validateBundledClientAssets(clientDir: string) {
427427}
428428
429429function resolveDesktopRuntimeDependencies(
430 dependencies: Record<string, unknown> | undefined,
431 catalog: Record<string, unknown>,
432): Record<string, unknown> {
430 dependencies: Record<string, string> | undefined,
431 catalog: Record<string, string>,
432): Record<string, string> {
433433 if (!dependencies || Object.keys(dependencies).length === 0) {
434434 return {};
435435 }
scripts/lib/resolve-catalog.ts+3-3
......@@ -5,10 +5,10 @@
55 * the concrete version string found in `catalog`. Throws on missing entries.
66 */
77export function resolveCatalogDependencies(
8 dependencies: Record<string, unknown>,
9 catalog: Record<string, unknown>,
8 dependencies: Record<string, string>,
9 catalog: Record<string, string>,
1010 label: string,
11): Record<string, unknown> {
11): Record<string, string> {
1212 return Object.fromEntries(
1313 Object.entries(dependencies).map(([name, spec]) => {
1414 if (typeof spec !== "string" || !spec.startsWith("catalog:")) {