diff --git a/.mise.toml b/.mise.toml deleted file mode 100644 index 2014a9bf2956..000000000000 --- a/.mise.toml +++ /dev/null @@ -1,2 +0,0 @@ -[tools] -node = "24.13.1" diff --git a/README.md b/README.md index a7a6cf402f3f..33fa5838ca64 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,26 @@ # T3 Code -T3 Code is a minimal web GUI for coding agents (currently Codex, Claude, and OpenCode, more coming soon). +T3 Code is a minimal web GUI for coding agents (currently Codex, Claude, Cursor, and OpenCode, more coming soon). ## Installation > [!WARNING] -> T3 Code currently supports Codex, Claude, and OpenCode. +> T3 Code currently supports Codex, Claude, Cursor, and OpenCode. > Install and authenticate at least one provider before use: > > - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login` > - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login` +> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `cursor-agent login` > - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login` ### Run without installing ```bash -npx t3 +npx t3@latest ``` +Tip: Use `npx t3@latest --help` for the full CLI reference. + ### Desktop app Install the latest version of the desktop app from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), or from your favorite package registry: @@ -46,11 +49,7 @@ We are very very early in this project. Expect bugs. We are not accepting contributions yet. -Observability guide: [docs/operations/observability.md](./docs/operations/observability.md) - -Relay observability: [docs/operations/relay-observability.md](./docs/operations/relay-observability.md) - -T3 Cloud Clerk setup: [docs/cloud/t3-cloud-clerk.md](./docs/cloud/t3-cloud-clerk.md) +There's no public docs site yet, checkout the miscellaneous markdown files in [docs](./docs). ## Documentation @@ -62,12 +61,28 @@ T3 Cloud Clerk setup: [docs/cloud/t3-cloud-clerk.md](./docs/cloud/t3-cloud-clerk ## If you REALLY want to contribute still.... read this first -Before local development, prepare the environment and install dependencies: +### Install `vp` + +T3 Code uses Vite+ so you'll need to install the global `vp` command-line tool. + +#### macOS / Linux + +```bash +curl -fsSL https://vite.plus | bash +``` + +#### Windows + +```bash +irm https://vite.plus/ps1 | iex +``` + +Checkout their getting started guide for more information: https://viteplus.dev/guide/ + +### Install dependencies ```bash -# Optional: only needed if you use mise for dev tool management. -mise install -vp install +vp i ``` T3 Cloud is optional and disabled in a fresh clone. To enable it for web, desktop, and mobile source diff --git a/apps/desktop/src/app/DesktopAppIdentity.ts b/apps/desktop/src/app/DesktopAppIdentity.ts index 7a566194ab1a..52f4b12808e7 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.ts @@ -52,7 +52,7 @@ const make = Effect.gen(function* () { Effect.map((parsed) => Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)), ), - Effect.catch(() => Effect.succeed(Option.none())), + Effect.orElseSucceed(() => Option.none()), ), }); }); diff --git a/apps/desktop/src/backend/DesktopServerExposure.test.ts b/apps/desktop/src/backend/DesktopServerExposure.test.ts index 0f3e9eaeb45a..e5fbb84c8adf 100644 --- a/apps/desktop/src/backend/DesktopServerExposure.test.ts +++ b/apps/desktop/src/backend/DesktopServerExposure.test.ts @@ -64,6 +64,13 @@ function mockSpawnerLayer(statusJson = "{}") { ); } +function dieOnSpawnLayer() { + return Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.die("unexpected tailscale spawn")), + ); +} + function makeEnvironmentLayer(baseDir: string, env: Record = {}) { return makeDesktopEnvironmentLayer({ dirname: "/repo/apps/desktop/src", @@ -86,6 +93,7 @@ function makeLayer(input: { readonly baseDir: string; readonly networkInterfaces?: DesktopNetworkInterfaces; readonly env?: Record; + readonly spawnerLayer?: Layer.Layer; }) { const env = { T3CODE_HOME: input.baseDir, ...input.env }; const environmentLayer = makeEnvironmentLayer(input.baseDir, env); @@ -97,7 +105,7 @@ function makeLayer(input: { Layer.provideMerge(DesktopAppSettings.layer), Layer.provideMerge(NodeFileSystem.layer), Layer.provideMerge(NodeHttpClient.layerUndici), - Layer.provideMerge(mockSpawnerLayer()), + Layer.provideMerge(input.spawnerLayer ?? mockSpawnerLayer()), Layer.provideMerge(networkLayer), Layer.provideMerge(DesktopConfig.layerTest(env)), Layer.provideMerge(environmentLayer), @@ -116,13 +124,23 @@ const withHarness = ( | DesktopAppSettings.DesktopAppSettings >, env: Record = {}, + spawnerLayer?: Layer.Layer, ) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-desktop-server-exposure-test-", }); - return yield* effect.pipe(Effect.provide(makeLayer({ baseDir, networkInterfaces, env }))); + return yield* effect.pipe( + Effect.provide( + makeLayer({ + baseDir, + networkInterfaces, + env, + ...(spawnerLayer ? { spawnerLayer } : {}), + }), + ), + ); }).pipe(Effect.provide(NodeServices.layer), Effect.scoped); describe("DesktopServerExposure", () => { @@ -239,6 +257,27 @@ describe("DesktopServerExposure", () => { ), ); + it.effect("does not spawn the tailscale CLI while server exposure is local-only", () => + withHarness( + lanNetworkInterfaces, + Effect.gen(function* () { + const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; + yield* serverExposure.configureFromSettings({ port: 4173 }); + // mode stays at default "local-only", tailscaleServeEnabled stays false. + + const endpoints = yield* serverExposure.getAdvertisedEndpoints; + // Only the loopback endpoint; no tailscale spawn means the dieOnSpawnLayer + // would have crashed the test if the gate was missing. + assert.deepEqual( + endpoints.map((endpoint) => endpoint.httpBaseUrl), + ["http://127.0.0.1:4173/"], + ); + }), + {}, + dieOnSpawnLayer(), + ), + ); + it.effect("uses ConfigProvider desktop exposure overrides", () => withHarness( lanNetworkInterfaces, diff --git a/apps/desktop/src/backend/DesktopServerExposure.ts b/apps/desktop/src/backend/DesktopServerExposure.ts index 839c76e65dab..8b62323499e1 100644 --- a/apps/desktop/src/backend/DesktopServerExposure.ts +++ b/apps/desktop/src/backend/DesktopServerExposure.ts @@ -12,6 +12,7 @@ import type { } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Data from "effect/Data"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; @@ -22,8 +23,11 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { DEFAULT_DESKTOP_SETTINGS, type DesktopSettings } from "../settings/DesktopAppSettings.ts"; import * as DesktopConfig from "../app/DesktopConfig.ts"; import { resolveTailscaleAdvertisedEndpoints } from "./tailscaleEndpointProvider.ts"; +import { readTailscaleStatus } from "@t3tools/tailscale"; import * as DesktopAppSettingsService from "../settings/DesktopAppSettings.ts"; +const TAILSCALE_STATUS_CACHE_TTL = Duration.seconds(60); + export const DESKTOP_LOOPBACK_HOST = "127.0.0.1"; const DESKTOP_LAN_BIND_HOST = "0.0.0.0"; @@ -412,6 +416,18 @@ const make = Effect.gen(function* () { const desktopSettings = yield* DesktopAppSettingsService.DesktopAppSettings; const stateRef = yield* Ref.make(initialRuntimeState()); + // Cache the `tailscale status` spawn for the TTL. On macOS, the Mac App + // Store Tailscale CLI lives inside Tailscale's sandbox container, so each + // spawn re-triggers the "Other apps" TCC prompt. + const cachedReadMagicDnsName = yield* Effect.cachedWithTTL( + readTailscaleStatus.pipe( + Effect.map((status) => status.magicDnsName), + Effect.orElseSucceed(() => null), + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner), + ), + TAILSCALE_STATUS_CACHE_TTL, + ); + const readNetworkInterfaces = networkInterfaces.read; const getState = Ref.get(stateRef).pipe(Effect.map(toContractState)); @@ -516,11 +532,20 @@ const make = Effect.gen(function* () { exposure: toResolvedExposure(state), customHttpsEndpointUrls: config.desktopHttpsEndpointUrls, }); + + // Don't spawn the Tailscale CLI when the user hasn't opted into any + // network exposure. The spawn itself triggers a macOS "Other apps" + // TCC prompt on Mac App Store Tailscale builds. + if (state.mode !== "network-accessible" && !state.tailscaleServeEnabled) { + return coreEndpoints; + } + const tailscaleEndpoints = yield* resolveTailscaleAdvertisedEndpoints({ port: state.port, serveEnabled: state.tailscaleServeEnabled, servePort: state.tailscaleServePort, networkInterfaces: currentNetworkInterfaces, + readMagicDnsName: cachedReadMagicDnsName, }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner), Effect.provideService(HttpClient.HttpClient, httpClient), diff --git a/apps/desktop/src/backend/tailscaleEndpointProvider.test.ts b/apps/desktop/src/backend/tailscaleEndpointProvider.test.ts index 612ef3bd73fa..28bf211f09aa 100644 --- a/apps/desktop/src/backend/tailscaleEndpointProvider.test.ts +++ b/apps/desktop/src/backend/tailscaleEndpointProvider.test.ts @@ -104,6 +104,25 @@ describe("tailscale endpoint provider", () => { }).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)), ); + it.effect("uses an injected magic DNS name reader instead of spawning tailscale", () => + Effect.gen(function* () { + let readerCalls = 0; + const endpoints = yield* resolveTailscaleAdvertisedEndpoints({ + port: 3773, + networkInterfaces: {}, + readMagicDnsName: Effect.sync(() => { + readerCalls += 1; + return "desktop.tail.ts.net"; + }), + }); + assert.equal(readerCalls, 1); + assert.deepEqual( + endpoints.map((endpoint) => endpoint.httpBaseUrl), + ["https://desktop.tail.ts.net/"], + ); + }).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)), + ); + it.effect( "marks the Tailscale HTTPS endpoint available after Serve is enabled and reachable", () => diff --git a/apps/desktop/src/backend/tailscaleEndpointProvider.ts b/apps/desktop/src/backend/tailscaleEndpointProvider.ts index bd46e9f03f54..cffe15724109 100644 --- a/apps/desktop/src/backend/tailscaleEndpointProvider.ts +++ b/apps/desktop/src/backend/tailscaleEndpointProvider.ts @@ -105,6 +105,11 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd readonly servePort?: number; readonly networkInterfaces: DesktopNetworkInterfaces; readonly statusJson?: string | null; + readonly readMagicDnsName?: Effect.Effect< + string | null, + never, + ChildProcessSpawner.ChildProcessSpawner + >; readonly probe?: (baseUrl: string) => Effect.Effect; }): Effect.fn.Return< readonly AdvertisedEndpoint[], @@ -112,15 +117,18 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd ChildProcessSpawner.ChildProcessSpawner | HttpClient.HttpClient > { const ipEndpoints = resolveTailscaleIpAdvertisedEndpoints(input); + const readDnsName = + input.readMagicDnsName ?? + readTailscaleStatus.pipe( + Effect.map((status) => status.magicDnsName), + Effect.catch(() => Effect.succeed(null)), + ); const dnsName = input.statusJson === undefined - ? yield* readTailscaleStatus.pipe( - Effect.map((status) => status.magicDnsName), - Effect.catch(() => Effect.succeed(null)), - ) + ? yield* readDnsName : input.statusJson ? yield* parseTailscaleMagicDnsName(input.statusJson).pipe( - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ) : null; const magicDnsEndpoint = yield* resolveTailscaleMagicDnsAdvertisedEndpoint({ diff --git a/apps/desktop/src/ipc/methods/cloudAuth.test.ts b/apps/desktop/src/ipc/methods/cloudAuth.test.ts index c0e4d8b2618c..c5f1e2b2c90b 100644 --- a/apps/desktop/src/ipc/methods/cloudAuth.test.ts +++ b/apps/desktop/src/ipc/methods/cloudAuth.test.ts @@ -1,29 +1,35 @@ import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import { afterEach } from "vite-plus/test"; import { fetchCloudAuth, validateClerkFrontendApiUrl } from "./cloudAuth.ts"; const originalClerkPublishableKey = process.env.T3CODE_CLERK_PUBLISHABLE_KEY; +const originalFetch = globalThis.fetch; const clerkPublishableKey = (hostname: string): string => `pk_test_${Buffer.from(`${hostname}$`).toString("base64")}`; -function makeHttpClientLayer( - handler: ( - request: HttpClientRequest.HttpClientRequest, - ) => Effect.Effect, -) { - return Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => handler(request)), - ); -} +type FetchCall = readonly [input: RequestInfo | URL, init: RequestInit]; + +const recordedFetch = (...responses: ReadonlyArray) => { + const calls: Array = []; + let responseIndex = 0; + const fetchFn = ((input, init) => { + calls.push([input, init ?? {}]); + const response = responses[responseIndex++]; + if (!response) { + return Promise.reject(new Error("Unexpected fetch call")); + } + return Promise.resolve(response); + }) satisfies typeof fetch; + + return { fetchFn, calls }; +}; describe("Desktop cloud auth IPC", () => { afterEach(() => { + globalThis.fetch = originalFetch; if (originalClerkPublishableKey === undefined) { delete process.env.T3CODE_CLERK_PUBLISHABLE_KEY; } else { @@ -33,16 +39,8 @@ describe("Desktop cloud auth IPC", () => { it.effect("preserves Clerk's URL-encoded OAuth form content type", () => { const body = "strategy=oauth_google&redirect_url=t3code%3A%2F%2Fauth%2Fcallback"; - let forwardedRequest: HttpClientRequest.HttpClientRequest | null = null; - const layer = makeHttpClientLayer((request) => - Effect.sync(() => { - forwardedRequest = request; - return HttpClientResponse.fromWeb( - request, - Response.json({ response: { object: "sign_in_attempt" } }), - ); - }), - ); + const fetch = recordedFetch(Response.json({ response: { object: "sign_in_attempt" } })); + globalThis.fetch = fetch.fetchFn; return Effect.gen(function* () { yield* fetchCloudAuth.handler({ @@ -55,32 +53,25 @@ describe("Desktop cloud auth IPC", () => { body, }); - assert(forwardedRequest !== null); + const forwardedRequest = fetch.calls[0]; + assert(forwardedRequest !== undefined); + const [url, init] = forwardedRequest; + assert.equal(String(url), "https://example.clerk.accounts.dev/v1/client/sign_ins"); + assert.equal(init.method, "POST"); assert.equal( - forwardedRequest.headers["content-type"], + new Headers(init.headers).get("content-type"), "application/x-www-form-urlencoded;charset=UTF-8", ); - assert.equal(forwardedRequest.body._tag, "Uint8Array"); - if (forwardedRequest.body._tag === "Uint8Array") { - assert.equal(new TextDecoder().decode(forwardedRequest.body.body), body); - } - }).pipe(Effect.provide(layer)); + assert.equal(new TextDecoder().decode(init.body as Uint8Array), body); + }); }); it.effect( "allows the custom Clerk Frontend API host encoded by the configured publishable key", () => { process.env.T3CODE_CLERK_PUBLISHABLE_KEY = clerkPublishableKey("clerk.t3.codes"); - let forwardedRequest: HttpClientRequest.HttpClientRequest | null = null; - const layer = makeHttpClientLayer((request) => - Effect.sync(() => { - forwardedRequest = request; - return HttpClientResponse.fromWeb( - request, - Response.json({ response: { object: "client" } }), - ); - }), - ); + const fetch = recordedFetch(Response.json({ response: { object: "client" } })); + globalThis.fetch = fetch.fetchFn; return Effect.gen(function* () { yield* fetchCloudAuth.handler({ @@ -89,9 +80,10 @@ describe("Desktop cloud auth IPC", () => { headers: {}, }); - assert(forwardedRequest !== null); - assert.equal(forwardedRequest.url.toString(), "https://clerk.t3.codes/v1/client"); - }).pipe(Effect.provide(layer)); + const forwardedRequest = fetch.calls[0]; + assert(forwardedRequest !== undefined); + assert.equal(String(forwardedRequest[0]), "https://clerk.t3.codes/v1/client"); + }); }, ); diff --git a/apps/desktop/src/ipc/methods/cloudAuth.ts b/apps/desktop/src/ipc/methods/cloudAuth.ts index 3c9ee264fc58..a5a7aacff797 100644 --- a/apps/desktop/src/ipc/methods/cloudAuth.ts +++ b/apps/desktop/src/ipc/methods/cloudAuth.ts @@ -8,9 +8,11 @@ import { } from "@t3tools/shared/relayAuth"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; +import { identity } from "effect/Function"; +import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http"; +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; import * as DesktopCloudAuth from "../../app/DesktopCloudAuth.ts"; import * as DesktopCloudAuthTokenStore from "../../app/DesktopCloudAuthTokenStore.ts"; @@ -52,6 +54,68 @@ export function validateClerkFrontendApiUrl(rawUrl: string): URL { return url; } +function executeCloudAuthFetch(url: URL, input: typeof DesktopCloudAuthFetchInputSchema.Type) { + return Effect.gen(function* () { + const method = (input.method ?? "GET") as "GET" | "POST"; + const headers = new Headers(input.headers); + const response = yield* HttpClientRequest.make(method)(url).pipe( + HttpClientRequest.setHeaders(headers), + input.body === undefined + ? identity + : HttpClientRequest.bodyText(input.body, headers.get("content-type") ?? undefined), + HttpClient.execute, + Effect.mapError( + (cause) => + new DesktopCloudAuthFetchError({ + reason: "Desktop cloud auth fetch failed to execute.", + cause, + }), + ), + ); + + const body = yield* response.text.pipe( + Effect.mapError( + (cause) => + new DesktopCloudAuthFetchError({ + reason: "Desktop cloud auth fetch response could not be read.", + cause, + }), + ), + ); + + return { + ok: response.status >= 200 && response.status < 300, + status: response.status, + statusText: "", + headers: response.headers, + body, + }; + }); +} + +const electronNetFetchLayer = Layer.unwrap( + Effect.gen(function* () { + const electronFetch = yield* Effect.promise(async () => { + const electron = (await import("electron")) as { + readonly net?: { readonly fetch?: typeof globalThis.fetch }; + }; + return typeof electron.net?.fetch === "function" + ? electron.net.fetch.bind(electron.net) + : null; + }).pipe(Effect.catchCause(() => Effect.succeed(null))); + + if (!electronFetch) { + yield* Effect.logWarning( + "electron.net.fetch is not available, falling back to global fetch. This may cause unexpected errors.", + ); + } + + return FetchHttpClient.layer.pipe( + Layer.provide(Layer.succeed(FetchHttpClient.Fetch, electronFetch ?? globalThis.fetch)), + ); + }), +); + export const createCloudAuthRequest = makeIpcMethod({ channel: IpcChannels.CREATE_CLOUD_AUTH_REQUEST_CHANNEL, payload: Schema.Void, @@ -108,47 +172,6 @@ export const fetchCloudAuth = makeIpcMethod({ }), }); - const requestWithoutBody = HttpClientRequest.make((input.method ?? "GET") as "GET" | "POST")( - url, - { - headers: input.headers, - }, - ); - const request = - input.body === undefined - ? requestWithoutBody - : HttpClientRequest.bodyText( - requestWithoutBody, - input.body, - Option.getOrUndefined(Headers.get(requestWithoutBody.headers, "content-type")), - ); - - const response = yield* HttpClient.execute(request).pipe( - Effect.mapError( - (cause) => - new DesktopCloudAuthFetchError({ - reason: "Desktop cloud auth fetch failed.", - cause, - }), - ), - ); - - const body = yield* response.text.pipe( - Effect.mapError( - (cause) => - new DesktopCloudAuthFetchError({ - reason: "Desktop cloud auth fetch response could not be read.", - cause, - }), - ), - ); - - return { - ok: response.status >= 200 && response.status < 300, - status: response.status, - statusText: "", - headers: response.headers, - body, - }; + return yield* executeCloudAuthFetch(url, input).pipe(Effect.provide(electronNetFetchLayer)); }), }); diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index 97b590b03500..a54f22fec5b1 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -209,7 +209,7 @@ function readSettings( onSome: (raw) => decodeDesktopSettingsJson(raw).pipe( Effect.map((parsed) => normalizeDesktopSettingsDocument(parsed, appVersion)), - Effect.catch(() => Effect.succeed(defaultSettings)), + Effect.orElseSucceed(() => defaultSettings), ), }), ), diff --git a/apps/desktop/src/settings/DesktopClientSettings.ts b/apps/desktop/src/settings/DesktopClientSettings.ts index be4943594129..68d3fdc904ac 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.ts @@ -63,7 +63,7 @@ const readClientSettings = ( onSome: (raw) => decodeClientSettingsJson(raw).pipe( Effect.map((settings) => Option.some(settings)), - Effect.catch(() => Effect.succeed(Option.none())), + Effect.orElseSucceed(() => Option.none()), ), }), ), diff --git a/apps/desktop/src/settings/DesktopSavedEnvironments.ts b/apps/desktop/src/settings/DesktopSavedEnvironments.ts index bb571a9ad3b8..531b50ba73b7 100644 --- a/apps/desktop/src/settings/DesktopSavedEnvironments.ts +++ b/apps/desktop/src/settings/DesktopSavedEnvironments.ts @@ -185,7 +185,7 @@ function readRegistryDocument( onSome: (raw) => decodeSavedEnvironmentRegistryDocumentJson(raw).pipe( Effect.map(normalizeSavedEnvironmentRegistryDocument), - Effect.catch(() => Effect.succeed({ version: 1, records: [] })), + Effect.orElseSucceed(() => ({ version: 1, records: [] })), ), }), ), diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index a7d775d68fb4..13ac35b6297a 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -196,7 +196,7 @@ const runCommandOutput = Effect.fn("desktop.shellEnvironment.runCommandOutput")( .pipe( Effect.timeoutOption(input.timeout), Effect.map(Option.getOrElse(() => "")), - Effect.catch(() => Effect.succeed("")), + Effect.orElseSucceed(() => ""), ); }); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index 056bca4a0e1d..e6c81d8d25be 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -120,7 +120,7 @@ function parseAppUpdateYml(raw: string): Effect.Effect (config.provider ? Option.some(config) : Option.none())), - Effect.catch(() => Effect.succeed(Option.none())), + Effect.orElseSucceed(() => Option.none()), ); } diff --git a/apps/server/package.json b/apps/server/package.json index 5f4555501fd7..8c248df16be6 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -38,7 +38,7 @@ "@t3tools/shared": "workspace:*", "@t3tools/tailscale": "workspace:*", "@t3tools/web": "workspace:*", - "@types/bun": "catalog:", + "@types/bun": "1.3.14", "@types/node": "catalog:", "effect-acp": "workspace:*", "effect-codex-app-server": "workspace:*", diff --git a/apps/server/src/checkpointing/Layers/CheckpointStore.ts b/apps/server/src/checkpointing/Layers/CheckpointStore.ts index 9bd72e78b6b6..53b8d163e4c9 100644 --- a/apps/server/src/checkpointing/Layers/CheckpointStore.ts +++ b/apps/server/src/checkpointing/Layers/CheckpointStore.ts @@ -35,7 +35,7 @@ const makeCheckpointStore = Effect.gen(function* () { const isGitRepository: CheckpointStoreShape["isGitRepository"] = (cwd) => vcsRegistry.resolve({ cwd, requestedKind: "git" }).pipe( Effect.map(() => true), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ); const captureCheckpoint: CheckpointStoreShape["captureCheckpoint"] = Effect.fn( diff --git a/apps/server/src/cloud/ManagedEndpointRuntime.ts b/apps/server/src/cloud/ManagedEndpointRuntime.ts index 65656292ebcb..73e549ebf49b 100644 --- a/apps/server/src/cloud/ManagedEndpointRuntime.ts +++ b/apps/server/src/cloud/ManagedEndpointRuntime.ts @@ -152,9 +152,7 @@ export const makeCloudManagedEndpointRuntime = Effect.gen(function* () { const nextConfigKey = runtimeConfigKey(config); const active = yield* Ref.get(activeRef); if (active?.configKey === nextConfigKey) { - const isRunning = yield* active.child.isRunning.pipe( - Effect.catch(() => Effect.succeed(false)), - ); + const isRunning = yield* active.child.isRunning.pipe(Effect.orElseSucceed(() => false)); if (isRunning) { return { status: "running", diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 3eb0297b87fb..ff82aea9b453 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -739,7 +739,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp"; const canonicalizeExistingPath = (value: string) => - fileSystem.realPath(value).pipe(Effect.catch(() => Effect.succeed(value))); + fileSystem.realPath(value).pipe(Effect.orElseSucceed(() => value)); const normalizeStatusCacheKey = canonicalizeExistingPath; const nonRepositoryStatusDetails = { isRepo: false, @@ -803,7 +803,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { if (details.isDefaultBranch && latest.state !== "open") return null; return toStatusPr(latest); }), - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ) : null; @@ -825,7 +825,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { ); const readConfigValueNullable = (cwd: string, key: string) => - gitCore.readConfigValue(cwd, key).pipe(Effect.catch(() => Effect.succeed(null))); + gitCore.readConfigValue(cwd, key).pipe(Effect.orElseSucceed(() => null)); const resolveHostingProvider = Effect.fn("resolveHostingProvider")(function* ( cwd: string, @@ -1011,7 +1011,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { ) { const terms = yield* sourceControlProvider(cwd).pipe( Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), - Effect.catch(() => Effect.succeed(getChangeRequestTerminologyForKind("unknown"))), + Effect.orElseSucceed(() => getChangeRequestTerminologyForKind("unknown")), ); const summary = summarizeGitActionResult(result, terms); let latestOpenPr: PullRequestInfo | null = null; @@ -1055,7 +1055,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { upstreamRef: finalBranchContext.upstreamRef, }).pipe( Effect.flatMap((headContext) => findOpenPr(cwd, headContext)), - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); } @@ -1119,7 +1119,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { const defaultFromProvider = yield* sourceControlProvider(cwd).pipe( Effect.flatMap((provider) => provider.getDefaultBranch({ cwd })), - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); if (defaultFromProvider) { return defaultFromProvider; @@ -1725,7 +1725,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { const changeRequestTerms = wantsPr ? yield* sourceControlProvider(input.cwd).pipe( Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), - Effect.catch(() => Effect.succeed(getChangeRequestTerminologyForKind("unknown"))), + Effect.orElseSucceed(() => getChangeRequestTerminologyForKind("unknown")), ) : null; diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts index 4e815aae8ccd..517d57168c39 100644 --- a/apps/server/src/http.ts +++ b/apps/server/src/http.ts @@ -156,8 +156,8 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add( otlpTracesUrl, }), ), - Effect.catch(() => - Effect.succeed(HttpServerResponse.text("Trace export failed.", { status: 502 })), + Effect.orElseSucceed(() => + HttpServerResponse.text("Trace export failed.", { status: 502 }), ), ); }).pipe( @@ -205,9 +205,7 @@ export const attachmentsRouteLayer = HttpRouter.add( } const fileSystem = yield* FileSystem.FileSystem; - const fileInfo = yield* fileSystem - .stat(filePath) - .pipe(Effect.catch(() => Effect.succeed(null))); + const fileInfo = yield* fileSystem.stat(filePath).pipe(Effect.orElseSucceed(() => null)); if (!fileInfo || fileInfo.type !== "File") { return HttpServerResponse.text("Not Found", { status: 404 }); } @@ -218,9 +216,7 @@ export const attachmentsRouteLayer = HttpRouter.add( "Cache-Control": "public, max-age=31536000, immutable", }, }).pipe( - Effect.catch(() => - Effect.succeed(HttpServerResponse.text("Internal Server Error", { status: 500 })), - ), + Effect.orElseSucceed(() => HttpServerResponse.text("Internal Server Error", { status: 500 })), ); }).pipe( Effect.catchTags({ @@ -265,9 +261,7 @@ export const projectFaviconRouteLayer = HttpRouter.add( "Cache-Control": PROJECT_FAVICON_CACHE_CONTROL, }, }).pipe( - Effect.catch(() => - Effect.succeed(HttpServerResponse.text("Internal Server Error", { status: 500 })), - ), + Effect.orElseSucceed(() => HttpServerResponse.text("Internal Server Error", { status: 500 })), ); }).pipe( Effect.catchTags({ @@ -337,14 +331,12 @@ export const staticAndDevRouteLayer = HttpRouter.add( } } - const fileInfo = yield* fileSystem - .stat(filePath) - .pipe(Effect.catch(() => Effect.succeed(null))); + const fileInfo = yield* fileSystem.stat(filePath).pipe(Effect.orElseSucceed(() => null)); if (!fileInfo || fileInfo.type !== "File") { const indexPath = path.resolve(staticRoot, "index.html"); const indexData = yield* fileSystem .readFile(indexPath) - .pipe(Effect.catch(() => Effect.succeed(null))); + .pipe(Effect.orElseSucceed(() => null)); if (!indexData) { return HttpServerResponse.text("Not Found", { status: 404 }); } @@ -355,9 +347,7 @@ export const staticAndDevRouteLayer = HttpRouter.add( } const contentType = Mime.getType(filePath) ?? "application/octet-stream"; - const data = yield* fileSystem - .readFile(filePath) - .pipe(Effect.catch(() => Effect.succeed(null))); + const data = yield* fileSystem.readFile(filePath).pipe(Effect.orElseSucceed(() => null)); if (!data) { return HttpServerResponse.text("Internal Server Error", { status: 500 }); } diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index a177ed338239..4ffe605dd93d 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -335,7 +335,7 @@ const runAttachmentSideEffects = Effect.fn("runAttachmentSideEffects")(function* const attachmentsRootDir = serverConfig.attachmentsDir; const readAttachmentRootEntries = fileSystem .readDirectory(attachmentsRootDir, { recursive: false }) - .pipe(Effect.catch(() => Effect.succeed([] as Array))); + .pipe(Effect.orElseSucceed(() => [] as Array)); const removeDeletedThreadAttachmentEntry = Effect.fn("removeDeletedThreadAttachmentEntry")( function* (threadSegment: string, entry: string) { @@ -397,9 +397,7 @@ const runAttachmentSideEffects = Effect.fn("runAttachmentSideEffects")(function* } const absolutePath = path.join(attachmentsRootDir, relativePath); - const fileInfo = yield* fileSystem - .stat(absolutePath) - .pipe(Effect.catch(() => Effect.succeed(null))); + const fileInfo = yield* fileSystem.stat(absolutePath).pipe(Effect.orElseSucceed(() => null)); if (!fileInfo || fileInfo.type !== "File") { return; } diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 59787e0b5451..c5c155fd24af 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -344,6 +344,26 @@ function runtimeEventToActivities( ]; } + case "tool.denied": { + return [ + { + id: event.eventId, + createdAt: event.createdAt, + tone: "error", + kind: "tool.denied", + summary: `Tool denied: ${event.payload.toolName}`, + payload: { + toolName: event.payload.toolName, + ...(event.payload.toolUseId ? { toolUseId: event.payload.toolUseId } : {}), + ...(event.payload.reason ? { detail: truncateDetail(event.payload.reason) } : {}), + ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), + }, + turnId: toTurnId(event.turnId) ?? null, + ...maybeSequence, + }, + ]; + } + case "runtime.warning": { return [ { @@ -351,7 +371,9 @@ function runtimeEventToActivities( createdAt: event.createdAt, tone: "info", kind: "runtime.warning", - summary: "Runtime warning", + // Use the adapter-supplied message as the row label so the work log + // shows what the warning was about, not a generic "Runtime warning". + summary: truncateDetail(event.payload.message, 120), payload: { message: truncateDetail(event.payload.message), ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}), diff --git a/apps/server/src/project/Layers/ProjectFaviconResolver.ts b/apps/server/src/project/Layers/ProjectFaviconResolver.ts index ed5412bf138c..cdfddd5438a0 100644 --- a/apps/server/src/project/Layers/ProjectFaviconResolver.ts +++ b/apps/server/src/project/Layers/ProjectFaviconResolver.ts @@ -80,9 +80,7 @@ export const makeProjectFaviconResolver = Effect.gen(function* () { if (!isPathWithinProject(projectCwd, candidate)) { continue; } - const stats = yield* fileSystem - .stat(candidate) - .pipe(Effect.catch(() => Effect.succeed(null))); + const stats = yield* fileSystem.stat(candidate).pipe(Effect.orElseSucceed(() => null)); if (stats?.type === "File") { return candidate; } @@ -105,7 +103,7 @@ export const makeProjectFaviconResolver = Effect.gen(function* () { const sourcePath = path.join(cwd, sourceFile); const source = yield* fileSystem .readFileString(sourcePath) - .pipe(Effect.catch(() => Effect.succeed(null))); + .pipe(Effect.orElseSucceed(() => null)); if (!source) { continue; } diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index c3021b3e1a1d..1a0f41621953 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -977,6 +977,52 @@ function sdkNativeMethod(message: SDKMessage): string { return `claude/${message.type}`; } +// Discriminator/identity keys carry no human-readable content; everything else +// on an unmodeled SDK message is potentially worth surfacing in the work log. +const SDK_MESSAGE_NOISE_KEYS = new Set([ + "type", + "subtype", + "uuid", + "parent_uuid", + "session_id", + "parent_tool_use_id", + "request_id", +]); + +// Pull the salient scalar content out of a message the adapter doesn't model +// yet, so the work-log row shows what actually arrived (e.g. a notification's +// text) instead of an opaque "unhandled subtype" placeholder. Nested structures +// are left to the full payload retained in the event's `detail`. +function previewUnknownSdkContent(message: unknown): string | undefined { + if (!message || typeof message !== "object") { + return undefined; + } + const parts: string[] = []; + for (const [key, value] of Object.entries(message as Record)) { + if (SDK_MESSAGE_NOISE_KEYS.has(key)) { + continue; + } + if (typeof value === "string") { + const trimmed = value.trim(); + if (trimmed.length > 0) { + parts.push(`${key}: ${trimmed}`); + } + } else if (typeof value === "number" || typeof value === "boolean") { + parts.push(`${key}: ${String(value)}`); + } + } + if (parts.length === 0) { + return undefined; + } + const joined = parts.join(" · "); + return joined.length > 280 ? `${joined.slice(0, 279)}…` : joined; +} + +function describeUnknownSdkMessage(kind: string, message: unknown): string { + const preview = previewUnknownSdkContent(message); + return preview ? `${kind} — ${preview}` : `${kind} (no displayable text content)`; +} + function sdkNativeItemId(message: SDKMessage): string | undefined { if (message.type === "assistant") { const maybeId = (message.message as { id?: unknown }).id; @@ -2274,10 +2320,31 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( }, }); return; + case "thinking_tokens": + return; + case "permission_denied": + yield* offerRuntimeEvent({ + ...base, + type: "tool.denied", + payload: { + toolName: message.tool_name, + ...(message.tool_use_id ? { toolUseId: message.tool_use_id } : {}), + ...(message.decision_reason ? { reason: message.decision_reason } : {}), + ...(message.agent_id ? { agentId: message.agent_id } : {}), + }, + }); + return; + case "mirror_error": + yield* emitRuntimeError( + context, + `Claude workspace mirror error: ${message.error}`, + message, + ); + return; default: yield* emitRuntimeWarning( context, - `Unhandled Claude system message subtype '${message.subtype}'.`, + describeUnknownSdkMessage(`Claude system message '${message.subtype}'`, message), message, ); return; @@ -2391,7 +2458,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( default: yield* emitRuntimeWarning( context, - `Unhandled Claude SDK message type '${message.type}'.`, + describeUnknownSdkMessage(`Claude SDK message '${message.type}'`, message), message, ); return; diff --git a/apps/server/src/provider/providerMaintenance.ts b/apps/server/src/provider/providerMaintenance.ts index 7f5e9d94dc5f..3b0fabf6a99b 100644 --- a/apps/server/src/provider/providerMaintenance.ts +++ b/apps/server/src/provider/providerMaintenance.ts @@ -347,7 +347,7 @@ export const resolveProviderMaintenanceCapabilitiesEffect = Effect.fn( const fileSystem = yield* FileSystem.FileSystem; const realCommandPath = yield* fileSystem .realPath(resolvedCommandPath) - .pipe(Effect.catch(() => Effect.succeed(resolvedCommandPath))); + .pipe(Effect.orElseSucceed(() => resolvedCommandPath)); return resolver.resolve({ ...options, realCommandPath, @@ -406,7 +406,7 @@ const fetchNpmLatestVersion = Effect.fn("fetchNpmLatestVersion")(function* (pack ).pipe(HttpClientRequest.setHeader("accept", "application/json")); const response = yield* client.execute(request).pipe( Effect.timeoutOption(LATEST_VERSION_TIMEOUT_MS), - Effect.catch(() => Effect.succeed(Option.none())), + Effect.orElseSucceed(() => Option.none()), ); if (Option.isNone(response)) { return null; @@ -417,7 +417,7 @@ const fetchNpmLatestVersion = Effect.fn("fetchNpmLatestVersion")(function* (pack } const payload = yield* httpResponse.json.pipe( Effect.flatMap(Schema.decodeUnknownEffect(NpmLatestVersionResponse)), - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); return payload ? nonEmptyString(payload.version) : null; }); diff --git a/apps/server/src/review/ReviewService.ts b/apps/server/src/review/ReviewService.ts index ddbad606e929..63f1d1332135 100644 --- a/apps/server/src/review/ReviewService.ts +++ b/apps/server/src/review/ReviewService.ts @@ -35,9 +35,7 @@ export const make = Effect.fn("makeReviewService")(function* () { const git = yield* GitVcsDriver.GitVcsDriver; const canonicalizePath = (value: string) => - fileSystem - .realPath(path.resolve(value)) - .pipe(Effect.catch(() => Effect.succeed(path.resolve(value)))); + fileSystem.realPath(path.resolve(value)).pipe(Effect.orElseSucceed(() => path.resolve(value))); const isWithinRoot = (candidate: string, root: string) => { const relative = path.relative(root, candidate); diff --git a/apps/server/src/sourceControl/BitbucketApi.ts b/apps/server/src/sourceControl/BitbucketApi.ts index 7b5f19b6b917..632778eca244 100644 --- a/apps/server/src/sourceControl/BitbucketApi.ts +++ b/apps/server/src/sourceControl/BitbucketApi.ts @@ -359,7 +359,7 @@ function responseError( response: HttpClientResponse.HttpClientResponse, ): Effect.Effect { return response.text.pipe( - Effect.catch(() => Effect.succeed("")), + Effect.orElseSucceed(() => ""), Effect.flatMap((body) => Effect.fail( new BitbucketApiError({ @@ -524,7 +524,7 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { ); const readConfigValueNullable = (cwd: string, key: string) => - git.readConfigValue(cwd, key).pipe(Effect.catch(() => Effect.succeed(null))); + git.readConfigValue(cwd, key).pipe(Effect.orElseSucceed(() => null)); const resolveCheckoutRemote = Effect.fn("BitbucketApi.resolveCheckoutRemote")(function* (input: { readonly cwd: string; @@ -544,7 +544,7 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { if (!input.isCrossRepository) { const remoteName = yield* git .resolvePrimaryRemoteName(input.cwd) - .pipe(Effect.catch(() => Effect.succeed(null))); + .pipe(Effect.orElseSucceed(() => null)); if (remoteName) return remoteName; } @@ -575,7 +575,7 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { host: Option.some("bitbucket.org"), detail: Option.none(), })), - Effect.catch(() => Effect.succeed(authFromConfig(config))), + Effect.orElseSucceed(() => authFromConfig(config)), ), listPullRequests: (input) => resolveRepository(input).pipe( @@ -685,8 +685,8 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { { repository: getRepositoryFromLocator(locator), branchingModel: getBranchingModelFromLocator(locator).pipe( - Effect.catch(() => - Effect.succeed(null), + Effect.orElseSucceed( + (): typeof RawBitbucketBranchingModelSchema.Type | null => null, ), ), }, diff --git a/apps/server/src/sourceControl/SourceControlProviderDiscovery.ts b/apps/server/src/sourceControl/SourceControlProviderDiscovery.ts index c5ab091ac458..856d6948e090 100644 --- a/apps/server/src/sourceControl/SourceControlProviderDiscovery.ts +++ b/apps/server/src/sourceControl/SourceControlProviderDiscovery.ts @@ -299,7 +299,7 @@ export const refineUnknownRemoteProvider = Effect.fn("refineUnknownRemoteProvide auth, }), ), - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ), ); const provider = providers.find((candidate) => candidate !== null); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 3f8c146f4c7a..106d300ec2da 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -277,12 +277,10 @@ export const make = Effect.fn("makeSourceControlRepositoryService")(function* () }) .pipe( Effect.map(() => true), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ); if (!hasCommits) { - const details = yield* git - .statusDetails(input.cwd) - .pipe(Effect.catch(() => Effect.succeed(null))); + const details = yield* git.statusDetails(input.cwd).pipe(Effect.orElseSucceed(() => null)); return { repository: toRepositoryInfo(providerKind, urls), remoteName, diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.test.ts b/apps/server/src/telemetry/Layers/AnalyticsService.test.ts index 03ebd2fdc65a..5aa47406d9b5 100644 --- a/apps/server/src/telemetry/Layers/AnalyticsService.test.ts +++ b/apps/server/src/telemetry/Layers/AnalyticsService.test.ts @@ -62,7 +62,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { const payload = yield* request.json.pipe( Effect.map((body) => body as RecordedBatchRequest["body"]), - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); capturedRequests.push({ path: request.url, body: payload }); diff --git a/apps/server/src/terminal/Layers/BunPTY.ts b/apps/server/src/terminal/Layers/BunPTY.ts index 40113a1915cd..5fde1469193c 100644 --- a/apps/server/src/terminal/Layers/BunPTY.ts +++ b/apps/server/src/terminal/Layers/BunPTY.ts @@ -1,3 +1,5 @@ +/// + import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { PtyAdapter } from "../Services/PTY.ts"; diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index a690975a4b95..cd490de1e3f3 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -1289,7 +1289,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith const threadPrefix = `${toSafeThreadId(threadId)}_`; const entries = yield* fileSystem .readDirectory(logsDir, { recursive: false }) - .pipe(Effect.catch(() => Effect.succeed([] as Array))); + .pipe(Effect.orElseSucceed(() => [] as Array)); yield* Effect.forEach( entries.filter( (name) => diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 7f400d9ce509..d42fb07aa034 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -140,9 +140,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func if (!resolvedPath || !path.isAbsolute(resolvedPath)) { continue; } - const fileInfo = yield* fileSystem - .stat(resolvedPath) - .pipe(Effect.catch(() => Effect.succeed(null))); + const fileInfo = yield* fileSystem.stat(resolvedPath).pipe(Effect.orElseSucceed(() => null)); if (!fileInfo || fileInfo.type !== "File") { continue; } diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index c75570d4b1cb..465cd21f320d 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -403,7 +403,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( "GitVcsDriver.detectRepository.commonDir", cwd, ["rev-parse", "--git-common-dir"], - ).pipe(Effect.catch(() => Effect.succeed(null))); + ).pipe(Effect.orElseSucceed(() => null)); return { kind: "git" as const, diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index b6a48f5b18c8..240c20336c60 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -885,7 +885,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const remoteNames = yield* runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe( Effect.map(parseRemoteNames), - Effect.catch(() => Effect.succeed>([])), + Effect.orElseSucceed((): ReadonlyArray => []), ); return ( parseUpstreamRefWithRemoteNames(upstreamRef, remoteNames) ?? @@ -996,7 +996,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* cwd: string, branchName: string, ) { - const remoteNames = yield* listRemoteNames(cwd).pipe(Effect.catch(() => Effect.succeed([]))); + const remoteNames = yield* listRemoteNames(cwd).pipe(Effect.orElseSucceed(() => [])); const parsedRemoteRef = parseRemoteRefWithRemoteNames(branchName, remoteNames); return parsedRemoteRef?.branchName ?? branchName; }); @@ -1042,7 +1042,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* return pushDefaultRemote; } - return yield* resolvePrimaryRemoteName(cwd).pipe(Effect.catch(() => Effect.succeed(null))); + return yield* resolvePrimaryRemoteName(cwd).pipe(Effect.orElseSucceed(() => null)); }); const ensureRemote: GitVcsDriver.GitVcsDriverShape["ensureRemote"] = Effect.fn("ensureRemote")( @@ -1090,7 +1090,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ).pipe(Effect.map((stdout) => stdout.trim())); const primaryRemoteName = yield* resolvePrimaryRemoteName(cwd).pipe( - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); const defaultBranch = primaryRemoteName === null ? null : yield* resolveDefaultBranchName(cwd, primaryRemoteName); @@ -1231,7 +1231,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* allowNonZeroExit: true, }, ), - originRemoteExists(cwd).pipe(Effect.catch(() => Effect.succeed(false))), + originRemoteExists(cwd).pipe(Effect.orElseSucceed(() => false)), ], { concurrency: "unbounded" }, ); @@ -1276,9 +1276,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const fallbackAheadCount = !upstreamRef && refName - ? yield* computeAheadCountAgainstBase(cwd, refName).pipe( - Effect.catch(() => Effect.succeed(0)), - ) + ? yield* computeAheadCountAgainstBase(cwd, refName).pipe(Effect.orElseSucceed(() => 0)) : null; if (fallbackAheadCount !== null) { @@ -1294,9 +1292,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* aheadOfDefaultCount = fallbackAheadCount !== null ? fallbackAheadCount - : yield* computeAheadCountAgainstBase(cwd, refName).pipe( - Effect.catch(() => Effect.succeed(0)), - ); + : yield* computeAheadCountAgainstBase(cwd, refName).pipe(Effect.orElseSucceed(() => 0)); } const stagedEntries = parseNumstatEntries(stagedNumstatStdout); @@ -1494,11 +1490,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* } const comparableBaseBranch = yield* resolveBaseBranchForNoUpstream(cwd, branch).pipe( - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); if (comparableBaseBranch) { const publishRemoteName = yield* resolvePushRemoteName(cwd, branch).pipe( - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); if (!publishRemoteName) { return { @@ -1508,7 +1504,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* } const hasRemoteBranch = yield* remoteBranchExists(cwd, publishRemoteName, branch).pipe( - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ); if (hasRemoteBranch) { return { @@ -1545,7 +1541,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* } const currentUpstream = yield* resolveCurrentUpstream(cwd).pipe( - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ); if (currentUpstream) { yield* runGit("GitVcsDriver.pushCurrentBranch.pushUpstream", cwd, [ @@ -1716,7 +1712,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* input.baseRef ?? (branch ? yield* resolveBaseBranchForNoUpstream(input.cwd, branch).pipe( - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ) : null); @@ -1729,18 +1725,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* appendTruncationMarker: true, }, ).pipe( - Effect.catch(() => - Effect.succeed({ - exitCode: 0, - stdout: "", - stderr: "", - stdoutTruncated: false, - stderrTruncated: false, - }), - ), + Effect.orElseSucceed(() => ({ + exitCode: 0, + stdout: "", + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + })), ); const dirtyUntracked = yield* readUntrackedReviewDiffs(input.cwd).pipe( - Effect.catch(() => Effect.succeed({ diff: "", truncated: false })), + Effect.orElseSucceed(() => ({ diff: "", truncated: false })), ); const dirtyDiff = [dirtyTrackedResult.stdout.trimEnd(), dirtyUntracked.diff.trimEnd()] .filter((diff) => diff.length > 0) @@ -1757,15 +1751,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* appendTruncationMarker: true, }, ).pipe( - Effect.catch(() => - Effect.succeed({ - exitCode: 0, - stdout: "", - stderr: "", - stdoutTruncated: false, - stderrTruncated: false, - }), - ), + Effect.orElseSucceed(() => ({ + exitCode: 0, + stdout: "", + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + })), ) : null; const baseDiff = baseResult?.stdout ?? ""; @@ -1827,7 +1819,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const listRefs: GitVcsDriver.GitVcsDriverShape["listRefs"] = Effect.fn("listRefs")( function* (input) { const branchRecencyPromise = readBranchRecency(input.cwd).pipe( - Effect.catch(() => Effect.succeed(new Map())), + Effect.orElseSucceed(() => new Map()), ); const localBranchResult = yield* executeGit( "GitVcsDriver.listRefs.branchNoColor", @@ -1970,7 +1962,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const candidatePath = line.slice("worktree ".length); const exists = yield* fileSystem.stat(candidatePath).pipe( Effect.map(() => true), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ); currentPath = exists ? candidatePath : null; } else if (line.startsWith("branch refs/heads/") && currentPath) { diff --git a/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts b/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts index 84ea5c519376..ffee4d56a526 100644 --- a/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts +++ b/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts @@ -392,5 +392,20 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => { expect(error.detail).toBe("Relative filesystem browse paths require a current project."); }), ); + + it.effect("returns an empty listing when the OS denies directory access", () => + Effect.gen(function* () { + const workspaceEntries = yield* WorkspaceEntries; + const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-eacces-" }); + + const denied = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" }); + vi.spyOn(fsPromises, "readdir").mockRejectedValueOnce(denied); + + const result = yield* workspaceEntries.browse({ + partialPath: appendSeparator(cwd), + }); + expect(result).toEqual({ parentPath: cwd, entries: [] }); + }), + ); }); }); diff --git a/apps/server/src/workspace/Layers/WorkspaceEntries.ts b/apps/server/src/workspace/Layers/WorkspaceEntries.ts index f3b9d5b7671b..95d957136b70 100644 --- a/apps/server/src/workspace/Layers/WorkspaceEntries.ts +++ b/apps/server/src/workspace/Layers/WorkspaceEntries.ts @@ -188,7 +188,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () { const isInsideVcsWorkTree = (cwd: string): Effect.Effect => vcsRegistry.detect({ cwd }).pipe( Effect.map((handle) => handle !== null), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ); const filterVcsIgnoredPaths = ( @@ -200,23 +200,23 @@ export const makeWorkspaceEntries = Effect.gen(function* () { handle ? handle.driver.filterIgnoredPaths(cwd, relativePaths).pipe( Effect.map((paths) => [...paths]), - Effect.catch(() => Effect.succeed(relativePaths)), + Effect.orElseSucceed(() => relativePaths), ) : Effect.succeed(relativePaths), ), - Effect.catch(() => Effect.succeed(relativePaths)), + Effect.orElseSucceed(() => relativePaths), ); const buildWorkspaceIndexFromVcs = Effect.fn("WorkspaceEntries.buildWorkspaceIndexFromVcs")( function* (cwd: string) { - const vcs = yield* vcsRegistry.detect({ cwd }).pipe(Effect.catch(() => Effect.succeed(null))); + const vcs = yield* vcsRegistry.detect({ cwd }).pipe(Effect.orElseSucceed(() => null)); if (!vcs) { return null; } const listedFiles = yield* vcs.driver .listWorkspaceFiles(cwd) - .pipe(Effect.catch(() => Effect.succeed(null))); + .pipe(Effect.orElseSucceed(() => null)); if (!listedFiles) { return null; @@ -431,7 +431,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () { const invalidate: WorkspaceEntriesShape["invalidate"] = Effect.fn("WorkspaceEntries.invalidate")( function* (cwd) { const normalizedCwd = yield* normalizeWorkspaceRoot(cwd).pipe( - Effect.catch(() => Effect.succeed(cwd)), + Effect.orElseSucceed(() => cwd), ); yield* Cache.invalidate(workspaceIndexCache, cwd); if (normalizedCwd !== cwd) { @@ -457,7 +457,18 @@ export const makeWorkspaceEntries = Effect.gen(function* () { detail: `Unable to browse '${parentPath}': ${cause instanceof Error ? cause.message : String(cause)}`, cause, }), - }); + }).pipe( + // The user can deny macOS TCC prompts for the target dir (Documents, + // Downloads, Music, etc.); surface an empty listing instead of an + // error so the caller doesn't retry-loop the prompt. + Effect.catchIf( + (error) => { + const code = (error.cause as NodeJS.ErrnoException | undefined)?.code; + return code === "EACCES" || code === "EPERM"; + }, + () => Effect.succeed([]), + ), + ); const showHidden = endsWithSeparator || prefix.startsWith("."); const lowerPrefix = prefix.toLowerCase(); diff --git a/apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts index e748a27a58ae..9b93b1e863bb 100644 --- a/apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts @@ -132,7 +132,7 @@ it.layer(TestLayer)("WorkspaceFileSystemLive", (it) => { const escapedPath = path.resolve(cwd, "..", "escape.md"); const escapedStat = yield* fileSystem .stat(escapedPath) - .pipe(Effect.catch(() => Effect.succeed(null))); + .pipe(Effect.orElseSucceed(() => null)); expect(escapedStat).toBeNull(); }), ); diff --git a/apps/server/src/workspace/Layers/WorkspacePaths.ts b/apps/server/src/workspace/Layers/WorkspacePaths.ts index 9dd33aaac7db..f994aa875efe 100644 --- a/apps/server/src/workspace/Layers/WorkspacePaths.ts +++ b/apps/server/src/workspace/Layers/WorkspacePaths.ts @@ -37,7 +37,7 @@ export const makeWorkspacePaths = Effect.gen(function* () { const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim(), path)); let workspaceStat = yield* fileSystem .stat(normalizedWorkspaceRoot) - .pipe(Effect.catch(() => Effect.succeed(null))); + .pipe(Effect.orElseSucceed(() => null)); if (!workspaceStat && options?.createIfMissing) { yield* fileSystem.makeDirectory(normalizedWorkspaceRoot, { recursive: true }).pipe( Effect.mapError( @@ -50,7 +50,7 @@ export const makeWorkspacePaths = Effect.gen(function* () { ); workspaceStat = yield* fileSystem .stat(normalizedWorkspaceRoot) - .pipe(Effect.catch(() => Effect.succeed(null))); + .pipe(Effect.orElseSucceed(() => null)); } if (!workspaceStat) { return yield* new WorkspaceRootNotExistsError({ diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 8e958545ec29..23a9a2c8892d 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -503,7 +503,7 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => repositoryIdentity, }, } satisfies OrchestrationEvent; - }).pipe(Effect.catch(() => Effect.succeed(event))); + }).pipe(Effect.orElseSucceed(() => event)); default: return Effect.succeed(event); } @@ -526,7 +526,7 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => project: nextProject, })), ), - Effect.catch(() => Effect.succeed(Option.none())), + Effect.orElseSucceed(() => Option.none()), ); case "project.deleted": return Effect.succeed( @@ -554,7 +554,7 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => thread: nextThread, })), ), - Effect.catch(() => Effect.succeed(Option.none())), + Effect.orElseSucceed(() => Option.none()), ); default: if (event.aggregateKind !== "thread") { @@ -570,7 +570,7 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => thread: nextThread, })), ), - Effect.catch(() => Effect.succeed(Option.none())), + Effect.orElseSucceed(() => Option.none()), ); } }; @@ -844,7 +844,7 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => thread.session !== null && thread.session.status !== "stopped", }), ), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ) : false; const result = yield* dispatchNormalizedCommand(normalizedCommand); diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 44e9d1206d08..63ce5a30c458 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["node", "bun"], + "types": ["node"], "lib": ["ESNext", "esnext.disposable"] }, "include": ["src", "vite.config.ts", "scripts", "integration", "../../scripts/lib"] diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 999542b605c1..ffd68b52c8dc 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -565,11 +565,7 @@ function OpenCommandPaletteDialog() { !relativePathNeedsActiveProject, }); const browseEntries = browseResult?.entries ?? EMPTY_BROWSE_ENTRIES; - const { - filteredEntries: filteredBrowseEntries, - highlightedEntry: highlightedBrowseEntry, - exactEntry: exactBrowseEntry, - } = useMemo( + const { filteredEntries: filteredBrowseEntries, exactEntry: exactBrowseEntry } = useMemo( () => filterBrowseEntries({ browseEntries, browseFilterQuery, highlightedItemValue }), [browseEntries, browseFilterQuery, highlightedItemValue], ); @@ -590,27 +586,17 @@ function OpenCommandPaletteDialog() { [browseEnvironmentId, currentProjectCwdForBrowse, fetchBrowseResult, queryClient], ); - // Prefetch the parent and the most likely next child so browse navigation - // stays warm without scanning every child directory in large trees. + // Prefetch only the parent (for back-navigation). Prefetching the + // highlighted child on every arrow-key press triggers a macOS TCC prompt + // whenever the highlighted entry is a permission-gated home dir (Music, + // Documents, Downloads, Desktop, etc.), so we wait for explicit navigation. useEffect(() => { if (!isBrowsing || filteredBrowseEntries.length === 0) return; if (canNavigateUp(query)) { prefetchBrowsePath(getBrowseParentPath(query)!); } - - const nextChild = highlightedBrowseEntry ?? exactBrowseEntry; - if (nextChild) { - prefetchBrowsePath(appendBrowsePathSegment(query, nextChild.name)); - } - }, [ - exactBrowseEntry, - filteredBrowseEntries.length, - highlightedBrowseEntry, - isBrowsing, - prefetchBrowsePath, - query, - ]); + }, [filteredBrowseEntries.length, isBrowsing, prefetchBrowsePath, query]); const openProjectFromSearch = useMemo( () => async (project: (typeof projects)[number]) => { diff --git a/infra/relay/scripts/deploy.ts b/infra/relay/scripts/deploy.ts index eb90a2ead334..d259b8b026cb 100644 --- a/infra/relay/scripts/deploy.ts +++ b/infra/relay/scripts/deploy.ts @@ -95,7 +95,7 @@ const loadDeployConfigProvider = Effect.fn("relay.deploy.loadConfigProvider")(fu } return yield* ConfigProvider.fromDotEnv({ path: path.join(root, ".env") }).pipe( - Effect.catch(() => Effect.succeed(ConfigProvider.fromEnv())), + Effect.orElseSucceed(() => ConfigProvider.fromEnv()), ); }); diff --git a/infra/relay/src/agentActivity/AgentActivityRows.ts b/infra/relay/src/agentActivity/AgentActivityRows.ts index 00d3f5f78006..6f940c5523f8 100644 --- a/infra/relay/src/agentActivity/AgentActivityRows.ts +++ b/infra/relay/src/agentActivity/AgentActivityRows.ts @@ -1,7 +1,6 @@ import type { RelayAgentActivityState } from "@t3tools/contracts/relay"; import { RelayAgentActivityState as RelayAgentActivityStateSchema } from "@t3tools/contracts/relay"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import { cast } from "effect/Function"; @@ -13,23 +12,32 @@ import { and, desc, eq, isNull } from "drizzle-orm"; import { RelayDb } from "../db.ts"; import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts"; -export class AgentActivityRowUpsertPersistenceError extends Data.TaggedError( +export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass()( "AgentActivityRowUpsertPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist agent activity state"; + } +} -export class AgentActivityRowDeletePersistenceError extends Data.TaggedError( +export class AgentActivityRowDeletePersistenceError extends Schema.TaggedErrorClass()( "AgentActivityRowDeletePersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to delete agent activity state"; + } +} -export class AgentActivityRowListPersistenceError extends Data.TaggedError( +export class AgentActivityRowListPersistenceError extends Schema.TaggedErrorClass()( "AgentActivityRowListPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to list agent activity state"; + } +} export interface AgentActivityRowsShape { readonly upsert: (input: { diff --git a/infra/relay/src/agentActivity/ApnsClient.ts b/infra/relay/src/agentActivity/ApnsClient.ts index 92ec060958d9..a779085118db 100644 --- a/infra/relay/src/agentActivity/ApnsClient.ts +++ b/infra/relay/src/agentActivity/ApnsClient.ts @@ -2,20 +2,13 @@ import * as NodeCrypto from "node:crypto"; import type { RelayAgentActivityAggregateState } from "@t3tools/contracts/relay"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Redacted from "effect/Redacted"; import * as Schema from "effect/Schema"; -import { - Headers, - HttpClient, - HttpClientRequest, - type HttpBody, - type HttpClientError, -} from "effect/unstable/http"; +import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http"; import type { ApnsCredentials } from "../Config.ts"; import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts"; @@ -45,18 +38,39 @@ export interface ApnsDeliveryResult { readonly apnsId: string | null; } -export class ApnsSigningError extends Data.TaggedError("ApnsSigningError")<{ - readonly phase: "encoding" | "signing"; - readonly cause: unknown; -}> {} +export class ApnsSigningError extends Schema.TaggedErrorClass()( + "ApnsSigningError", + { + phase: Schema.Literals(["encoding", "signing"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed during APNs JWT ${this.phase}`; + } +} -export class ApnsHttpRequestError extends Data.TaggedError("ApnsHttpRequestError")<{ - readonly cause: HttpClientError.HttpClientError | HttpBody.HttpBodyError; -}> {} +export class ApnsHttpRequestError extends Schema.TaggedErrorClass()( + "ApnsHttpRequestError", + { + cause: Schema.Defect(), + }, +) { + override get message(): string { + return "APNs HTTP request failed"; + } +} -export class ApnsInvalidResponseError extends Data.TaggedError("ApnsInvalidResponseError")<{ - readonly cause: unknown; -}> {} +export class ApnsInvalidResponseError extends Schema.TaggedErrorClass()( + "ApnsInvalidResponseError", + { + cause: Schema.Defect(), + }, +) { + override get message(): string { + return "APNs returned an invalid response"; + } +} export type ApnsError = ApnsSigningError | ApnsHttpRequestError | ApnsInvalidResponseError; diff --git a/infra/relay/src/agentActivity/ApnsDeliveries.ts b/infra/relay/src/agentActivity/ApnsDeliveries.ts index d6f61b375992..c1dba1467fac 100644 --- a/infra/relay/src/agentActivity/ApnsDeliveries.ts +++ b/infra/relay/src/agentActivity/ApnsDeliveries.ts @@ -9,7 +9,6 @@ import { RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema, } from "@t3tools/contracts/relay"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -74,9 +73,16 @@ export type ApnsDeliveryError = | LiveActivities.LiveActivityTargetListPersistenceError | LiveActivities.LiveActivityDeliveryMarkPersistenceError; -export class ApnsDeliveryJobClaimInFlight extends Data.TaggedError("ApnsDeliveryJobClaimInFlight")<{ - readonly sourceJobId: string; -}> {} +export class ApnsDeliveryJobClaimInFlight extends Schema.TaggedErrorClass()( + "ApnsDeliveryJobClaimInFlight", + { + sourceJobId: Schema.String, + }, +) { + override get message(): string { + return `APNs delivery job '${this.sourceJobId}' is already in flight`; + } +} const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption( Schema.fromJsonString(RelayAgentActivityAggregateStateSchema), diff --git a/infra/relay/src/agentActivity/ApnsDeliveryQueue.ts b/infra/relay/src/agentActivity/ApnsDeliveryQueue.ts index 219c0595293d..3582e236b4dc 100644 --- a/infra/relay/src/agentActivity/ApnsDeliveryQueue.ts +++ b/infra/relay/src/agentActivity/ApnsDeliveryQueue.ts @@ -2,10 +2,10 @@ import * as Alchemy from "alchemy"; import * as Cloudflare from "alchemy/Cloudflare"; import * as Crypto from "effect/Crypto"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; import type { RelayDeliveryResult } from "@t3tools/contracts/relay"; @@ -22,9 +22,14 @@ import { } from "./apnsDeliveryJobs.ts"; import * as RelayConfiguration from "../Config.ts"; -export class ApnsDeliveryQueueSendError extends Data.TaggedError("ApnsDeliveryQueueSendError")<{ - readonly cause: unknown; -}> {} +export class ApnsDeliveryQueueSendError extends Schema.TaggedErrorClass()( + "ApnsDeliveryQueueSendError", + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to enqueue APNs delivery"; + } +} export type ApnsDeliveryQueueError = ApnsDeliveryQueueSendError; diff --git a/infra/relay/src/agentActivity/DeliveryAttempts.ts b/infra/relay/src/agentActivity/DeliveryAttempts.ts index 52c58b84a83b..b88e5c82c516 100644 --- a/infra/relay/src/agentActivity/DeliveryAttempts.ts +++ b/infra/relay/src/agentActivity/DeliveryAttempts.ts @@ -1,20 +1,23 @@ import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import { and, eq, isNull } from "drizzle-orm"; import * as Crypto from "effect/Crypto"; +import * as Schema from "effect/Schema"; import { RelayDb } from "../db.ts"; import { relayDeliveryAttempts } from "../persistence/schema.ts"; -export class DeliveryAttemptRecordPersistenceError extends Data.TaggedError( +export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass()( "DeliveryAttemptRecordPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist APNs delivery attempt"; + } +} export interface DeliveryAttemptInput { readonly userId: string | null; diff --git a/infra/relay/src/agentActivity/Devices.ts b/infra/relay/src/agentActivity/Devices.ts index 417dba80dd0f..86c338b09125 100644 --- a/infra/relay/src/agentActivity/Devices.ts +++ b/infra/relay/src/agentActivity/Devices.ts @@ -3,31 +3,42 @@ import type { RelayDeviceRegistrationRequest, } from "@t3tools/contracts/relay"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; import { and, eq } from "drizzle-orm"; import { sql } from "drizzle-orm"; import { RelayDb } from "../db.ts"; import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts"; -export class DeviceRegistrationPersistenceError extends Data.TaggedError( +export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass()( "DeviceRegistrationPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist mobile device registration"; + } +} -export class DeviceUnregistrationPersistenceError extends Data.TaggedError( +export class DeviceUnregistrationPersistenceError extends Schema.TaggedErrorClass()( "DeviceUnregistrationPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to unregister mobile device"; + } +} -export class DeviceListPersistenceError extends Data.TaggedError("DeviceListPersistenceError")<{ - readonly cause: unknown; -}> {} +export class DeviceListPersistenceError extends Schema.TaggedErrorClass()( + "DeviceListPersistenceError", + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to list mobile devices"; + } +} export interface DevicesShape { readonly register: (input: { diff --git a/infra/relay/src/agentActivity/LiveActivities.ts b/infra/relay/src/agentActivity/LiveActivities.ts index d90e5695b7c0..e76499221241 100644 --- a/infra/relay/src/agentActivity/LiveActivities.ts +++ b/infra/relay/src/agentActivity/LiveActivities.ts @@ -5,7 +5,6 @@ import type { } from "@t3tools/contracts/relay"; import { RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema } from "@t3tools/contracts/relay"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import { cast } from "effect/Function"; @@ -16,23 +15,32 @@ import { and, eq, sql } from "drizzle-orm"; import { RelayDb } from "../db.ts"; import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts"; -export class LiveActivityRegistrationPersistenceError extends Data.TaggedError( +export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass()( "LiveActivityRegistrationPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist Live Activity registration"; + } +} -export class LiveActivityTargetListPersistenceError extends Data.TaggedError( +export class LiveActivityTargetListPersistenceError extends Schema.TaggedErrorClass()( "LiveActivityTargetListPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to list Live Activity delivery targets"; + } +} -export class LiveActivityDeliveryMarkPersistenceError extends Data.TaggedError( +export class LiveActivityDeliveryMarkPersistenceError extends Schema.TaggedErrorClass()( "LiveActivityDeliveryMarkPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist Live Activity delivery state"; + } +} export interface DeviceRow { readonly user_id: string; diff --git a/infra/relay/src/agentActivity/apnsDeliveryJobs.ts b/infra/relay/src/agentActivity/apnsDeliveryJobs.ts index 0de28197e0be..d509baa91685 100644 --- a/infra/relay/src/agentActivity/apnsDeliveryJobs.ts +++ b/infra/relay/src/agentActivity/apnsDeliveryJobs.ts @@ -2,7 +2,6 @@ import * as NodeCrypto from "node:crypto"; import { RelayAgentActivityAggregateState, type RelayDeliveryKind } from "@t3tools/contracts/relay"; import { stableStringify } from "@t3tools/shared/relaySigning"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Option from "effect/Option"; import * as Redacted from "effect/Redacted"; @@ -50,13 +49,23 @@ export const SignedApnsDeliveryJob = Schema.Struct({ }); export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type; -export class ApnsDeliveryJobInvalid extends Data.TaggedError("ApnsDeliveryJobInvalid")<{ - readonly message: string; -}> {} - -export class ApnsDeliveryJobExpired extends Data.TaggedError("ApnsDeliveryJobExpired")<{ - readonly expiresAt: string; -}> {} +export class ApnsDeliveryJobInvalid extends Schema.TaggedErrorClass()( + "ApnsDeliveryJobInvalid", + { + message: Schema.String, + }, +) {} + +export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass()( + "ApnsDeliveryJobExpired", + { + expiresAt: Schema.String, + }, +) { + override get message(): string { + return `APNs delivery job expired at ${this.expiresAt}`; + } +} export type ApnsDeliveryJobVerificationError = ApnsDeliveryJobInvalid | ApnsDeliveryJobExpired; diff --git a/infra/relay/src/auth/DpopProofs.ts b/infra/relay/src/auth/DpopProofs.ts index fb94d59ffd2a..cd59a984fa15 100644 --- a/infra/relay/src/auth/DpopProofs.ts +++ b/infra/relay/src/auth/DpopProofs.ts @@ -1,8 +1,8 @@ import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; import * as HttpApiError from "effect/unstable/httpapi/HttpApiError"; import { lt } from "drizzle-orm"; @@ -10,11 +10,16 @@ import { verifyDpopProof } from "@t3tools/shared/dpop"; import { RelayDb } from "../db.ts"; import { relayDpopProofs } from "../persistence/schema.ts"; -export class DpopProofReplayPersistenceError extends Data.TaggedError( +export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass()( "DpopProofReplayPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { + cause: Schema.Defect(), + }, +) { + override get message(): string { + return "Failed to persist DPoP proof replay state"; + } +} export interface DpopProofReplayShape { readonly verifyAndConsume: (input: { diff --git a/infra/relay/src/auth/RelayTokens.ts b/infra/relay/src/auth/RelayTokens.ts index b9c50bd2e81f..f7f02c49f8c5 100644 --- a/infra/relay/src/auth/RelayTokens.ts +++ b/infra/relay/src/auth/RelayTokens.ts @@ -161,7 +161,7 @@ const make = Effect.gen(function* () { } return claims; }), - Effect.catch(() => Effect.succeed(null)), + Effect.orElseSucceed(() => null), ), ); diff --git a/infra/relay/src/environments/EnvironmentConnector.test.ts b/infra/relay/src/environments/EnvironmentConnector.test.ts index 505d1f31d009..0ca7e16ab539 100644 --- a/infra/relay/src/environments/EnvironmentConnector.test.ts +++ b/infra/relay/src/environments/EnvironmentConnector.test.ts @@ -22,6 +22,7 @@ import * as Redacted from "effect/Redacted"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as TestClock from "effect/testing/TestClock"; +import * as Tracer from "effect/Tracer"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import * as EnvironmentLinks from "./EnvironmentLinks.ts"; @@ -50,6 +51,9 @@ const decodeHealthRequestBody = Schema.decodeUnknownSync( const decodeMintRequestBody = Schema.decodeUnknownSync( Schema.fromJsonString(RelayCloudMintCredentialRequest), ); +const isEnvironmentConnectNotAuthorized = Schema.is( + EnvironmentConnector.EnvironmentConnectNotAuthorized, +); function requestBodyText(request: HttpClientRequest.HttpClientRequest): string { return request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "{}"; @@ -280,7 +284,13 @@ describe("EnvironmentConnector", () => { expect(Result.isFailure(result)).toBe(true); if (Result.isFailure(result)) { - expect(result.failure).toBeInstanceOf(EnvironmentConnector.EnvironmentConnectNotAuthorized); + expect(isEnvironmentConnectNotAuthorized(result.failure)).toBe(true); + if (isEnvironmentConnectNotAuthorized(result.failure)) { + expect(result.failure).toMatchObject({ + operation: "status", + reason: "endpoint_provider_not_managed", + }); + } } expect(requestCount).toBe(0); }).pipe( @@ -300,6 +310,14 @@ describe("EnvironmentConnector", () => { it.effect("rejects stale managed endpoints before sending a mint request", () => { let requestCount = 0; + const spans: Array = []; + const tracer = Tracer.make({ + span: (options) => { + const span = new Tracer.NativeSpan(options); + spans.push(span); + return span; + }, + }); const execute = () => Effect.sync(() => { requestCount += 1; @@ -318,8 +336,27 @@ describe("EnvironmentConnector", () => { expect(Result.isFailure(result)).toBe(true); if (Result.isFailure(result)) { - expect(result.failure).toBeInstanceOf(EnvironmentConnector.EnvironmentConnectNotAuthorized); + expect(isEnvironmentConnectNotAuthorized(result.failure)).toBe(true); + if (isEnvironmentConnectNotAuthorized(result.failure)) { + expect(result.failure).toMatchObject({ + operation: "connect", + reason: "managed_endpoint_mismatch", + }); + } } + const resolutionSpan = spans.find( + (span) => span.name === "relay.environment_connector.resolve_managed_endpoint", + ); + expect(Object.fromEntries(resolutionSpan?.attributes ?? [])).toMatchObject({ + "relay.authorization.allocation_hostname": "env.example.test", + "relay.authorization.allocation_has_ready_at": true, + "relay.authorization.allocation_has_tunnel_id": true, + "relay.authorization.allocation_has_dns_record_id": true, + "relay.authorization.linked_http_base_url": "https://attacker.example.test/", + "relay.authorization.linked_ws_base_url": "wss://attacker.example.test/ws", + "relay.authorization.resolved_http_base_url": "https://env.example.test/", + "relay.authorization.resolved_ws_base_url": "wss://env.example.test/ws", + }); expect(requestCount).toBe(0); }).pipe( Effect.provide( @@ -333,6 +370,7 @@ describe("EnvironmentConnector", () => { }), }), ), + Effect.provideService(Tracer.Tracer, tracer), ); }); @@ -355,7 +393,13 @@ describe("EnvironmentConnector", () => { expect(Result.isFailure(result)).toBe(true); if (Result.isFailure(result)) { - expect(result.failure).toBeInstanceOf(EnvironmentConnector.EnvironmentConnectNotAuthorized); + expect(isEnvironmentConnectNotAuthorized(result.failure)).toBe(true); + if (isEnvironmentConnectNotAuthorized(result.failure)) { + expect(result.failure).toMatchObject({ + operation: "status", + reason: "managed_endpoint_allocation_not_ready", + }); + } } expect(requestCount).toBe(0); }).pipe( diff --git a/infra/relay/src/environments/EnvironmentConnector.ts b/infra/relay/src/environments/EnvironmentConnector.ts index d31cf499e44d..dac5c81f6b04 100644 --- a/infra/relay/src/environments/EnvironmentConnector.ts +++ b/infra/relay/src/environments/EnvironmentConnector.ts @@ -28,7 +28,6 @@ import { import { stableStringify } from "@t3tools/shared/relaySigning"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -41,29 +40,93 @@ import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import * as EnvironmentLinks from "./EnvironmentLinks.ts"; import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; import * as RelayConfiguration from "../Config.ts"; +import { isManagedEndpointHostname } from "../deploymentConfig.ts"; -export class EnvironmentConnectNotAuthorized extends Data.TaggedError( +export const EnvironmentConnectNotAuthorizedReason = Schema.Literals([ + "client_proof_key_thumbprint_missing", + "environment_link_not_found", + "endpoint_provider_not_managed", + "managed_endpoint_allocation_not_found", + "managed_endpoint_base_domain_not_configured", + "managed_endpoint_allocation_not_ready", + "managed_endpoint_hostname_invalid", + "managed_endpoint_mismatch", +]); +export type EnvironmentConnectNotAuthorizedReason = + typeof EnvironmentConnectNotAuthorizedReason.Type; + +function environmentConnectNotAuthorizedReasonMessage( + reason: EnvironmentConnectNotAuthorizedReason, +): string { + switch (reason) { + case "client_proof_key_thumbprint_missing": + return "the client proof key thumbprint is missing"; + case "environment_link_not_found": + return "no active environment link was found"; + case "endpoint_provider_not_managed": + return "the linked endpoint is not relay-managed"; + case "managed_endpoint_allocation_not_found": + return "no managed endpoint allocation was found"; + case "managed_endpoint_base_domain_not_configured": + return "the managed endpoint base domain is not configured"; + case "managed_endpoint_allocation_not_ready": + return "the managed endpoint allocation is incomplete"; + case "managed_endpoint_hostname_invalid": + return "the managed endpoint hostname is invalid"; + case "managed_endpoint_mismatch": + return "the linked endpoint does not match its managed allocation"; + } +} + +export class EnvironmentConnectNotAuthorized extends Schema.TaggedErrorClass()( "EnvironmentConnectNotAuthorized", -)<{ - readonly environmentId: string; -}> {} + { + environmentId: Schema.String, + operation: Schema.Literals(["connect", "status"]), + reason: EnvironmentConnectNotAuthorizedReason, + }, +) { + override get message(): string { + return `Environment '${this.environmentId}' is not authorized for ${this.operation}: ${environmentConnectNotAuthorizedReasonMessage(this.reason)}`; + } +} -export class EnvironmentMintRequestFailed extends Data.TaggedError("EnvironmentMintRequestFailed")<{ - readonly cause: unknown; -}> {} +export class EnvironmentMintRequestFailed extends Schema.TaggedErrorClass()( + "EnvironmentMintRequestFailed", + { + environmentId: Schema.String, + operation: Schema.Literals(["connect", "status"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Environment '${this.environmentId}' ${this.operation} request failed`; + } +} -export class EnvironmentMintRequestTimedOut extends Data.TaggedError( +export class EnvironmentMintRequestTimedOut extends Schema.TaggedErrorClass()( "EnvironmentMintRequestTimedOut", -)<{ - readonly environmentId: string; - readonly timeoutMs: number; -}> {} + { + environmentId: Schema.String, + timeoutMs: Schema.Number, + }, +) { + override get message(): string { + return `Environment '${this.environmentId}' mint request timed out after ${this.timeoutMs}ms`; + } +} -export class EnvironmentMintResponseInvalid extends Data.TaggedError( +export class EnvironmentMintResponseInvalid extends Schema.TaggedErrorClass()( "EnvironmentMintResponseInvalid", -)<{ - readonly environmentId: string; -}> {} + { + environmentId: Schema.String, + operation: Schema.Literals(["connect", "status"]), + }, +) { + override get message(): string { + return `Environment '${this.environmentId}' returned an invalid ${this.operation} response`; + } +} export type EnvironmentConnectorError = | EnvironmentConnectNotAuthorized @@ -233,30 +296,91 @@ const make = Effect.gen(function* () { const resolveManagedEndpoint = Effect.fn("relay.environment_connector.resolve_managed_endpoint")( function* (input: { readonly userId: string; + readonly operation: "connect" | "status"; readonly link: EnvironmentLinks.RelayLinkedEnvironmentRecord; }) { if (input.link.endpoint.providerKind !== "cloudflare_tunnel") { + yield* Effect.annotateCurrentSpan({ + "relay.authorization.endpoint_provider_kind": input.link.endpoint.providerKind, + }); return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.link.environmentId, + operation: input.operation, + reason: "endpoint_provider_not_managed", }); } const allocation = yield* allocations.get({ userId: input.userId, environmentId: input.link.environmentId, }); - const endpoint = allocation - ? ManagedEndpointAllocations.resolveReadyManagedEndpoint({ - allocation, - baseDomain: settings.managedEndpointBaseDomain, - }) - : null; + if (!allocation) { + return yield* new EnvironmentConnectNotAuthorized({ + environmentId: input.link.environmentId, + operation: input.operation, + reason: "managed_endpoint_allocation_not_found", + }); + } + const allocationAttributes = { + "relay.authorization.allocation_hostname": allocation.hostname, + "relay.authorization.allocation_has_ready_at": allocation.readyAt !== null, + "relay.authorization.allocation_has_tunnel_id": allocation.tunnelId !== null, + "relay.authorization.allocation_has_dns_record_id": allocation.dnsRecordId !== null, + } as const; + if (!settings.managedEndpointBaseDomain) { + yield* Effect.annotateCurrentSpan(allocationAttributes); + return yield* new EnvironmentConnectNotAuthorized({ + environmentId: input.link.environmentId, + operation: input.operation, + reason: "managed_endpoint_base_domain_not_configured", + }); + } + if ( + allocation.readyAt === null || + allocation.tunnelId === null || + allocation.dnsRecordId === null + ) { + yield* Effect.annotateCurrentSpan(allocationAttributes); + return yield* new EnvironmentConnectNotAuthorized({ + environmentId: input.link.environmentId, + operation: input.operation, + reason: "managed_endpoint_allocation_not_ready", + }); + } + if (!isManagedEndpointHostname(allocation.hostname, settings.managedEndpointBaseDomain)) { + yield* Effect.annotateCurrentSpan({ + ...allocationAttributes, + "relay.authorization.managed_endpoint_base_domain": settings.managedEndpointBaseDomain, + }); + return yield* new EnvironmentConnectNotAuthorized({ + environmentId: input.link.environmentId, + operation: input.operation, + reason: "managed_endpoint_hostname_invalid", + }); + } + const endpoint = ManagedEndpointAllocations.resolveReadyManagedEndpoint({ + allocation, + baseDomain: settings.managedEndpointBaseDomain, + }); if ( endpoint === null || endpoint.httpBaseUrl !== input.link.endpoint.httpBaseUrl || endpoint.wsBaseUrl !== input.link.endpoint.wsBaseUrl ) { + yield* Effect.annotateCurrentSpan({ + ...allocationAttributes, + "relay.authorization.linked_http_base_url": input.link.endpoint.httpBaseUrl, + "relay.authorization.linked_ws_base_url": input.link.endpoint.wsBaseUrl, + ...(endpoint + ? { + "relay.authorization.resolved_http_base_url": endpoint.httpBaseUrl, + "relay.authorization.resolved_ws_base_url": endpoint.wsBaseUrl, + } + : {}), + }); return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.link.environmentId, + operation: input.operation, + reason: "managed_endpoint_mismatch", }); } return endpoint; @@ -271,20 +395,42 @@ const make = Effect.gen(function* () { }); const link = yield* links.getForUser(input); if (!link) { - return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.environmentId }); + return yield* new EnvironmentConnectNotAuthorized({ + environmentId: input.environmentId, + operation: "status", + reason: "environment_link_not_found", + }); } - const endpoint = yield* resolveManagedEndpoint({ userId: input.userId, link }); + const endpoint = yield* resolveManagedEndpoint({ + userId: input.userId, + operation: "status", + link, + }); const now = yield* DateTime.now; const expiresAt = DateTime.add(now, { minutes: 2 }); const nonce = yield* crypto.randomUUIDv4.pipe( - Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })), + Effect.mapError( + (cause) => + new EnvironmentMintRequestFailed({ + environmentId: input.environmentId, + operation: "status", + cause, + }), + ), ); const payload = { iss: relayIssuer, aud: `t3-env:${link.environmentId}`, sub: input.userId, jti: yield* crypto.randomUUIDv4.pipe( - Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })), + Effect.mapError( + (cause) => + new EnvironmentMintRequestFailed({ + environmentId: input.environmentId, + operation: "status", + cause, + }), + ), ), iat: Math.floor(now.epochMilliseconds / 1_000), exp: Math.floor(expiresAt.epochMilliseconds / 1_000), @@ -296,7 +442,16 @@ const make = Effect.gen(function* () { privateKey: Redacted.value(settings.cloudMintPrivateKey), typ: RELAY_HEALTH_REQUEST_TYP, payload, - }).pipe(Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause }))); + }).pipe( + Effect.mapError( + (cause) => + new EnvironmentMintRequestFailed({ + environmentId: input.environmentId, + operation: "status", + cause, + }), + ), + ); const checkedAt = DateTime.formatIso(now); const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl); const responseOption = yield* environmentClient.cloud.health({ payload: { proof } }).pipe( @@ -336,7 +491,10 @@ const make = Effect.gen(function* () { now: yield* DateTime.now, }); if (!verified) { - return yield* new EnvironmentMintResponseInvalid({ environmentId: input.environmentId }); + return yield* new EnvironmentMintResponseInvalid({ + environmentId: input.environmentId, + operation: "status", + }); } return { environmentId: link.environmentId, @@ -354,24 +512,50 @@ const make = Effect.gen(function* () { ...(input.deviceId ? { "relay.mobile.device_id": input.deviceId } : {}), }); if (input.clientProofKeyThumbprint.trim().length === 0) { - return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.environmentId }); + return yield* new EnvironmentConnectNotAuthorized({ + environmentId: input.environmentId, + operation: "connect", + reason: "client_proof_key_thumbprint_missing", + }); } const link = yield* links.getForUser(input); if (!link) { - return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.environmentId }); + return yield* new EnvironmentConnectNotAuthorized({ + environmentId: input.environmentId, + operation: "connect", + reason: "environment_link_not_found", + }); } - const endpoint = yield* resolveManagedEndpoint({ userId: input.userId, link }); + const endpoint = yield* resolveManagedEndpoint({ + userId: input.userId, + operation: "connect", + link, + }); const now = yield* DateTime.now; const expiresAt = DateTime.add(now, { minutes: 2 }); const nonce = yield* crypto.randomUUIDv4.pipe( - Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })), + Effect.mapError( + (cause) => + new EnvironmentMintRequestFailed({ + environmentId: input.environmentId, + operation: "connect", + cause, + }), + ), ); const payload = { iss: relayIssuer, aud: `t3-env:${link.environmentId}`, sub: input.userId, jti: yield* crypto.randomUUIDv4.pipe( - Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })), + Effect.mapError( + (cause) => + new EnvironmentMintRequestFailed({ + environmentId: input.environmentId, + operation: "connect", + cause, + }), + ), ), iat: Math.floor(now.epochMilliseconds / 1_000), exp: Math.floor(expiresAt.epochMilliseconds / 1_000), @@ -386,11 +570,27 @@ const make = Effect.gen(function* () { privateKey: Redacted.value(settings.cloudMintPrivateKey), typ: RELAY_MINT_REQUEST_TYP, payload, - }).pipe(Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause }))); + }).pipe( + Effect.mapError( + (cause) => + new EnvironmentMintRequestFailed({ + environmentId: input.environmentId, + operation: "connect", + cause, + }), + ), + ); const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl); const decoded = yield* environmentClient.cloud.t3MintCredential({ payload: { proof } }).pipe( withoutRedirects, - Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })), + Effect.mapError( + (cause) => + new EnvironmentMintRequestFailed({ + environmentId: input.environmentId, + operation: "connect", + cause, + }), + ), Effect.timeoutOption(Duration.millis(ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS)), Effect.flatMap( Option.match({ @@ -415,7 +615,10 @@ const make = Effect.gen(function* () { nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000), }); if (!verified) { - return yield* new EnvironmentMintResponseInvalid({ environmentId: input.environmentId }); + return yield* new EnvironmentMintResponseInvalid({ + environmentId: input.environmentId, + operation: "connect", + }); } return { environmentId: link.environmentId, diff --git a/infra/relay/src/environments/EnvironmentCredentials.ts b/infra/relay/src/environments/EnvironmentCredentials.ts index 9acde2eef6c6..13ced74c77af 100644 --- a/infra/relay/src/environments/EnvironmentCredentials.ts +++ b/infra/relay/src/environments/EnvironmentCredentials.ts @@ -1,33 +1,42 @@ import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; import { and, eq, isNull, ne, notExists } from "drizzle-orm"; import { RelayDb } from "../db.ts"; import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts"; -export class EnvironmentCredentialCreatePersistenceError extends Data.TaggedError( +export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentCredentialCreatePersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist environment credential"; + } +} -export class EnvironmentCredentialAuthenticatePersistenceError extends Data.TaggedError( +export class EnvironmentCredentialAuthenticatePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentCredentialAuthenticatePersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to authenticate environment credential"; + } +} -export class EnvironmentCredentialRevokePersistenceError extends Data.TaggedError( +export class EnvironmentCredentialRevokePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentCredentialRevokePersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to revoke environment credential"; + } +} export interface EnvironmentCredentialPrincipal { readonly credentialId: string; diff --git a/infra/relay/src/environments/EnvironmentLinker.ts b/infra/relay/src/environments/EnvironmentLinker.ts index 853ea41cbdaf..5eb12181692b 100644 --- a/infra/relay/src/environments/EnvironmentLinker.ts +++ b/infra/relay/src/environments/EnvironmentLinker.ts @@ -1,6 +1,6 @@ import { RelayEnvironmentLinkProofPayload, - type RelayEnvironmentLinkProofInvalidReason, + RelayEnvironmentLinkProofInvalidReason, type RelayEnvironmentLinkRequest, } from "@t3tools/contracts/relay"; import { @@ -10,7 +10,6 @@ import { verifyRelayJwt, } from "@t3tools/shared/relayJwt"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -23,14 +22,28 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts"; import * as ManagedEndpointProvider from "./ManagedEndpointProvider.ts"; import * as RelayConfiguration from "../Config.ts"; -export class EnvironmentLinkProofExpired extends Data.TaggedError("EnvironmentLinkProofExpired")<{ - readonly expiresAt: string; -}> {} +export class EnvironmentLinkProofExpired extends Schema.TaggedErrorClass()( + "EnvironmentLinkProofExpired", + { + expiresAt: Schema.String, + }, +) { + override get message(): string { + return `Environment link proof expired at ${this.expiresAt}`; + } +} -export class EnvironmentLinkProofInvalid extends Data.TaggedError("EnvironmentLinkProofInvalid")<{ - readonly environmentId: string; - readonly reason: RelayEnvironmentLinkProofInvalidReason; -}> {} +export class EnvironmentLinkProofInvalid extends Schema.TaggedErrorClass()( + "EnvironmentLinkProofInvalid", + { + environmentId: Schema.String, + reason: RelayEnvironmentLinkProofInvalidReason, + }, +) { + override get message(): string { + return `Environment '${this.environmentId}' link proof is invalid: ${this.reason}`; + } +} export type EnvironmentLinkError = | EnvironmentLinkProofExpired diff --git a/infra/relay/src/environments/EnvironmentLinks.ts b/infra/relay/src/environments/EnvironmentLinks.ts index b1e28d0da0ad..9ed48c279055 100644 --- a/infra/relay/src/environments/EnvironmentLinks.ts +++ b/infra/relay/src/environments/EnvironmentLinks.ts @@ -5,10 +5,10 @@ import type { RelayManagedEndpoint, } from "@t3tools/contracts/relay"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; import { and, eq, isNull, or } from "drizzle-orm"; import { RelayDb } from "../db.ts"; @@ -24,41 +24,59 @@ export interface AgentAwarenessDeliveryUserRecord { readonly liveActivitiesEnabled: boolean; } -export class EnvironmentLinkUpsertPersistenceError extends Data.TaggedError( +export class EnvironmentLinkUpsertPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkUpsertPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist environment link"; + } +} -export class EnvironmentLinkUserListPersistenceError extends Data.TaggedError( +export class EnvironmentLinkUserListPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkUserListPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to list users linked to environment"; + } +} -export class EnvironmentPublicKeyListPersistenceError extends Data.TaggedError( +export class EnvironmentPublicKeyListPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentPublicKeyListPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to list environment public keys"; + } +} -export class EnvironmentLinkListPersistenceError extends Data.TaggedError( +export class EnvironmentLinkListPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkListPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to list environment links"; + } +} -export class EnvironmentLinkLookupPersistenceError extends Data.TaggedError( +export class EnvironmentLinkLookupPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkLookupPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to look up environment link"; + } +} -export class EnvironmentLinkRevokePersistenceError extends Data.TaggedError( +export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkRevokePersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to revoke environment link"; + } +} export interface EnvironmentLinksShape { readonly upsert: (input: { diff --git a/infra/relay/src/environments/EnvironmentPublishSignatures.ts b/infra/relay/src/environments/EnvironmentPublishSignatures.ts index cca694ac512a..4d2d316b2287 100644 --- a/infra/relay/src/environments/EnvironmentPublishSignatures.ts +++ b/infra/relay/src/environments/EnvironmentPublishSignatures.ts @@ -11,7 +11,6 @@ import { import { stableStringify } from "@t3tools/shared/relaySigning"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; @@ -21,23 +20,38 @@ import * as Schema from "effect/Schema"; import * as DpopProofs from "../auth/DpopProofs.ts"; import * as RelayConfiguration from "../Config.ts"; -export class EnvironmentPublishSignatureExpired extends Data.TaggedError( +export class EnvironmentPublishSignatureExpired extends Schema.TaggedErrorClass()( "EnvironmentPublishSignatureExpired", -)<{ - readonly expiresAt: string; -}> {} + { + expiresAt: Schema.String, + }, +) { + override get message(): string { + return `Environment publish signature expired at ${this.expiresAt}`; + } +} -export class EnvironmentPublishSignatureInvalid extends Data.TaggedError( +export class EnvironmentPublishSignatureInvalid extends Schema.TaggedErrorClass()( "EnvironmentPublishSignatureInvalid", -)<{ - readonly environmentId: string; -}> {} + { + environmentId: Schema.String, + }, +) { + override get message(): string { + return `Environment '${this.environmentId}' publish signature is invalid`; + } +} -export class EnvironmentPublishPublicKeyMissing extends Data.TaggedError( +export class EnvironmentPublishPublicKeyMissing extends Schema.TaggedErrorClass()( "EnvironmentPublishPublicKeyMissing", -)<{ - readonly environmentId: string; -}> {} + { + environmentId: Schema.String, + }, +) { + override get message(): string { + return `Environment '${this.environmentId}' has no publish public key`; + } +} export type EnvironmentPublishSignatureError = | EnvironmentPublishSignatureExpired diff --git a/infra/relay/src/environments/ManagedEndpointAllocations.ts b/infra/relay/src/environments/ManagedEndpointAllocations.ts index 236414e9552d..7809b43393ec 100644 --- a/infra/relay/src/environments/ManagedEndpointAllocations.ts +++ b/infra/relay/src/environments/ManagedEndpointAllocations.ts @@ -1,10 +1,10 @@ import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; import { and, eq } from "drizzle-orm"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; import { RelayDb } from "../db.ts"; import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts"; @@ -36,11 +36,17 @@ export function resolveReadyManagedEndpoint(input: { return managedEndpointForHostname(input.allocation.hostname); } -export class ManagedEndpointAllocationPersistenceError extends Data.TaggedError( +export class ManagedEndpointAllocationPersistenceError extends Schema.TaggedErrorClass()( "ManagedEndpointAllocationPersistenceError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Failed to persist managed endpoint allocation"; + } +} +const isManagedEndpointAllocationPersistenceError = Schema.is( + ManagedEndpointAllocationPersistenceError, +); interface ManagedEndpointAllocationKey { readonly userId: string; @@ -98,7 +104,7 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) => ); const persistenceError = (cause: unknown) => - cause instanceof ManagedEndpointAllocationPersistenceError + isManagedEndpointAllocationPersistenceError(cause) ? cause : new ManagedEndpointAllocationPersistenceError({ cause }); diff --git a/infra/relay/src/environments/ManagedEndpointProvider.ts b/infra/relay/src/environments/ManagedEndpointProvider.ts index 068beccff00d..bdbcc569dcb8 100644 --- a/infra/relay/src/environments/ManagedEndpointProvider.ts +++ b/infra/relay/src/environments/ManagedEndpointProvider.ts @@ -3,12 +3,12 @@ import * as Cloudflare from "alchemy/Cloudflare"; import * as Arr from "effect/Array"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; -import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Result from "effect/Result"; +import * as Schema from "effect/Schema"; import type { RelayManagedEndpoint, @@ -25,28 +25,44 @@ import { } from "../deploymentConfig.ts"; import { ManagedEndpointAllocations } from "./ManagedEndpointAllocations.ts"; -export class ManagedEndpointProvisioningNotConfigured extends Data.TaggedError( +export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass()( "ManagedEndpointProvisioningNotConfigured", -)<{}> {} + {}, +) { + override get message(): string { + return "Managed endpoint provisioning is not configured"; + } +} -export class ManagedEndpointProvisioningFailed extends Data.TaggedError( +export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass()( "ManagedEndpointProvisioningFailed", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Managed endpoint provisioning failed"; + } +} -export class ManagedEndpointDeprovisioningFailed extends Data.TaggedError( +export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass()( "ManagedEndpointDeprovisioningFailed", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Managed endpoint deprovisioning failed"; + } +} -export class ManagedEndpointOriginNotAllowed extends Data.TaggedError( +export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass()( "ManagedEndpointOriginNotAllowed", -)<{ - readonly host: string; - readonly port: number; -}> {} + { + host: Schema.String, + port: Schema.Number, + }, +) { + override get message(): string { + return `Managed endpoint origin '${this.host}:${this.port}' is not allowed`; + } +} export type ManagedEndpointProviderError = | ManagedEndpointProvisioningNotConfigured @@ -80,11 +96,14 @@ interface ManagedEndpointTunnel { readonly name?: string | null; } -export class ManagedEndpointTunnelClientError extends Data.TaggedError( +export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass()( "ManagedEndpointTunnelClientError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Managed endpoint tunnel provider request failed"; + } +} export interface ManagedEndpointTunnelClientShape { readonly list: (request: { @@ -124,11 +143,14 @@ interface ManagedEndpointCnameRecordInput { readonly proxied: true; } -export class ManagedEndpointDnsClientError extends Data.TaggedError( +export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass()( "ManagedEndpointDnsClientError", -)<{ - readonly cause: unknown; -}> {} + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Managed endpoint DNS provider request failed"; + } +} export interface ManagedEndpointDnsClientShape { readonly listRecords: ( @@ -242,7 +264,7 @@ const make = Effect.gen(function* () { .updateRecord(preferredDnsRecordId, dnsRecord) .pipe( Effect.as(true), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ); if (checkpointedRecordUpdated) { return preferredDnsRecordId; diff --git a/infra/relay/src/http/Api.ts b/infra/relay/src/http/Api.ts index 3a9e59a67193..45dde8d397f5 100644 --- a/infra/relay/src/http/Api.ts +++ b/infra/relay/src/http/Api.ts @@ -1,6 +1,5 @@ import { createClerkClient, verifyToken } from "@clerk/backend"; import { sql as drizzleSql } from "drizzle-orm"; -import * as Data from "effect/Data"; import * as Crypto from "effect/Crypto"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; @@ -813,9 +812,16 @@ export const serverApi = HttpApiBuilder.group( }), ); -class ClerkTokenVerificationFailed extends Data.TaggedError("ClerkTokenVerificationFailed")<{ - readonly cause: unknown; -}> {} +class ClerkTokenVerificationFailed extends Schema.TaggedErrorClass()( + "ClerkTokenVerificationFailed", + { + cause: Schema.Defect(), + }, +) { + override get message(): string { + return "Clerk token verification failed"; + } +} const isHttpUnauthorized = Schema.is(HttpApiError.Unauthorized); @@ -824,7 +830,7 @@ const currentTraceId = Effect.currentParentSpan.pipe( Effect.orElseSucceed(() => "unavailable"), ); -const COMMON_AUTH_INVALID_REASONS = [ +const RelayCommonPersistenceError = Schema.Union([ Devices.DeviceRegistrationPersistenceError, Devices.DeviceUnregistrationPersistenceError, Devices.DeviceListPersistenceError, @@ -844,18 +850,15 @@ const COMMON_AUTH_INVALID_REASONS = [ AgentActivityRows.AgentActivityRowListPersistenceError, LiveActivities.LiveActivityDeliveryMarkPersistenceError, DeliveryAttempts.DeliveryAttemptRecordPersistenceError, -] as const; -type RelayCommonPersistenceError = InstanceType<(typeof COMMON_AUTH_INVALID_REASONS)[number]>; +]); +type RelayCommonPersistenceError = typeof RelayCommonPersistenceError.Type; +const isRelayCommonPersistenceError = Schema.is(RelayCommonPersistenceError); type MapRelayCommonApiError = | Exclude | (Extract extends never ? never : RelayAuthInvalidError) | (Extract extends never ? never : RelayInternalError); -function isRelayCommonPersistenceError(error: unknown): error is RelayCommonPersistenceError { - return COMMON_AUTH_INVALID_REASONS.some((ErrorType) => error instanceof ErrorType); -} - function relayInternalErrorResponse(reason: RelayInternalError["reason"]) { return currentTraceId.pipe( Effect.flatMap((traceId) => diff --git a/infra/relay/src/observability.test.ts b/infra/relay/src/observability.test.ts new file mode 100644 index 000000000000..ff543672f7f0 --- /dev/null +++ b/infra/relay/src/observability.test.ts @@ -0,0 +1,85 @@ +import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; +import { expect, it } from "@effect/vitest"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as Schema from "effect/Schema"; +import * as HttpServer from "effect/unstable/http/HttpServer"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import type { OtlpTracer } from "effect/unstable/observability"; + +import { EnvironmentConnectNotAuthorized } from "./environments/EnvironmentConnector.ts"; +import { makeRelayTraceLayer } from "./observability.ts"; + +interface ExportedRequest { + readonly authorization: string | undefined; + readonly body: string; + readonly dataset: string | undefined; +} + +const otlpAttributeValue = (value: { + readonly stringValue?: string | null; + readonly boolValue?: boolean | null; + readonly intValue?: number | null; + readonly doubleValue?: number | null; +}) => value.stringValue ?? value.boolValue ?? value.intValue ?? value.doubleValue; + +const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString); + +it.effect("exports schema error fields as span attributes", () => + Effect.gen(function* () { + const exportedRequest = yield* Deferred.make(); + yield* HttpServer.serveEffect( + Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + yield* Deferred.succeed(exportedRequest, { + authorization: request.headers.authorization, + body: yield* request.text, + dataset: request.headers["x-axiom-dataset"], + }); + return HttpServerResponse.empty({ status: 204 }); + }), + ); + + yield* Effect.fail( + new EnvironmentConnectNotAuthorized({ + environmentId: "environment-1", + operation: "connect", + reason: "managed_endpoint_allocation_not_ready", + }), + ).pipe( + Effect.withSpan("relay.test.schema_error"), + Effect.exit, + Effect.provide( + makeRelayTraceLayer({ + tracesEndpoint: "/v1/traces", + tracesDatasetName: "relay-test-traces", + ingestToken: Redacted.make("test-token"), + }), + ), + ); + + const request = yield* Deferred.await(exportedRequest).pipe(Effect.timeout("1 second")); + const payload = (yield* decodeJson(request.body)) as OtlpTracer.TraceData; + const span = payload.resourceSpans + .flatMap((resourceSpan) => resourceSpan.scopeSpans) + .flatMap((scopeSpan) => scopeSpan.spans) + .find((candidate) => candidate.name === "relay.test.schema_error"); + const attributes = Object.fromEntries( + (span?.attributes ?? []).map((attribute) => [ + attribute.key, + otlpAttributeValue(attribute.value), + ]), + ); + + expect(request.authorization).toBe("Bearer test-token"); + expect(request.dataset).toBe("relay-test-traces"); + expect(attributes).toMatchObject({ + "error.type": "EnvironmentConnectNotAuthorized", + "error.environmentId": "environment-1", + "error.operation": "connect", + "error.reason": "managed_endpoint_allocation_not_ready", + }); + }).pipe(Effect.provide(NodeHttpServer.layerTest), Effect.scoped), +); diff --git a/infra/relay/src/observability.ts b/infra/relay/src/observability.ts index b54567d1f0af..1e5c651ec9e4 100644 --- a/infra/relay/src/observability.ts +++ b/infra/relay/src/observability.ts @@ -1,9 +1,14 @@ import * as Alchemy from "alchemy"; import * as Axiom from "alchemy/Axiom"; import * as Output from "alchemy/Output"; -import * as Layer from "effect/Layer"; +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 Redacted from "effect/Redacted"; +import * as Schema from "effect/Schema"; +import * as Tracer from "effect/Tracer"; import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; import { relayResourceNameForStage } from "./deploymentConfig.ts"; @@ -54,23 +59,163 @@ export const withSpanAttributes = Effect.andThen(effect.pipe(Effect.annotateSpans(attributes))), ); +const appendEncodedAttributes = ( + attributes: Record, + prefix: string, + value: unknown, +): void => { + if ( + value === null || + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" || + typeof value === "bigint" || + Array.isArray(value) + ) { + attributes[prefix] = value; + return; + } + if (typeof value !== "object") { + return; + } + for (const [key, child] of Object.entries(value)) { + appendEncodedAttributes(attributes, `${prefix}.${key}`, child); + } +}; + +const schemaErrorAttributes = (error: unknown): Record | undefined => { + if (typeof error !== "object" || error === null) { + return undefined; + } + const constructor = error.constructor; + if (!Schema.isSchema(constructor)) { + return undefined; + } + const encoded = Schema.encodeUnknownOption(constructor as unknown as Schema.Encoder)( + error, + ); + if (Option.isNone(encoded) || typeof encoded.value !== "object" || encoded.value === null) { + return undefined; + } + const tag = Reflect.get(encoded.value, "_tag"); + if (typeof tag !== "string") { + return undefined; + } + + const attributes: Record = { + "error.type": tag, + }; + for (const [key, value] of Object.entries(encoded.value)) { + if (key !== "_tag") { + appendEncodedAttributes(attributes, `error.${key}`, value); + } + } + return attributes; +}; + +const annotateSchemaError = (span: Tracer.Span, exit: Exit.Exit): void => { + if (Exit.isSuccess(exit)) { + return; + } + for (const reason of exit.cause.reasons) { + const error = Cause.isFailReason(reason) + ? reason.error + : Cause.isDieReason(reason) + ? reason.defect + : undefined; + const attributes = schemaErrorAttributes(error); + if (attributes) { + for (const [key, value] of Object.entries(attributes)) { + span.attribute(key, value); + } + return; + } + } +}; + +class RelayTraceSpan implements Tracer.Span { + readonly _tag = "Span"; + private readonly delegate: Tracer.Span; + + constructor(delegate: Tracer.Span) { + this.delegate = delegate; + } + + get name() { + return this.delegate.name; + } + get spanId() { + return this.delegate.spanId; + } + get traceId() { + return this.delegate.traceId; + } + get parent() { + return this.delegate.parent; + } + get annotations() { + return this.delegate.annotations; + } + get status() { + return this.delegate.status; + } + get attributes() { + return this.delegate.attributes; + } + get links() { + return this.delegate.links; + } + get sampled() { + return this.delegate.sampled; + } + get kind() { + return this.delegate.kind; + } + + end(endTime: bigint, exit: Exit.Exit): void { + annotateSchemaError(this.delegate, exit); + this.delegate.end(endTime, exit); + } + + attribute(key: string, value: unknown): void { + this.delegate.attribute(key, value); + } + + event(name: string, startTime: bigint, attributes?: Record): void { + this.delegate.event(name, startTime, attributes); + } + + addLinks(links: ReadonlyArray): void { + this.delegate.addLinks(links); + } +} + +const withSchemaErrorAttributes = (delegate: Tracer.Tracer): Tracer.Tracer => + Tracer.make({ + span: (options) => new RelayTraceSpan(delegate.span(options)), + ...(delegate.context ? { context: delegate.context } : {}), + }); + export const makeRelayTraceLayer = (input: { readonly tracesEndpoint: string; readonly tracesDatasetName: string; readonly ingestToken: Redacted.Redacted; }) => - OtlpTracer.layer({ - url: input.tracesEndpoint, - resource: { - serviceName: "t3-code-relay-worker", - attributes: { - "service.runtime": "cloudflare-worker", - "service.component": "relay", + Layer.effect( + Tracer.Tracer, + OtlpTracer.make({ + url: input.tracesEndpoint, + resource: { + serviceName: "t3-code-relay-worker", + attributes: { + "service.runtime": "cloudflare-worker", + "service.component": "relay", + }, + }, + headers: { + Authorization: `Bearer ${Redacted.value(input.ingestToken)}`, + "X-Axiom-Dataset": input.tracesDatasetName, }, - }, - headers: { - Authorization: `Bearer ${Redacted.value(input.ingestToken)}`, - "X-Axiom-Dataset": input.tracesDatasetName, - }, - exportInterval: "1 second", - }).pipe(Layer.provide(OtlpSerialization.layerJson)); + exportInterval: "1 second", + }).pipe(Effect.map(withSchemaErrorAttributes)), + ).pipe(Layer.provide(OtlpSerialization.layerJson)); diff --git a/oxlint-plugin-t3code/package.json b/oxlint-plugin-t3code/package.json index 4c71d04094dc..08c7c54b9f9a 100644 --- a/oxlint-plugin-t3code/package.json +++ b/oxlint-plugin-t3code/package.json @@ -13,7 +13,6 @@ }, "devDependencies": { "@effect/vitest": "catalog:", - "@types/bun": "catalog:", "vite-plus": "catalog:" } } diff --git a/oxlint-plugin-t3code/tsconfig.json b/oxlint-plugin-t3code/tsconfig.json index 266b534fef00..61307d2992e0 100644 --- a/oxlint-plugin-t3code/tsconfig.json +++ b/oxlint-plugin-t3code/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "composite": true, - "types": ["bun", "node"], + "types": ["node"], "lib": ["ESNext", "esnext.disposable"] }, "include": ["**/*.ts"] diff --git a/package.json b/package.json index 6b2fde910e31..e7235ab6b6c0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build:marketing": "vp run --filter @t3tools/marketing build", "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build", "typecheck": "vp run -r --concurrency-limit 2 typecheck", + "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", @@ -38,7 +39,6 @@ "sync:repos": "node scripts/sync-reference-repos.ts" }, "dependencies": { - "@t3tools/monorepo": "." }, "devDependencies": { "@babel/plugin-transform-react-jsx": "7.28.6", diff --git a/packages/contracts/src/providerRuntime.ts b/packages/contracts/src/providerRuntime.ts index 5032dc4eb415..eb2563eff004 100644 --- a/packages/contracts/src/providerRuntime.ts +++ b/packages/contracts/src/providerRuntime.ts @@ -241,6 +241,7 @@ const ModelReroutedType = Schema.Literal("model.rerouted"); const ConfigWarningType = Schema.Literal("config.warning"); const DeprecationNoticeType = Schema.Literal("deprecation.notice"); const FilesPersistedType = Schema.Literal("files.persisted"); +const ToolDeniedType = Schema.Literal("tool.denied"); const RuntimeWarningType = Schema.Literal("runtime.warning"); const RuntimeErrorType = Schema.Literal("runtime.error"); @@ -589,6 +590,14 @@ const FilesPersistedPayload = Schema.Struct({ }); export type FilesPersistedPayload = typeof FilesPersistedPayload.Type; +const ToolDeniedPayload = Schema.Struct({ + toolName: TrimmedNonEmptyStringSchema, + toolUseId: Schema.optional(TrimmedNonEmptyStringSchema), + reason: Schema.optional(TrimmedNonEmptyStringSchema), + agentId: Schema.optional(TrimmedNonEmptyStringSchema), +}); +export type ToolDeniedPayload = typeof ToolDeniedPayload.Type; + const RuntimeWarningPayload = Schema.Struct({ message: TrimmedNonEmptyStringSchema, detail: Schema.optional(Schema.Unknown), @@ -934,6 +943,13 @@ const ProviderRuntimeFilesPersistedEvent = Schema.Struct({ }); export type ProviderRuntimeFilesPersistedEvent = typeof ProviderRuntimeFilesPersistedEvent.Type; +const ProviderRuntimeToolDeniedEvent = Schema.Struct({ + ...ProviderRuntimeEventBase.fields, + type: ToolDeniedType, + payload: ToolDeniedPayload, +}); +export type ProviderRuntimeToolDeniedEvent = typeof ProviderRuntimeToolDeniedEvent.Type; + const ProviderRuntimeWarningEvent = Schema.Struct({ ...ProviderRuntimeEventBase.fields, type: RuntimeWarningType, @@ -994,6 +1010,7 @@ export const ProviderRuntimeEventV2 = Schema.Union([ ProviderRuntimeConfigWarningEvent, ProviderRuntimeDeprecationNoticeEvent, ProviderRuntimeFilesPersistedEvent, + ProviderRuntimeToolDeniedEvent, ProviderRuntimeWarningEvent, ProviderRuntimeErrorEvent, ]); diff --git a/packages/contracts/src/relay.ts b/packages/contracts/src/relay.ts index 11b30ac3eeef..eaec8314fdf9 100644 --- a/packages/contracts/src/relay.ts +++ b/packages/contracts/src/relay.ts @@ -329,7 +329,11 @@ export class RelayAuthInvalidError extends Schema.TaggedErrorClass()( "RelayEnvironmentLinkProofExpiredError", @@ -338,7 +342,11 @@ export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorCla traceId: TrimmedNonEmptyString, }, { httpApiStatus: 401 }, -) {} +) { + override get message(): string { + return "Relay environment link proof expired"; + } +} export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorClass()( "RelayEnvironmentLinkProofInvalidError", @@ -348,7 +356,11 @@ export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorCla traceId: TrimmedNonEmptyString, }, { httpApiStatus: 400 }, -) {} +) { + override get message(): string { + return `Relay environment link proof is invalid: ${this.reason}`; + } +} export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErrorClass()( "RelayEnvironmentConnectNotAuthorizedError", @@ -357,7 +369,11 @@ export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErro traceId: TrimmedNonEmptyString, }, { httpApiStatus: 403 }, -) {} +) { + override get message(): string { + return "Relay environment connection is not authorized"; + } +} export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedErrorClass()( "RelayEnvironmentEndpointUnavailableError", @@ -367,7 +383,11 @@ export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedError traceId: TrimmedNonEmptyString, }, { httpApiStatus: 502 }, -) {} +) { + override get message(): string { + return `Relay environment endpoint is unavailable: ${this.reason}`; + } +} export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorClass()( "RelayEnvironmentEndpointTimedOutError", @@ -376,7 +396,11 @@ export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorCla traceId: TrimmedNonEmptyString, }, { httpApiStatus: 504 }, -) {} +) { + override get message(): string { + return "Relay environment endpoint request timed out"; + } +} export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass()( "RelayEnvironmentLinkFailedError", @@ -386,7 +410,11 @@ export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass()( "RelayEnvironmentLinkUnavailableError", @@ -396,7 +424,11 @@ export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClas traceId: TrimmedNonEmptyString, }, { httpApiStatus: 503 }, -) {} +) { + override get message(): string { + return `Relay environment link is unavailable: ${this.reason}`; + } +} export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErrorClass()( "RelayAgentActivityPublishProofExpiredError", @@ -405,7 +437,11 @@ export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErr traceId: TrimmedNonEmptyString, }, { httpApiStatus: 401 }, -) {} +) { + override get message(): string { + return "Relay agent activity publish proof expired"; + } +} export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErrorClass()( "RelayAgentActivityPublishProofInvalidError", @@ -415,7 +451,11 @@ export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErr traceId: TrimmedNonEmptyString, }, { httpApiStatus: 401 }, -) {} +) { + override get message(): string { + return `Relay agent activity publish proof is invalid: ${this.reason}`; + } +} export class RelayInternalError extends Schema.TaggedErrorClass()( "RelayInternalError", @@ -425,7 +465,11 @@ export class RelayInternalError extends Schema.TaggedErrorClass false, onSome: (httpResponse) => httpResponse.status >= 200 && httpResponse.status < 300, }); - }).pipe(Effect.catch(() => Effect.succeed(false))); + }).pipe(Effect.orElseSucceed(() => false)); export const resolveTailscaleHttpsBaseUrl = ( input: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 393fd95dedf7..686a725883e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,8 +10,8 @@ catalogs: specifier: 4.0.0-beta.78 version: 4.0.0-beta.78 '@effect/tsgo': - specifier: 0.11.4 - version: 0.11.4 + specifier: 0.13.2 + version: 0.13.2 '@noble/curves': specifier: 1.9.1 version: 1.9.1 @@ -21,12 +21,9 @@ catalogs: '@pierre/diffs': specifier: 1.1.20 version: 1.1.20 - '@types/bun': - specifier: ^1.3.11 - version: 1.3.14 '@typescript/native-preview': - specifier: 7.0.0-dev.20260527.2 - version: 7.0.0-dev.20260527.2 + specifier: 7.0.0-dev.20260604.1 + version: 7.0.0-dev.20260604.1 jose: specifier: 6.2.2 version: 6.2.2 @@ -87,7 +84,7 @@ importers: version: 7.28.6(@babel/core@7.29.7) '@effect/tsgo': specifier: 'catalog:' - version: 0.11.4 + version: 0.13.2 '@oxlint/plugins': specifier: ^1.63.0 version: 1.68.0 @@ -96,7 +93,7 @@ importers: version: 24.12.4 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260527.2 + version: 7.0.0-dev.20260604.1 vite-plus: specifier: 'catalog:' version: 0.1.24(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) @@ -417,7 +414,7 @@ importers: specifier: workspace:* version: link:../web '@types/bun': - specifier: 'catalog:' + specifier: 1.3.14 version: 1.3.14 '@types/node': specifier: 24.12.4 @@ -644,9 +641,6 @@ importers: '@effect/vitest': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(patch_hash=42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f)(effect@4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754)) - '@types/bun': - specifier: 'catalog:' - version: 1.3.14 vite-plus: specifier: 'catalog:' version: 0.1.24(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) @@ -807,9 +801,6 @@ importers: scripts: dependencies: - '@anthropic-ai/claude-agent-sdk': - specifier: ^0.2.77 - version: 0.2.141(zod@4.4.3) '@effect/platform-node': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(bufferutil@4.1.0)(effect@4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754))(ioredis@5.11.0)(utf-8-validate@6.0.6) @@ -829,9 +820,6 @@ importers: '@effect/vitest': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(patch_hash=42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f)(effect@4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754)) - '@types/bun': - specifier: 'catalog:' - version: 1.3.14 vite-plus: specifier: 'catalog:' version: 0.1.24(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) @@ -851,92 +839,46 @@ packages: '@alchemy.run/node-utils@0.0.4': resolution: {integrity: sha512-TiIhPXCTCi3tk0zmdYJJ14CNSesSfsJxXdIOP0HTSItQ1mZWLocrF7qCuEWKyW/IEFzp6kaiOf19aIA/mbCp1g==} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.141': - resolution: {integrity: sha512-9HZ0ot6+FwOfQ1aeMqQLH4IJGMm/DcP08SysDxscVjBm6l2JjqleHohxi3zid0DurfGweqT+4x9GScJffwg55g==} - cpu: [arm64] - os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.159': resolution: {integrity: sha512-3nnH4yUNJVSyaU5DBlGw2yxc4zlnVvAnc9UOe+La47QVG7/dN+rWAgn4zCbqKk9bWFLDQ1Ek0r56EZE1Qo4UKQ==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.141': - resolution: {integrity: sha512-4iAdarJaQ+2R58s6QJswZCzUdz2WQmL5lYG7Y+FLzWbRSROFfcH0QYpmOqSaPXd2KRQhIJwEacqecDZd/Q1XKQ==} - cpu: [x64] - os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.159': resolution: {integrity: sha512-iv+NRjz+t4Q1R2+kLdDbccSo3b0wedVJ9jMT3noznOVojZHzgkxVpTvt36/XkXV0rqIDQ5H18bBYIZZzOXu7mg==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.141': - resolution: {integrity: sha512-6H1AJ/AVaWNnV22kubUPkOTRzZFH0+qP9k7WlhriHMN9gtgZcVAsITMddDeGjQsQJMCAdhXFd6sgi7TM1LdeOQ==} - cpu: [arm64] - os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.159': resolution: {integrity: sha512-WvwiQBWt3tdu5EwqjpDZszI6p2uetYsw4Cxc6ptO/SmLIYXcDienP8nmirZdsZrS+Gzk6imgY0IY5mmNaRhelQ==} cpu: [arm64] os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.141': - resolution: {integrity: sha512-Jdf0ZEwJzOP8sE6rPqdJN+SxMb0/L8sxJg4twCv/7S+Qzk0hJtls+wxSi+0Tjh6EEMaNxJqEGc7S3fx99Wi99Q==} - cpu: [arm64] - os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.159': resolution: {integrity: sha512-FlsS5M4GCpzsQVaNDFF8dRgFGR3QwyAHZFl/xM/2Y2BqVBH+NH17RpKQSJxr1qr41QnsNkinMnu2iSKoc33hKg==} cpu: [arm64] os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.141': - resolution: {integrity: sha512-fTI1YuM4cxOa4nSgsyMAdB5ELizkWp+w5Ispo4JnnYtcczMAL4D9GBNjWPW0sUzKvjsJOUVim68SmWLWhUOpXQ==} - cpu: [x64] - os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.159': resolution: {integrity: sha512-kFH6RC2YJbPc8XWRNy/wL4YU7LzdJjSwAdH488sVzIif3q+TrvVrV5y/IW0+MLmta+CKIqtFYpGaucsJYvj7Eg==} cpu: [x64] os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.141': - resolution: {integrity: sha512-DVjp72f3HmrRYpbneWZZWIqkUht5kTZXS7wXGFiwzLz6eNYEgjjh+GcsnhIi8UOwZUtNiKUrjZnoP38ovFqV8A==} - cpu: [x64] - os: [linux] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.159': resolution: {integrity: sha512-uNPEC/iRzVb4bEdzs0KAz1zV7i1PVGEZZnJTQyi1OtgVa81sAoH/H0CbbzDiTsquKdaESf+1DSSEkUlfZmMUEw==} cpu: [x64] os: [linux] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.141': - resolution: {integrity: sha512-Wm10J6kfbufbPGFELokiJ/7Y5Oqug4Uag3HXFsV8g7TWCpaItx/oqVaJoiGptuAtXQB7xGLQVTuk082wER+Y5w==} - cpu: [arm64] - os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.159': resolution: {integrity: sha512-WN1QEZGgWXz9GMl61QU6j9E+LEF5plki87bL2xsGwuCPzK+OeVPQU55pabuP8P+vFBFHUo3Y9OlTVyZHnUzmAQ==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.141': - resolution: {integrity: sha512-IXuP29YJuWbR5Q6xOHrjFVGG54V2s1FC61UVNwEN5fpxL09MwPnbwtQL6fqgzt/U1MP7vWAwpXZriYAklkH/mg==} - cpu: [x64] - os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.159': resolution: {integrity: sha512-Ty4seccD+dTDX5hhj89IUELZd/LkxO5O43Uiz5Mo8ZJktoX38SK4XMZlBS935QdqTFLRvPL0hvK4Lt4dTOqzPw==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.2.141': - resolution: {integrity: sha512-AIBacMWGcZIUcXlUoObqjwJ6pmJI3BayAqPAFXuvSq3DHJXdiuZVs7l/zTB5l3nRhRv5cqSrI2XbiDeHgZWizw==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^4.0.0 - '@anthropic-ai/claude-agent-sdk@0.3.159': resolution: {integrity: sha512-Xh1oVMIK6N3KsiNIhqNH8ZK90zjRmAEL9d1Md8ZlGdHJE+HhdMYdBadujc3KEkV0uufsEUvYp+A3fDenfypGSA==} engines: {node: '>=18.0.0'} @@ -1776,43 +1718,43 @@ packages: peerDependencies: effect: 4.0.0-beta.78 - '@effect/tsgo-darwin-arm64@0.11.4': - resolution: {integrity: sha512-rslT4W8tWY8pdd48kDpY7Q+yo0xkrmcEySG1+sJfb06w7TwbywjHsGlC0BmcUaQt0qmLY+BApbWWDfZw7fgwjw==} + '@effect/tsgo-darwin-arm64@0.13.2': + resolution: {integrity: sha512-AlYlxU2sD3urNCmNCQWtatPJS/+m2r3CvgZdDTs9KEz2YcyQc2GV1g2RWFkoMwmgzYBx69DBLu1oscqJO2qAog==} cpu: [arm64] os: [darwin] - '@effect/tsgo-darwin-x64@0.11.4': - resolution: {integrity: sha512-p3v3j+zp7L7yx5ctjvA1W85I4WjOpbOcga8kUu5MFEKfYfsApxUHXwsunCVbUKwrpXqcTc0sw9AjnCNs6i6GRw==} + '@effect/tsgo-darwin-x64@0.13.2': + resolution: {integrity: sha512-oGWIUsWuzCGBRaxB3R4ZjzmqaWb20OtLpBc8HKf3mGstyiCpGUtb7arqncpSq04nN9yb+mM5yBpGKZ9EJAYuEA==} cpu: [x64] os: [darwin] - '@effect/tsgo-linux-arm64@0.11.4': - resolution: {integrity: sha512-dz5ZIQUnRzN9mpVcELYCmVERpmstes5rVHbKUb7DOWCbrbupT3tV1LCzOaNX9sIsoBKmpvdGvAWEPLHgymSgTQ==} + '@effect/tsgo-linux-arm64@0.13.2': + resolution: {integrity: sha512-v9PIgdLR6jREQk42EIm/mtFBwmB4Xzuq70l1Uf/7MnV/eivrZP+Wjfex/UuzMRbjM5eLRP3FFMj2HZvjkFQv6Q==} cpu: [arm64] os: [linux] - '@effect/tsgo-linux-arm@0.11.4': - resolution: {integrity: sha512-uXsw+IbO+ltn/fbhOWWqXK8ZW3Qa5EZD+wnGbF3gIGWINthbF+/7X+CgRzvcgLBWeBfV6F83hAmOaEZ4hQmd+w==} + '@effect/tsgo-linux-arm@0.13.2': + resolution: {integrity: sha512-y5aTN7MrwnsStcSJ4Z8J93Nszeh9yf8kbaHk+03bRm9n0TrhVea/bJZoi9cprLJSy01Umt+xC3jFyvOSMrWB8g==} cpu: [arm] os: [linux] - '@effect/tsgo-linux-x64@0.11.4': - resolution: {integrity: sha512-P3m7Y4FJtziL13Muffyb99AnQeTRvo0/knfYRqoHKNwYUaNM59RO3qFi5y57Hasgd8rKat0xTPmUkK6koZPPFQ==} + '@effect/tsgo-linux-x64@0.13.2': + resolution: {integrity: sha512-MzrApKfCL0DnFGvoyJUKtW39PGDykJZkkB/tOZSuTSurcYAzkmUIOUwuTASeNqDjj4/DixT7Ha4CMVeCQgg6hg==} cpu: [x64] os: [linux] - '@effect/tsgo-win32-arm64@0.11.4': - resolution: {integrity: sha512-FKLm9Y+luZSdLpfkbdw8VXa8xy0zR+Asg078qemqHHGwRQLDVGjvIk7tj1HxBM6VQN+TwsofDd225ht83kMk8w==} + '@effect/tsgo-win32-arm64@0.13.2': + resolution: {integrity: sha512-WwYLSrgCa4iiSIZe5KkG2P1q+JmIloDI6tbqHqpvumrz3uU8fg33e5A7wQ68sRgr4ofqrTYPQhJ6b+Eyeadazg==} cpu: [arm64] os: [win32] - '@effect/tsgo-win32-x64@0.11.4': - resolution: {integrity: sha512-oc4Y8TbLlv0b1deAILAEep1N6pnSS1d4pk7gJJM4NDXrTKWDyEulO6tXwRQ6vWkO2lhLTUVWHDFfzmMJj3HxPQ==} + '@effect/tsgo-win32-x64@0.13.2': + resolution: {integrity: sha512-M4Cf98tlCzCWig61QNoPxhE0LyMJtlVIQY2hed0Y1uYpwv0W1BMyOACLcj6ll+vuLWmXvVlOfXC+BuDOqPo9fg==} cpu: [x64] os: [win32] - '@effect/tsgo@0.11.4': - resolution: {integrity: sha512-wliq5Dis2gLoIUKoCRT3Iv5kpO+fKrUBJPPxIsHADeirk9HMQevKvYaBpGS3Khw1tNLmoQWsSX559owJAdHgBA==} + '@effect/tsgo@0.13.2': + resolution: {integrity: sha512-zPQMHBdCN1k7Jbxjxq0HVAbzsZd+LAoRPzBEZ3F4y/uxNK0K/XGvFXUUw/CUAsD6rhUtC2VahZY1tp0owb5lgg==} hasBin: true '@effect/vitest@4.0.0-beta.78': @@ -4505,50 +4447,50 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-3LqSu4DlxkEfeC/Z/29QMCJn5jjkDtXI7LYuxfmjdmAatS6umDKqm8J17fnP/7fyrZUMBTIYRwSDpChGV3G1ew==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-zs616um9UuaODLsNlCu5Aw95rFcTV4u3hVt090r6k0lVvTxfaJOv8HKA6BpIotcEYlZlMQowrMSYCCdedo7iyA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-H4+sxE9qaBbLF83wMdWE0FsgfK0Pom+/O+/oxqyGzhVkDJlNt3vfpgQZMit48/Gm44AacGfBggJ9Dhbi3aeSFw==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-pOdNAf2pwc9JBjo8gUsvLs5uqg7d0AhYXfE8/3zvPKBlIZG+mTcvEWW1hPawlWxXzf/vxnP4dgYwUHAMDghhKA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-BGUDMjC2Z3TTdZRkGGwhBLelkP5UYgO2rbep8aF4dS3fu7T5lFPPrnfS6EgqJgie+cF5Fsev7xEq8wWyBDM+lg==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-GjIrt6YHP3bbOWBCCE08SlBSDf84Lnjn3Td822/lOX9nm6ODlA/HI7rtGh7KzS/fxehep2Vy4dXU4Il12X1s5A==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-6I9Cv9ozwfS9zB9vRQDPIYseLX3artEO9jl3yVgLj4ishwlSF4cWAbIsjl5IztPaEgHv8coej/6tX1D0uaBzXg==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-IKaZL3i5HKmKqwb2IZEXW1j68fVg1HsvAaXkbrkOIG/J5Eyksu5tEnTzucrIY1oPdzgHT+y2HpDIYp2sGeHFvw==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-vpazOu+ozlxBo8U57YJMzsOPuxAV8H7fu36KJ8ea8At/D8pdGmOAy5TuB+9OBQV9JDe0OXJMy2kmbhOpmkTAmA==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-twQ7XDjsmaHIevN1MjeRYIVLUPL0fBm8A0jg1FhGYPckhTxEBiHIpJf9E//eFidAdrEHjeTSBP1jJw3GNAensg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-DBFnFE3V6AITkPO1K1VxXf3yEZKjU2FwtXlNwRqhzDu0rrL2SsJHOSrBDX+OacTxQFzZMxFcpiuhV8jHZALPEg==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-QBRxaVT3SFiNfOhwYb/56ddpHWPMFdfiJ4zkFJIjaAXeZ/ssWNHM9lH7yR++GrE9VTsUZ4eVSZfOmQbzRERhgw==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-1tBlErMvQgcMqqYwsx4tytupcjCJcOUXD3vBn1Wb/kAvus1FzWQAFE0fcKBvLfcqLQfTiiEwKKEtbLjGmakqqg==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-hR7YHoRpm88Q86gK6/IMayWUA+ROdHGzOPKK8EBp1xD/Cg2Bh5AXbut8HcyUDx/bcGQvnuht/XsW47bT3to9mg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260527.2': - resolution: {integrity: sha512-piqkDwikVeizCFqA1lcwI5F4wOAtBdxuliWe77ApBNRyBPPvfCJB+u/HYi9/8t5nd0sWvFs6/qt/AzJ1CCoykQ==} + '@typescript/native-preview@7.0.0-dev.20260604.1': + resolution: {integrity: sha512-A3/9yZTt2V5NlDURcVJ4mN2YjfeQTXCRyLuENKrNdGhO+y59mC/2UDr7UvpB3Li+83TRAuhDN8SBoM+7gkHdzQ==} engines: {node: '>=16.20.0'} hasBin: true @@ -9698,72 +9640,30 @@ snapshots: '@alchemy.run/node-utils@0.0.4': {} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.141': - optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.159': optional: true - '@anthropic-ai/claude-agent-sdk@0.2.141(zod@4.4.3)': - dependencies: - '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - zod: 4.4.3 - optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.2.141 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.2.141 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.2.141 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.2.141 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.2.141 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.2.141 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.2.141 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.2.141 - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - '@anthropic-ai/claude-agent-sdk@0.3.159(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) @@ -10931,36 +10831,36 @@ snapshots: dependencies: effect: 4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754) - '@effect/tsgo-darwin-arm64@0.11.4': + '@effect/tsgo-darwin-arm64@0.13.2': optional: true - '@effect/tsgo-darwin-x64@0.11.4': + '@effect/tsgo-darwin-x64@0.13.2': optional: true - '@effect/tsgo-linux-arm64@0.11.4': + '@effect/tsgo-linux-arm64@0.13.2': optional: true - '@effect/tsgo-linux-arm@0.11.4': + '@effect/tsgo-linux-arm@0.13.2': optional: true - '@effect/tsgo-linux-x64@0.11.4': + '@effect/tsgo-linux-x64@0.13.2': optional: true - '@effect/tsgo-win32-arm64@0.11.4': + '@effect/tsgo-win32-arm64@0.13.2': optional: true - '@effect/tsgo-win32-x64@0.11.4': + '@effect/tsgo-win32-x64@0.13.2': optional: true - '@effect/tsgo@0.11.4': + '@effect/tsgo@0.13.2': optionalDependencies: - '@effect/tsgo-darwin-arm64': 0.11.4 - '@effect/tsgo-darwin-x64': 0.11.4 - '@effect/tsgo-linux-arm': 0.11.4 - '@effect/tsgo-linux-arm64': 0.11.4 - '@effect/tsgo-linux-x64': 0.11.4 - '@effect/tsgo-win32-arm64': 0.11.4 - '@effect/tsgo-win32-x64': 0.11.4 + '@effect/tsgo-darwin-arm64': 0.13.2 + '@effect/tsgo-darwin-x64': 0.13.2 + '@effect/tsgo-linux-arm': 0.13.2 + '@effect/tsgo-linux-arm64': 0.13.2 + '@effect/tsgo-linux-x64': 0.13.2 + '@effect/tsgo-win32-arm64': 0.13.2 + '@effect/tsgo-win32-x64': 0.13.2 '@effect/vitest@4.0.0-beta.78(patch_hash=42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f)(effect@4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754))': dependencies: @@ -13581,36 +13481,36 @@ snapshots: '@types/node': 24.12.4 optional: true - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260527.2': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260604.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260527.2': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260604.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260527.2': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260604.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260527.2': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260604.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260527.2': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260604.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260527.2': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260604.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260527.2': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260604.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260527.2': + '@typescript/native-preview@7.0.0-dev.20260604.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260527.2 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260527.2 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260527.2 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260527.2 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260527.2 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260527.2 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260527.2 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260604.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260604.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260604.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260604.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260604.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260604.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260604.1 '@ungap/structured-clone@1.3.1': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ac001acc9ff0..ccd810fdbf7f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,16 +25,14 @@ catalog: "@effect/sql-pg": 4.0.0-beta.78 "@effect/sql-sqlite-bun": 4.0.0-beta.78 "@effect/vitest": 4.0.0-beta.78 - "@effect/tsgo": 0.11.4 + "@effect/tsgo": 0.13.2 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 "@pierre/diffs": 1.1.20 "@vitest/runner": 4.1.8 - "@types/bun": ^1.3.11 "@types/node": 24.12.4 - "@typescript/native-preview": 7.0.0-dev.20260527.2 + "@typescript/native-preview": 7.0.0-dev.20260604.1 jose: 6.2.2 - tsdown: ^0.20.3 typescript: ~6.0.3 vitest: npm:@voidzero-dev/vite-plus-test@0.1.24 vite: npm:@voidzero-dev/vite-plus-core@0.1.24 diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 0ee2e38880ef..4d63a11dbb05 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -178,13 +178,11 @@ const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRo cwd: repoRoot, }), ).pipe( - Effect.catch(() => - Effect.succeed({ - stdout: "", - stderr: "", - exitCode: 1, - }), - ), + Effect.orElseSucceed(() => ({ + stdout: "", + stderr: "", + exitCode: 1, + })), ); if (result.exitCode !== 0) { @@ -220,13 +218,11 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* ( const probe = yield* spawnAndCollectOutput( ChildProcess.make("python", ["-c", "import sys;print(sys.executable)"]), ).pipe( - Effect.catch(() => - Effect.succeed({ - stdout: "", - stderr: "", - exitCode: 1, - }), - ), + Effect.orElseSucceed(() => ({ + stdout: "", + stderr: "", + exitCode: 1, + })), ); if (probe.exitCode !== 0) { @@ -1008,7 +1004,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const copiedArtifacts: string[] = []; for (const entry of stageEntries) { const from = path.join(stageDistDir, entry); - const stat = yield* fs.stat(from).pipe(Effect.catch(() => Effect.succeed(null))); + const stat = yield* fs.stat(from).pipe(Effect.orElseSucceed(() => null)); if (!stat || stat.type !== "File") continue; const to = path.join(options.outputDir, entry); diff --git a/scripts/mobile-native-static-check.ts b/scripts/mobile-native-static-check.ts index ca871fd8a179..2f29034b42c3 100644 --- a/scripts/mobile-native-static-check.ts +++ b/scripts/mobile-native-static-check.ts @@ -73,7 +73,7 @@ const commandExists = Effect.fn("commandExists")(function* (command: string) { return yield* spawner.spawn(lookupCommand).pipe( Effect.flatMap((child) => child.exitCode), Effect.map((exitCode) => exitCode === 0), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ); }); diff --git a/scripts/package.json b/scripts/package.json index 7a01e5e34191..510bb230b8c9 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -7,7 +7,6 @@ "test": "vp test run" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.77", "@effect/platform-node": "catalog:", "@t3tools/contracts": "workspace:*", "@t3tools/shared": "workspace:*", @@ -16,7 +15,6 @@ }, "devDependencies": { "@effect/vitest": "catalog:", - "@types/bun": "catalog:", "vite-plus": "catalog:" } }