From 97188bb75fa21b85edf9b4929aabb82c49371306 Mon Sep 17 00:00:00 2001 From: Erik Thorelli Date: Mon, 5 Oct 2026 21:22:41 -0700 Subject: [PATCH 1/6] chore(deps): upgrade @effect/tsgo to 0.46.1 0.46.1 is the version Effect itself uses. It reports unknown rule names, so `importFromBarrel`, which Effect TSGo never ported, goes. It also extends `nodeBuiltinImport` to `crypto`, `timers`, and `stream`. Effect's Crypto covers only random values and ids, so each flagged import keeps Node's API behind a next-line opt-out that names what it needs. --- apps/desktop/src/electron/ElectronProtocol.ts | 1 + apps/desktop/src/preview/Manager.ts | 1 + apps/server/src/assets/AttachmentUpload.ts | 1 + .../src/assets/NativeAppIconResolver.ts | 1 + apps/server/src/auth/ReusableDevAuth.ts | 1 + apps/server/src/auth/utils.ts | 1 + apps/server/src/cli/app.ts | 1 + apps/server/src/cloud/CloudLink.ts | 1 + apps/server/src/cloud/environmentKeys.ts | 1 + apps/server/src/device/AgentDeviceTarget.ts | 1 + apps/server/src/device/SshDeviceHost.ts | 1 + .../src/htmlRender/PreviewBrowser.test.ts | 1 + apps/server/src/mcp/McpHttpServer.ts | 1 + .../Adapters/ClaudeAdapterV2.ts | 1 + .../src/orchestration-v2/CheckpointService.ts | 1 + apps/server/src/preview/Manager.ts | 1 + .../src/project/ProjectSetupScriptRunner.ts | 1 + .../provider/AntigravityInstallation.test.ts | 1 + .../src/provider/CodexInstallation.test.ts | 1 + .../src/provider/CodexToolPresentation.ts | 1 + .../src/provider/OpenCodeProvider.test.ts | 1 + .../src/provider/ProviderCredentialStore.ts | 1 + .../acp/AcpRegistryAuthenticationState.ts | 1 + .../provider/acp/AcpRegistrySupport.test.ts | 1 + .../src/provider/acp/AcpRegistrySupport.ts | 1 + .../src/provider/openCodeUsageLimits.ts | 1 + .../opencode2/OpenCode2Client.live.test.ts | 1 + .../src/pullRequest/GitHubPullRequestCli.ts | 1 + .../ScheduledTaskService.webhook.test.ts | 1 + .../src/scheduledTasks/webhookRoute.test.ts | 1 + .../webhookVerification.test.ts | 1 + .../src/scheduledTasks/webhookVerification.ts | 1 + apps/server/src/secrets/SecretRequests.ts | 1 + apps/server/src/telemetry/Identify.test.ts | 1 + apps/server/src/usage/cliproxyApi.ts | 1 + apps/server/src/vcs/GitVcsDriver.ts | 1 + apps/server/src/ws.ts | 1 + apps/server/src/zipArchive.ts | 1 + .../src/agentActivity/ApnsClient.test.ts | 1 + .../src/agentActivity/ApnsDeliveries.test.ts | 1 + .../agentActivity/ApnsProviderTokens.test.ts | 1 + .../relay/src/agentActivity/FcmClient.test.ts | 1 + .../src/agentActivity/apnsDeliveryJobs.ts | 1 + infra/relay/src/agentActivity/apnsJwt.ts | 1 + .../auth/DpopProofs.verifyAndConsume.test.ts | 1 + infra/relay/src/auth/RelayTokens.test.ts | 1 + .../environments/EnvironmentConnector.test.ts | 1 + .../environments/EnvironmentLinker.test.ts | 1 + .../EnvironmentPublishSignatures.test.ts | 1 + .../ManagedEndpointProvider.test.ts | 1 + infra/relay/src/hooks/HookForwarder.test.ts | 1 + infra/relay/src/http/Api.test.ts | 1 + packages/shared/src/desktopBootstrapToken.ts | 1 + packages/shared/src/dpop.test.ts | 1 + packages/shared/src/relayJwt.test.ts | 1 + packages/ssh/src/command.ts | 1 + pnpm-lock.yaml | 68 +++++++++---------- pnpm-workspace.yaml | 2 +- tsconfig.base.json | 1 - 59 files changed, 91 insertions(+), 36 deletions(-) diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index 6ac3488917b9..fdb3da89b59b 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -3,6 +3,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- fetchWithTransientRetry is plain async code, outside Effect. import * as NodeTimersPromises from "node:timers/promises"; import * as Path from "effect/Path"; import * as Mime from "effect/http/Mime"; diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index 3593ae5f5df6..d6535f11735b 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -5,6 +5,7 @@ * elements live in the renderer; we only attach listeners and forward state * here). Single layer-scoped browser session partition. */ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import { DesktopPreviewRecordingInputSchema, diff --git a/apps/server/src/assets/AttachmentUpload.ts b/apps/server/src/assets/AttachmentUpload.ts index a6e83d8853c8..2d5d6d179312 100644 --- a/apps/server/src/assets/AttachmentUpload.ts +++ b/apps/server/src/assets/AttachmentUpload.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import { diff --git a/apps/server/src/assets/NativeAppIconResolver.ts b/apps/server/src/assets/NativeAppIconResolver.ts index b883f625d33d..6bc8bd0170ef 100644 --- a/apps/server/src/assets/NativeAppIconResolver.ts +++ b/apps/server/src/assets/NativeAppIconResolver.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import type { ToolActivityNativeAppReference } from "@t3tools/contracts"; import * as Cache from "effect/Cache"; diff --git a/apps/server/src/auth/ReusableDevAuth.ts b/apps/server/src/auth/ReusableDevAuth.ts index 5b7d59db8da2..b96a369b63fb 100644 --- a/apps/server/src/auth/ReusableDevAuth.ts +++ b/apps/server/src/auth/ReusableDevAuth.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash or timingSafeEqual. import * as NodeCrypto from "node:crypto"; import { AuthSessionId } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts index 88315aebbf2a..a05f67f12b08 100644 --- a/apps/server/src/auth/utils.ts +++ b/apps/server/src/auth/utils.ts @@ -4,6 +4,7 @@ import type { AuthClientPresentationMetadata, } from "@t3tools/contracts"; import type * as HttpServerRequest from "effect/http/HttpServerRequest"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash, createHmac, or timingSafeEqual. import * as NodeCrypto from "node:crypto"; import * as Base64Url from "effect/encoding/Base64Url"; import * as Result from "effect/Result"; diff --git a/apps/server/src/cli/app.ts b/apps/server/src/cli/app.ts index 35fe0489f52f..f2fbcfa46f2a 100644 --- a/apps/server/src/cli/app.ts +++ b/apps/server/src/cli/app.ts @@ -1,4 +1,5 @@ // @effect-diagnostics globalTimers:off -- The Node socket client owns its response deadline and clears it on every completion path. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeNet from "node:net"; import * as NodeOS from "node:os"; diff --git a/apps/server/src/cloud/CloudLink.ts b/apps/server/src/cloud/CloudLink.ts index 7289a3d814e8..1436e98c37cf 100644 --- a/apps/server/src/cloud/CloudLink.ts +++ b/apps/server/src/cloud/CloudLink.ts @@ -4,6 +4,7 @@ * and mint requests, and keeping the managed tunnel registered, recovered and * released. HTTP handlers, server startup and shutdown all go through it. */ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createPublicKey. import * as NodeCrypto from "node:crypto"; import { AuthStandardClientScopes, diff --git a/apps/server/src/cloud/environmentKeys.ts b/apps/server/src/cloud/environmentKeys.ts index 1d0cde91bf4f..f829f93e8026 100644 --- a/apps/server/src/cloud/environmentKeys.ts +++ b/apps/server/src/cloud/environmentKeys.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; diff --git a/apps/server/src/device/AgentDeviceTarget.ts b/apps/server/src/device/AgentDeviceTarget.ts index 379649b1257c..81981fe7d0a3 100644 --- a/apps/server/src/device/AgentDeviceTarget.ts +++ b/apps/server/src/device/AgentDeviceTarget.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as Schema from "effect/Schema"; import * as Effect from "effect/Effect"; diff --git a/apps/server/src/device/SshDeviceHost.ts b/apps/server/src/device/SshDeviceHost.ts index f9ad91087c3b..b75a26e93e51 100644 --- a/apps/server/src/device/SshDeviceHost.ts +++ b/apps/server/src/device/SshDeviceHost.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import { type DeviceHostSummary, diff --git a/apps/server/src/htmlRender/PreviewBrowser.test.ts b/apps/server/src/htmlRender/PreviewBrowser.test.ts index 15eaaa2bab38..01140007092c 100644 --- a/apps/server/src/htmlRender/PreviewBrowser.test.ts +++ b/apps/server/src/htmlRender/PreviewBrowser.test.ts @@ -12,6 +12,7 @@ import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as NodeZlib from "node:zlib"; diff --git a/apps/server/src/mcp/McpHttpServer.ts b/apps/server/src/mcp/McpHttpServer.ts index 377956eb620b..bfa8cfad38f1 100644 --- a/apps/server/src/mcp/McpHttpServer.ts +++ b/apps/server/src/mcp/McpHttpServer.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 9aa6163f2adb..4f2f6cbd5296 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; diff --git a/apps/server/src/orchestration-v2/CheckpointService.ts b/apps/server/src/orchestration-v2/CheckpointService.ts index 10e12150c5a3..4734e00b4ca4 100644 --- a/apps/server/src/orchestration-v2/CheckpointService.ts +++ b/apps/server/src/orchestration-v2/CheckpointService.ts @@ -9,6 +9,7 @@ import { RunId, ThreadId, } from "@t3tools/contracts"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; diff --git a/apps/server/src/preview/Manager.ts b/apps/server/src/preview/Manager.ts index a5f5cba9ccd0..30c7ac86827b 100644 --- a/apps/server/src/preview/Manager.ts +++ b/apps/server/src/preview/Manager.ts @@ -31,6 +31,7 @@ import { newPreviewTabId, normalizePreviewUrl, } from "@t3tools/shared/preview"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; diff --git a/apps/server/src/project/ProjectSetupScriptRunner.ts b/apps/server/src/project/ProjectSetupScriptRunner.ts index 956733ba8506..41fe20a5fd3c 100644 --- a/apps/server/src/project/ProjectSetupScriptRunner.ts +++ b/apps/server/src/project/ProjectSetupScriptRunner.ts @@ -5,6 +5,7 @@ import { resolveProjectScripts, setupProjectScript, } from "@t3tools/shared/projectScripts"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as Clock from "effect/Clock"; diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index be0fa4f00b16..a96d67106575 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -21,6 +21,7 @@ import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as AntigravityInstallation from "./AntigravityInstallation.ts"; diff --git a/apps/server/src/provider/CodexInstallation.test.ts b/apps/server/src/provider/CodexInstallation.test.ts index a00319e22625..398bc6e02f17 100644 --- a/apps/server/src/provider/CodexInstallation.test.ts +++ b/apps/server/src/provider/CodexInstallation.test.ts @@ -14,6 +14,7 @@ import * as Exit from "effect/Exit"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/http"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as CodexInstallation from "./CodexInstallation.ts"; diff --git a/apps/server/src/provider/CodexToolPresentation.ts b/apps/server/src/provider/CodexToolPresentation.ts index a01390582bda..3b1f321a5549 100644 --- a/apps/server/src/provider/CodexToolPresentation.ts +++ b/apps/server/src/provider/CodexToolPresentation.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import type { ToolActivityIcon, diff --git a/apps/server/src/provider/OpenCodeProvider.test.ts b/apps/server/src/provider/OpenCodeProvider.test.ts index a7cfb54502df..7089fb1cbf89 100644 --- a/apps/server/src/provider/OpenCodeProvider.test.ts +++ b/apps/server/src/provider/OpenCodeProvider.test.ts @@ -1,4 +1,5 @@ import * as NodeAssert from "node:assert/strict"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; diff --git a/apps/server/src/provider/ProviderCredentialStore.ts b/apps/server/src/provider/ProviderCredentialStore.ts index 259b441654c9..37fb4b3f1214 100644 --- a/apps/server/src/provider/ProviderCredentialStore.ts +++ b/apps/server/src/provider/ProviderCredentialStore.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as Effect from "effect/Effect"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; diff --git a/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts b/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts index b38fcfef54e3..1fea4723eee4 100644 --- a/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts +++ b/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import type { AcpRegistrySettings, diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.test.ts b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts index 47e2ef7fe179..5b36936efe11 100644 --- a/apps/server/src/provider/acp/AcpRegistrySupport.test.ts +++ b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts @@ -16,6 +16,7 @@ import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as TestClock from "effect/testing/TestClock"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.ts b/apps/server/src/provider/acp/AcpRegistrySupport.ts index 0b02f733babb..25b5df12e01d 100644 --- a/apps/server/src/provider/acp/AcpRegistrySupport.ts +++ b/apps/server/src/provider/acp/AcpRegistrySupport.ts @@ -37,6 +37,7 @@ import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import { collectUint8StreamText } from "../../stream/collectUint8StreamText.ts"; diff --git a/apps/server/src/provider/openCodeUsageLimits.ts b/apps/server/src/provider/openCodeUsageLimits.ts index 194f7be5c2fb..044730972882 100644 --- a/apps/server/src/provider/openCodeUsageLimits.ts +++ b/apps/server/src/provider/openCodeUsageLimits.ts @@ -1,4 +1,5 @@ import * as NodeOS from "node:os"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import type { ServerProviderUsageWindow } from "@t3tools/contracts"; diff --git a/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts b/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts index 95da430a6e2f..d78e8b2b1018 100644 --- a/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts +++ b/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts @@ -21,6 +21,7 @@ import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; import { FetchHttpClient } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no randomBytes. import * as NodeCrypto from "node:crypto"; import { describe } from "vite-plus/test"; diff --git a/apps/server/src/pullRequest/GitHubPullRequestCli.ts b/apps/server/src/pullRequest/GitHubPullRequestCli.ts index 25c379228457..4f7460616892 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestCli.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestCli.ts @@ -4,6 +4,7 @@ import { runGitHubStackAction, type GitHubStackActionError } from "./githubStack import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Clock from "effect/Clock"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; diff --git a/apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts b/apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts index 310baf135b8f..feb8461fa984 100644 --- a/apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts +++ b/apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac. import * as NodeCrypto from "node:crypto"; import * as NodePlatformCrypto from "@effect/platform-node/NodeCrypto"; diff --git a/apps/server/src/scheduledTasks/webhookRoute.test.ts b/apps/server/src/scheduledTasks/webhookRoute.test.ts index 965c58da3b43..d8b20e73510e 100644 --- a/apps/server/src/scheduledTasks/webhookRoute.test.ts +++ b/apps/server/src/scheduledTasks/webhookRoute.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; diff --git a/apps/server/src/scheduledTasks/webhookVerification.test.ts b/apps/server/src/scheduledTasks/webhookVerification.test.ts index de0bf869c400..b5c2b676084c 100644 --- a/apps/server/src/scheduledTasks/webhookVerification.test.ts +++ b/apps/server/src/scheduledTasks/webhookVerification.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac. import * as NodeCrypto from "node:crypto"; import { assert, describe, it } from "@effect/vitest"; diff --git a/apps/server/src/scheduledTasks/webhookVerification.ts b/apps/server/src/scheduledTasks/webhookVerification.ts index 6bda529e4834..60f067796410 100644 --- a/apps/server/src/scheduledTasks/webhookVerification.ts +++ b/apps/server/src/scheduledTasks/webhookVerification.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash, createHmac, or timingSafeEqual. import * as NodeCrypto from "node:crypto"; import type { ScheduledTaskWebhookSignature } from "@t3tools/contracts"; diff --git a/apps/server/src/secrets/SecretRequests.ts b/apps/server/src/secrets/SecretRequests.ts index 4693e29b12f1..c20b120880c9 100644 --- a/apps/server/src/secrets/SecretRequests.ts +++ b/apps/server/src/secrets/SecretRequests.ts @@ -16,6 +16,7 @@ import { type SecretRequestAnswerInput, type ThreadId, } from "@t3tools/contracts"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac. import * as NodeCrypto from "node:crypto"; import * as Clock from "effect/Clock"; diff --git a/apps/server/src/telemetry/Identify.test.ts b/apps/server/src/telemetry/Identify.test.ts index c4240be18345..1ce7b88abf94 100644 --- a/apps/server/src/telemetry/Identify.test.ts +++ b/apps/server/src/telemetry/Identify.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; diff --git a/apps/server/src/usage/cliproxyApi.ts b/apps/server/src/usage/cliproxyApi.ts index 8bfb66c6e379..821fd96d4ddc 100644 --- a/apps/server/src/usage/cliproxyApi.ts +++ b/apps/server/src/usage/cliproxyApi.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import { diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 4d9390b159f9..497bea9958fa 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeBuffer from "node:buffer"; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 96844f8cfd79..1c6b7f64dbe5 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1,6 +1,7 @@ import { OrchestrationDispatchCommandError } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as DateTime from "effect/DateTime"; diff --git a/apps/server/src/zipArchive.ts b/apps/server/src/zipArchive.ts index cc1692543242..cd0d688ce14c 100644 --- a/apps/server/src/zipArchive.ts +++ b/apps/server/src/zipArchive.ts @@ -1,4 +1,5 @@ import * as Effect from "effect/Effect"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- type-only, to adapt a Node Readable into Effect. import type * as NodeStream from "node:stream"; import * as Yauzl from "yauzl"; diff --git a/infra/relay/src/agentActivity/ApnsClient.test.ts b/infra/relay/src/agentActivity/ApnsClient.test.ts index aede4106157f..e23e1ba3b032 100644 --- a/infra/relay/src/agentActivity/ApnsClient.test.ts +++ b/infra/relay/src/agentActivity/ApnsClient.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; diff --git a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts index c7f76d26c72f..0fd81a5cd87b 100644 --- a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts +++ b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts @@ -4,6 +4,7 @@ import type { } from "@t3tools/contracts/relay"; import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto"; import { describe, expect, it } from "@effect/vitest"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; diff --git a/infra/relay/src/agentActivity/ApnsProviderTokens.test.ts b/infra/relay/src/agentActivity/ApnsProviderTokens.test.ts index d98b9f920bed..d3bbc11ef16b 100644 --- a/infra/relay/src/agentActivity/ApnsProviderTokens.test.ts +++ b/infra/relay/src/agentActivity/ApnsProviderTokens.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import { describe, expect, it } from "@effect/vitest"; diff --git a/infra/relay/src/agentActivity/FcmClient.test.ts b/infra/relay/src/agentActivity/FcmClient.test.ts index b40ba0abb25f..d03b18b9b54b 100644 --- a/infra/relay/src/agentActivity/FcmClient.test.ts +++ b/infra/relay/src/agentActivity/FcmClient.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or verify. import * as NodeCrypto from "node:crypto"; import { describe, expect, it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; diff --git a/infra/relay/src/agentActivity/apnsDeliveryJobs.ts b/infra/relay/src/agentActivity/apnsDeliveryJobs.ts index 96c60072a961..9b7e9579a5f1 100644 --- a/infra/relay/src/agentActivity/apnsDeliveryJobs.ts +++ b/infra/relay/src/agentActivity/apnsDeliveryJobs.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. import * as NodeCrypto from "node:crypto"; import { diff --git a/infra/relay/src/agentActivity/apnsJwt.ts b/infra/relay/src/agentActivity/apnsJwt.ts index 8ca65b73fb3b..98cb714660a1 100644 --- a/infra/relay/src/agentActivity/apnsJwt.ts +++ b/infra/relay/src/agentActivity/apnsJwt.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash or createPrivateKey. import * as NodeCrypto from "node:crypto"; import { p256 } from "@noble/curves/nist"; diff --git a/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts b/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts index fc83df40691e..564030cd0549 100644 --- a/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts +++ b/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. import * as NodeCrypto from "node:crypto"; import { describe, expect, it } from "@effect/vitest"; diff --git a/infra/relay/src/auth/RelayTokens.test.ts b/infra/relay/src/auth/RelayTokens.test.ts index c4a65771e584..9b3a26b8dfe2 100644 --- a/infra/relay/src/auth/RelayTokens.test.ts +++ b/infra/relay/src/auth/RelayTokens.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import { describe, expect, it } from "@effect/vitest"; diff --git a/infra/relay/src/environments/EnvironmentConnector.test.ts b/infra/relay/src/environments/EnvironmentConnector.test.ts index 71170196e942..d3fbf6c57b0f 100644 --- a/infra/relay/src/environments/EnvironmentConnector.test.ts +++ b/infra/relay/src/environments/EnvironmentConnector.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. import * as NodeCrypto from "node:crypto"; import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto"; diff --git a/infra/relay/src/environments/EnvironmentLinker.test.ts b/infra/relay/src/environments/EnvironmentLinker.test.ts index 5705cb78e89c..e4f81a76fca4 100644 --- a/infra/relay/src/environments/EnvironmentLinker.test.ts +++ b/infra/relay/src/environments/EnvironmentLinker.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. import * as NodeCrypto from "node:crypto"; import type { RelayEnvironmentLinkProofPayload, diff --git a/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts b/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts index f61c5a27d5bc..f25ae6753841 100644 --- a/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts +++ b/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash, generateKeyPairSync, or sign. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import type { diff --git a/infra/relay/src/environments/ManagedEndpointProvider.test.ts b/infra/relay/src/environments/ManagedEndpointProvider.test.ts index 29aaefa7b9dd..35e882380ae9 100644 --- a/infra/relay/src/environments/ManagedEndpointProvider.test.ts +++ b/infra/relay/src/environments/ManagedEndpointProvider.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; diff --git a/infra/relay/src/hooks/HookForwarder.test.ts b/infra/relay/src/hooks/HookForwarder.test.ts index b00b4f9d750d..898b34fa2d5f 100644 --- a/infra/relay/src/hooks/HookForwarder.test.ts +++ b/infra/relay/src/hooks/HookForwarder.test.ts @@ -1,5 +1,6 @@ import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; import * as NodeServices from "@effect/platform-node/NodeServices"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import * as EffectNodeCrypto from "@effect/platform-node/NodeCrypto"; import { describe, expect, it } from "@effect/vitest"; diff --git a/infra/relay/src/http/Api.test.ts b/infra/relay/src/http/Api.test.ts index b0e047bd829c..967e23826388 100644 --- a/infra/relay/src/http/Api.test.ts +++ b/infra/relay/src/http/Api.test.ts @@ -7,6 +7,7 @@ import { import * as EnvironmentLinker from "../environments/EnvironmentLinker.ts"; import * as RelayTokens from "../auth/RelayTokens.ts"; import * as Devices from "../agentActivity/Devices.ts"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import { createClerkClient, verifyToken } from "@clerk/backend"; import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; diff --git a/packages/shared/src/desktopBootstrapToken.ts b/packages/shared/src/desktopBootstrapToken.ts index b13f370e3fe2..f18a0f6aa91f 100644 --- a/packages/shared/src/desktopBootstrapToken.ts +++ b/packages/shared/src/desktopBootstrapToken.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. import * as NodeCrypto from "node:crypto"; /** diff --git a/packages/shared/src/dpop.test.ts b/packages/shared/src/dpop.test.ts index c7bc6ff3028d..67a56f394b26 100644 --- a/packages/shared/src/dpop.test.ts +++ b/packages/shared/src/dpop.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no KeyObject, generateKeyPairSync, or sign. import * as NodeCrypto from "node:crypto"; import { assert, describe, it } from "@effect/vitest"; diff --git a/packages/shared/src/relayJwt.test.ts b/packages/shared/src/relayJwt.test.ts index 4e863af484e2..890421de1bd5 100644 --- a/packages/shared/src/relayJwt.test.ts +++ b/packages/shared/src/relayJwt.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. import * as NodeCrypto from "node:crypto"; import { describe, expect, it } from "@effect/vitest"; diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index 65e898a59437..9dd3decab7b3 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. import * as NodeCrypto from "node:crypto"; import type { DesktopSshEnvironmentTarget } from "@t3tools/contracts"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39ebcca645f4..0dc6274ad4dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ catalogs: specifier: 4.0.1 version: 4.0.1 '@effect/tsgo': - specifier: 0.41.0 - version: 0.41.0 + specifier: 0.46.1 + version: 0.46.1 '@legendapp/list': specifier: 3.3.5 version: 3.3.5 @@ -129,7 +129,7 @@ importers: version: 1.1.0 '@effect/tsgo': specifier: 'catalog:' - version: 0.41.0 + version: 0.46.1 '@shadcn/lint': specifier: 0.1.5 version: 0.1.5 @@ -2376,43 +2376,43 @@ packages: peerDependencies: effect: 4.0.1 - '@effect/tsgo-darwin-arm64@0.41.0': - resolution: {integrity: sha512-3iEPtcHF72yDjv7T5YpnX+Io1LDLywJb1oGG3Fp/Fth4xmqiT1Vacyz5wnPCfEWGQ6CyrlyCZSSjUxPyJ70+DQ==} + '@effect/tsgo-darwin-arm64@0.46.1': + resolution: {integrity: sha512-ErehNqI9p8me4XKO8N4bzdDWgBbSNlJ3CIZGfXoAW6Fo4Oo7x4ZbZV7KNsMSn9aJSiUUOQUoXjGBdslU5OYMdw==} cpu: [arm64] os: [darwin] - '@effect/tsgo-darwin-x64@0.41.0': - resolution: {integrity: sha512-rrVVNacJ/Qh8DfdfgNJDuzcVmR3xPnqes3z+F1kio99umJwPIRB8iPWRAY0SrO+Y84A/y2SSb0AjMGqA5V3Aiw==} + '@effect/tsgo-darwin-x64@0.46.1': + resolution: {integrity: sha512-zsQI1gjGMYF/C2FWDUwbStBAf0tJDVgXeWwTAZ+VUiNY06fR1SrZHK0L+22PE49Nim3biWK2h+XXRFVzql+ojg==} cpu: [x64] os: [darwin] - '@effect/tsgo-linux-arm64@0.41.0': - resolution: {integrity: sha512-RVI+7pG3tP56LfH06a2aq4KLp2RQ0HohW8mmoGmrBPyNEL9DnzFEGB98ZoMJ+ERYUJoIp74Jy3nKRVbnzVhxFw==} + '@effect/tsgo-linux-arm64@0.46.1': + resolution: {integrity: sha512-e283BZEQC3mJpk1N7vdUhoF/aYHc4YwCE/fwz/80d11OSvG35U+c49grxS7V7+dAl2ld6QiT8D1rcL8PYu8DLQ==} cpu: [arm64] os: [linux] - '@effect/tsgo-linux-arm@0.41.0': - resolution: {integrity: sha512-JddS91IaupLa22oc4YQiYppe7iAZAEyx/2iR9sAAAhpVanWACsIAcyUy4RJrs8sPLLoLsZQaKQa+9rbQO1FLsg==} + '@effect/tsgo-linux-arm@0.46.1': + resolution: {integrity: sha512-Y12wOcJ6HgwiCgg/oDQH4tdRQKRsWnul/KjCOwMxzVT4kO+ndczu1sKUZT+qIxV7qNk4Sq+uSWKEnFBA3ETI8w==} cpu: [arm] os: [linux] - '@effect/tsgo-linux-x64@0.41.0': - resolution: {integrity: sha512-U3+kMDVe2Opa5mxbN4B6PgOizWx5B4LXLN98CZDUl2vYtRGfwrRRhKQiB+de3ELaZ0MuDjPlCvU7S2mXQ8UiuQ==} + '@effect/tsgo-linux-x64@0.46.1': + resolution: {integrity: sha512-NYGBC3Scfo9yZJfs7cg0KMcHgtukJ4+xff18nqIrON9Zzc/qD6w1AwEwYKYCsbai5hTQBb3VLIWmHM8BJ54WJg==} cpu: [x64] os: [linux] - '@effect/tsgo-win32-arm64@0.41.0': - resolution: {integrity: sha512-H6/xkn+B4B63OPKc0UAzQQFvLs0MXKigiD9dtvQeCU7czWGztOQILuLkyHnM1Dlc+uy4cJ5eXHiMXd+lwl+E8g==} + '@effect/tsgo-win32-arm64@0.46.1': + resolution: {integrity: sha512-MK93SN9qjowrCjch+2wGGncd6Y7KgDyJvLKVVFCJV8cPpHoL3xDCpxFCGqQWe120mVpLmnzbM2evKdh0M5l5PA==} cpu: [arm64] os: [win32] - '@effect/tsgo-win32-x64@0.41.0': - resolution: {integrity: sha512-vRN/Mhi381xEGrvV68IN/VO4Lc0pnlVQzHSVmNCJXLwGLmJUvuvPoyZ7Lv4wwHRX9iZe8Rch21xUGu5jSQtUSw==} + '@effect/tsgo-win32-x64@0.46.1': + resolution: {integrity: sha512-hlRbeNlp0N/dfZZFl+xVzT3XfHYqPmkXzCadR6e5yoe/vyA3O9RnO5UrOBHsXvL2RTE6hrh8bRfx2Uu6g3LL5w==} cpu: [x64] os: [win32] - '@effect/tsgo@0.41.0': - resolution: {integrity: sha512-C/U7lFM0AXsfpqRilDOgwu+llBhAo8bcdIlzgbRWf1LWlU4KZKB2UsUvBPL3qPnm+wmbCvQLeTQc4BjV9oJrcg==} + '@effect/tsgo@0.46.1': + resolution: {integrity: sha512-DMcBCg5nWLufejRlyqFEkXmbtaLUJ56NKny6L1LTtimvz0U3VNj9H6ZUXV4WeeWvJzVyL0y7G2wsCBAFUgy1jw==} hasBin: true '@effect/vitest@4.0.1': @@ -13059,36 +13059,36 @@ snapshots: dependencies: effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) - '@effect/tsgo-darwin-arm64@0.41.0': + '@effect/tsgo-darwin-arm64@0.46.1': optional: true - '@effect/tsgo-darwin-x64@0.41.0': + '@effect/tsgo-darwin-x64@0.46.1': optional: true - '@effect/tsgo-linux-arm64@0.41.0': + '@effect/tsgo-linux-arm64@0.46.1': optional: true - '@effect/tsgo-linux-arm@0.41.0': + '@effect/tsgo-linux-arm@0.46.1': optional: true - '@effect/tsgo-linux-x64@0.41.0': + '@effect/tsgo-linux-x64@0.46.1': optional: true - '@effect/tsgo-win32-arm64@0.41.0': + '@effect/tsgo-win32-arm64@0.46.1': optional: true - '@effect/tsgo-win32-x64@0.41.0': + '@effect/tsgo-win32-x64@0.46.1': optional: true - '@effect/tsgo@0.41.0': + '@effect/tsgo@0.46.1': optionalDependencies: - '@effect/tsgo-darwin-arm64': 0.41.0 - '@effect/tsgo-darwin-x64': 0.41.0 - '@effect/tsgo-linux-arm': 0.41.0 - '@effect/tsgo-linux-arm64': 0.41.0 - '@effect/tsgo-linux-x64': 0.41.0 - '@effect/tsgo-win32-arm64': 0.41.0 - '@effect/tsgo-win32-x64': 0.41.0 + '@effect/tsgo-darwin-arm64': 0.46.1 + '@effect/tsgo-darwin-x64': 0.46.1 + '@effect/tsgo-linux-arm': 0.46.1 + '@effect/tsgo-linux-arm64': 0.46.1 + '@effect/tsgo-linux-x64': 0.46.1 + '@effect/tsgo-win32-arm64': 0.46.1 + '@effect/tsgo-win32-x64': 0.46.1 '@effect/vitest@4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7f74dc5ba16b..e18dd73b5a6c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -36,7 +36,7 @@ catalog: "@effect/platform-node-shared": 4.0.1 "@effect/sql-pg": 4.0.1 "@effect/sql-sqlite-do": 4.0.1 - "@effect/tsgo": 0.41.0 + "@effect/tsgo": 0.46.1 "@effect/vitest": 4.0.1 "@legendapp/list": 3.3.5 "@noble/curves": 1.9.1 diff --git a/tsconfig.base.json b/tsconfig.base.json index 7897a92f557f..61f331a128f9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,7 +23,6 @@ "name": "@effect/language-service", "namespaceImportPackages": ["@effect/platform-node", "effect"], "diagnosticSeverity": { - "importFromBarrel": "error", "anyUnknownInErrorContext": "error", "unsafeEffectTypeAssertion": "error", "instanceOfSchema": "error", From ee0371e2d74db9bb3e6897cd4e54b6406df28f73 Mon Sep 17 00:00:00 2001 From: Erik Thorelli Date: Mon, 5 Oct 2026 22:11:47 -0700 Subject: [PATCH 2/6] chore(deps): correct what the Crypto opt-outs say Effect lacks Effect's Crypto has `digest`, `randomBytes`, and UUIDs, so the opt-outs that said it has no `createHash` or `randomBytes` were wrong. Name only the APIs it lacks, and where a file needs only hashing or ids, say that the Node call is synchronous and Effect's is an Effect. --- apps/desktop/src/preview/Manager.ts | 2 +- apps/server/src/assets/AttachmentUpload.ts | 2 +- apps/server/src/assets/NativeAppIconResolver.ts | 2 +- apps/server/src/auth/ReusableDevAuth.ts | 2 +- apps/server/src/auth/utils.ts | 2 +- apps/server/src/cli/app.ts | 2 +- apps/server/src/device/AgentDeviceTarget.ts | 2 +- apps/server/src/device/SshDeviceHost.ts | 2 +- apps/server/src/htmlRender/PreviewBrowser.test.ts | 2 +- apps/server/src/mcp/McpHttpServer.ts | 2 +- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 2 +- apps/server/src/orchestration-v2/CheckpointService.ts | 2 +- apps/server/src/preview/Manager.ts | 2 +- apps/server/src/project/ProjectSetupScriptRunner.ts | 2 +- apps/server/src/provider/AntigravityInstallation.test.ts | 2 +- apps/server/src/provider/CodexInstallation.test.ts | 2 +- apps/server/src/provider/CodexToolPresentation.ts | 2 +- apps/server/src/provider/OpenCodeProvider.test.ts | 2 +- apps/server/src/provider/ProviderCredentialStore.ts | 2 +- apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts | 2 +- apps/server/src/provider/acp/AcpRegistrySupport.test.ts | 2 +- apps/server/src/provider/acp/AcpRegistrySupport.ts | 2 +- apps/server/src/provider/openCodeUsageLimits.ts | 2 +- apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts | 2 +- apps/server/src/pullRequest/GitHubPullRequestCli.ts | 2 +- apps/server/src/scheduledTasks/webhookVerification.ts | 2 +- apps/server/src/telemetry/Identify.test.ts | 2 +- apps/server/src/usage/cliproxyApi.ts | 2 +- apps/server/src/vcs/GitVcsDriver.ts | 2 +- apps/server/src/ws.ts | 2 +- infra/relay/src/agentActivity/apnsJwt.ts | 2 +- .../relay/src/environments/EnvironmentPublishSignatures.test.ts | 2 +- infra/relay/src/environments/ManagedEndpointProvider.test.ts | 2 +- packages/ssh/src/command.ts | 2 +- 34 files changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index d6535f11735b..8d745e0f7878 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -5,7 +5,7 @@ * elements live in the renderer; we only attach listeners and forward state * here). Single layer-scoped browser session partition. */ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import { DesktopPreviewRecordingInputSchema, diff --git a/apps/server/src/assets/AttachmentUpload.ts b/apps/server/src/assets/AttachmentUpload.ts index 2d5d6d179312..0611eebc2794 100644 --- a/apps/server/src/assets/AttachmentUpload.ts +++ b/apps/server/src/assets/AttachmentUpload.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import { diff --git a/apps/server/src/assets/NativeAppIconResolver.ts b/apps/server/src/assets/NativeAppIconResolver.ts index 6bc8bd0170ef..03151fa409f4 100644 --- a/apps/server/src/assets/NativeAppIconResolver.ts +++ b/apps/server/src/assets/NativeAppIconResolver.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash and randomUUID; Effect's Crypto has digest and randomUUIDv4 as Effects. import * as NodeCrypto from "node:crypto"; import type { ToolActivityNativeAppReference } from "@t3tools/contracts"; import * as Cache from "effect/Cache"; diff --git a/apps/server/src/auth/ReusableDevAuth.ts b/apps/server/src/auth/ReusableDevAuth.ts index b96a369b63fb..5d8da9a0f459 100644 --- a/apps/server/src/auth/ReusableDevAuth.ts +++ b/apps/server/src/auth/ReusableDevAuth.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash or timingSafeEqual. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no timingSafeEqual. import * as NodeCrypto from "node:crypto"; import { AuthSessionId } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts index a05f67f12b08..3da688e22cca 100644 --- a/apps/server/src/auth/utils.ts +++ b/apps/server/src/auth/utils.ts @@ -4,7 +4,7 @@ import type { AuthClientPresentationMetadata, } from "@t3tools/contracts"; import type * as HttpServerRequest from "effect/http/HttpServerRequest"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash, createHmac, or timingSafeEqual. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. import * as NodeCrypto from "node:crypto"; import * as Base64Url from "effect/encoding/Base64Url"; import * as Result from "effect/Result"; diff --git a/apps/server/src/cli/app.ts b/apps/server/src/cli/app.ts index f2fbcfa46f2a..dd2867663fe7 100644 --- a/apps/server/src/cli/app.ts +++ b/apps/server/src/cli/app.ts @@ -1,5 +1,5 @@ // @effect-diagnostics globalTimers:off -- The Node socket client owns its response deadline and clears it on every completion path. -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeNet from "node:net"; import * as NodeOS from "node:os"; diff --git a/apps/server/src/device/AgentDeviceTarget.ts b/apps/server/src/device/AgentDeviceTarget.ts index 81981fe7d0a3..b346a5b8e8c3 100644 --- a/apps/server/src/device/AgentDeviceTarget.ts +++ b/apps/server/src/device/AgentDeviceTarget.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as Schema from "effect/Schema"; import * as Effect from "effect/Effect"; diff --git a/apps/server/src/device/SshDeviceHost.ts b/apps/server/src/device/SshDeviceHost.ts index b75a26e93e51..f1a73762fdcd 100644 --- a/apps/server/src/device/SshDeviceHost.ts +++ b/apps/server/src/device/SshDeviceHost.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import { type DeviceHostSummary, diff --git a/apps/server/src/htmlRender/PreviewBrowser.test.ts b/apps/server/src/htmlRender/PreviewBrowser.test.ts index 01140007092c..8caa70dae8ae 100644 --- a/apps/server/src/htmlRender/PreviewBrowser.test.ts +++ b/apps/server/src/htmlRender/PreviewBrowser.test.ts @@ -12,7 +12,7 @@ import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeZlib from "node:zlib"; diff --git a/apps/server/src/mcp/McpHttpServer.ts b/apps/server/src/mcp/McpHttpServer.ts index bfa8cfad38f1..36ad472fbe24 100644 --- a/apps/server/src/mcp/McpHttpServer.ts +++ b/apps/server/src/mcp/McpHttpServer.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 4f2f6cbd5296..132f7c72d4f8 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; diff --git a/apps/server/src/orchestration-v2/CheckpointService.ts b/apps/server/src/orchestration-v2/CheckpointService.ts index 4734e00b4ca4..fcc612e580fb 100644 --- a/apps/server/src/orchestration-v2/CheckpointService.ts +++ b/apps/server/src/orchestration-v2/CheckpointService.ts @@ -9,7 +9,7 @@ import { RunId, ThreadId, } from "@t3tools/contracts"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; diff --git a/apps/server/src/preview/Manager.ts b/apps/server/src/preview/Manager.ts index 30c7ac86827b..6e0175aa0f84 100644 --- a/apps/server/src/preview/Manager.ts +++ b/apps/server/src/preview/Manager.ts @@ -31,7 +31,7 @@ import { newPreviewTabId, normalizePreviewUrl, } from "@t3tools/shared/preview"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; diff --git a/apps/server/src/project/ProjectSetupScriptRunner.ts b/apps/server/src/project/ProjectSetupScriptRunner.ts index 41fe20a5fd3c..41e8fc0c52ab 100644 --- a/apps/server/src/project/ProjectSetupScriptRunner.ts +++ b/apps/server/src/project/ProjectSetupScriptRunner.ts @@ -5,7 +5,7 @@ import { resolveProjectScripts, setupProjectScript, } from "@t3tools/shared/projectScripts"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as Clock from "effect/Clock"; diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index a96d67106575..e65eac27f52a 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -21,7 +21,7 @@ import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as AntigravityInstallation from "./AntigravityInstallation.ts"; diff --git a/apps/server/src/provider/CodexInstallation.test.ts b/apps/server/src/provider/CodexInstallation.test.ts index 398bc6e02f17..8a7dc99f423f 100644 --- a/apps/server/src/provider/CodexInstallation.test.ts +++ b/apps/server/src/provider/CodexInstallation.test.ts @@ -14,7 +14,7 @@ import * as Exit from "effect/Exit"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/http"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as CodexInstallation from "./CodexInstallation.ts"; diff --git a/apps/server/src/provider/CodexToolPresentation.ts b/apps/server/src/provider/CodexToolPresentation.ts index 3b1f321a5549..9a19e20135e6 100644 --- a/apps/server/src/provider/CodexToolPresentation.ts +++ b/apps/server/src/provider/CodexToolPresentation.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import type { ToolActivityIcon, diff --git a/apps/server/src/provider/OpenCodeProvider.test.ts b/apps/server/src/provider/OpenCodeProvider.test.ts index 7089fb1cbf89..1b51bb66b3a1 100644 --- a/apps/server/src/provider/OpenCodeProvider.test.ts +++ b/apps/server/src/provider/OpenCodeProvider.test.ts @@ -1,5 +1,5 @@ import * as NodeAssert from "node:assert/strict"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; diff --git a/apps/server/src/provider/ProviderCredentialStore.ts b/apps/server/src/provider/ProviderCredentialStore.ts index 37fb4b3f1214..0d4691331779 100644 --- a/apps/server/src/provider/ProviderCredentialStore.ts +++ b/apps/server/src/provider/ProviderCredentialStore.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as Effect from "effect/Effect"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; diff --git a/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts b/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts index 1fea4723eee4..ad98a0fa416f 100644 --- a/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts +++ b/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import type { AcpRegistrySettings, diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.test.ts b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts index 5b36936efe11..e348d79dfb63 100644 --- a/apps/server/src/provider/acp/AcpRegistrySupport.test.ts +++ b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts @@ -16,7 +16,7 @@ import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as TestClock from "effect/testing/TestClock"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.ts b/apps/server/src/provider/acp/AcpRegistrySupport.ts index fe2cfbf00a66..6a27896f4784 100644 --- a/apps/server/src/provider/acp/AcpRegistrySupport.ts +++ b/apps/server/src/provider/acp/AcpRegistrySupport.ts @@ -37,7 +37,7 @@ import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash and randomUUID; Effect's Crypto has digest and randomUUIDv4 as Effects. import * as NodeCrypto from "node:crypto"; import { collectUint8StreamText } from "../../stream/collectUint8StreamText.ts"; diff --git a/apps/server/src/provider/openCodeUsageLimits.ts b/apps/server/src/provider/openCodeUsageLimits.ts index 044730972882..32171edda5ae 100644 --- a/apps/server/src/provider/openCodeUsageLimits.ts +++ b/apps/server/src/provider/openCodeUsageLimits.ts @@ -1,5 +1,5 @@ import * as NodeOS from "node:os"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import type { ServerProviderUsageWindow } from "@t3tools/contracts"; diff --git a/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts b/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts index d78e8b2b1018..ae42600c94ac 100644 --- a/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts +++ b/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts @@ -21,7 +21,7 @@ import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; import { FetchHttpClient } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no randomBytes. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomBytes; Effect's Crypto.randomBytes is an Effect. import * as NodeCrypto from "node:crypto"; import { describe } from "vite-plus/test"; diff --git a/apps/server/src/pullRequest/GitHubPullRequestCli.ts b/apps/server/src/pullRequest/GitHubPullRequestCli.ts index 4f7460616892..9f8f8fc22cc6 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestCli.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestCli.ts @@ -4,7 +4,7 @@ import { runGitHubStackAction, type GitHubStackActionError } from "./githubStack import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Clock from "effect/Clock"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; diff --git a/apps/server/src/scheduledTasks/webhookVerification.ts b/apps/server/src/scheduledTasks/webhookVerification.ts index 60f067796410..b5d97defa3c4 100644 --- a/apps/server/src/scheduledTasks/webhookVerification.ts +++ b/apps/server/src/scheduledTasks/webhookVerification.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash, createHmac, or timingSafeEqual. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. import * as NodeCrypto from "node:crypto"; import type { ScheduledTaskWebhookSignature } from "@t3tools/contracts"; diff --git a/apps/server/src/telemetry/Identify.test.ts b/apps/server/src/telemetry/Identify.test.ts index 5c9189db4ce5..5b9e8f221876 100644 --- a/apps/server/src/telemetry/Identify.test.ts +++ b/apps/server/src/telemetry/Identify.test.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; diff --git a/apps/server/src/usage/cliproxyApi.ts b/apps/server/src/usage/cliproxyApi.ts index 821fd96d4ddc..9ee91331d300 100644 --- a/apps/server/src/usage/cliproxyApi.ts +++ b/apps/server/src/usage/cliproxyApi.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import { diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 497bea9958fa..b7e5605fa9b0 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeBuffer from "node:buffer"; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 32e1acad9a92..c3b377ac2946 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1,7 +1,7 @@ import { OrchestrationDispatchCommandError } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses a synchronous UUID; Effect's Crypto.randomUUIDv4 is an Effect. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomUUID; Effect's Crypto.randomUUIDv4 is an Effect. import * as NodeCrypto from "node:crypto"; import * as DateTime from "effect/DateTime"; diff --git a/infra/relay/src/agentActivity/apnsJwt.ts b/infra/relay/src/agentActivity/apnsJwt.ts index 98cb714660a1..355390d03dae 100644 --- a/infra/relay/src/agentActivity/apnsJwt.ts +++ b/infra/relay/src/agentActivity/apnsJwt.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash or createPrivateKey. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createPrivateKey. import * as NodeCrypto from "node:crypto"; import { p256 } from "@noble/curves/nist"; diff --git a/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts b/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts index f25ae6753841..babfa62404e0 100644 --- a/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts +++ b/infra/relay/src/environments/EnvironmentPublishSignatures.test.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash, generateKeyPairSync, or sign. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import type { diff --git a/infra/relay/src/environments/ManagedEndpointProvider.test.ts b/infra/relay/src/environments/ManagedEndpointProvider.test.ts index 35e882380ae9..31c6953f0457 100644 --- a/infra/relay/src/environments/ManagedEndpointProvider.test.ts +++ b/infra/relay/src/environments/ManagedEndpointProvider.test.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index 9dd3decab7b3..b0fda9a14334 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHash. +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import type { DesktopSshEnvironmentTarget } from "@t3tools/contracts"; From 2d8692cfade2684dd0e3b777326311c8d3a274e3 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:52:26 -0700 Subject: [PATCH 3/6] chore(deps): opt out of nodeBuiltinImport for new Node builtin imports from main Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts | 1 + apps/server/src/orchestration-v2/ProviderTurnControlService.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts index 8d19156ee8d7..24879ecc2815 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts @@ -21,6 +21,7 @@ * * @module orchestration-v2/Adapters/OpenCode2AdapterV2 */ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. import * as NodeCrypto from "node:crypto"; import { diff --git a/apps/server/src/orchestration-v2/ProviderTurnControlService.ts b/apps/server/src/orchestration-v2/ProviderTurnControlService.ts index 659922b3a876..4214a0c83d6e 100644 --- a/apps/server/src/orchestration-v2/ProviderTurnControlService.ts +++ b/apps/server/src/orchestration-v2/ProviderTurnControlService.ts @@ -12,6 +12,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- the stop wait polls against a performance.now() deadline, so it sleeps in real time. import * as NodeTimersPromises from "node:timers/promises"; import * as ProjectionStore from "./ProjectionStore.ts"; From 6252c4bae12855eecbba91c37487fde8319f3069 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:37:48 -0700 Subject: [PATCH 4/6] chore(deps): use Effect Crypto and Effect.sleep instead of Node builtins Replaces the nodeBuiltinImport opt-outs for synchronous createHash, randomUUID, randomBytes and Node timers with Effect's Crypto service and Effect.sleep/Schedule, threading Crypto through callers. Persisted hashes, refs and ids are unchanged; tests pin the previous outputs. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/electron/ElectronProtocol.test.ts | 52 ++- apps/desktop/src/electron/ElectronProtocol.ts | 58 +-- apps/desktop/src/ssh/DesktopSshEnvironment.ts | 2 + .../checkpointing/CheckpointDiffQuery.test.ts | 4 +- .../src/checkpointing/CheckpointDiffQuery.ts | 27 +- .../src/device/AgentDeviceTarget.test.ts | 8 +- apps/server/src/device/AgentDeviceTarget.ts | 19 +- .../server/src/device/DeviceMultiHost.test.ts | 3 +- apps/server/src/device/DeviceService.test.ts | 4 + apps/server/src/device/DeviceService.ts | 34 +- .../Adapters/ClaudeAdapterV2.test.ts | 86 +++-- .../Adapters/ClaudeAdapterV2.testkit.ts | 6 +- .../Adapters/ClaudeAdapterV2.ts | 64 ++-- .../Adapters/CodexAdapterV2.test.ts | 335 +++++++++--------- .../Adapters/CodexAdapterV2.ts | 58 +-- .../Adapters/OpenCode2AdapterV2.test.ts | 35 +- .../Adapters/OpenCode2AdapterV2.testkit.ts | 16 +- .../Adapters/OpenCode2AdapterV2.ts | 32 +- .../CheckpointCaptureService.test.ts | 2 + .../CheckpointScopeOwnership.test.ts | 19 +- .../CheckpointService.test.ts | 4 +- .../src/orchestration-v2/CheckpointService.ts | 38 +- ...laudeAutomaticDelivery.integration.test.ts | 2 + .../ProviderTurnControlService.ts | 28 +- .../server/src/preview/PreviewBrowser.test.ts | 9 +- .../provider/AntigravityInstallation.test.ts | 34 +- .../src/provider/CodexInstallation.test.ts | 9 +- .../src/provider/CodexToolPresentation.ts | 220 ++++++------ .../src/provider/OpenCodeProvider.test.ts | 5 +- .../acp/AcpRegistryAuthenticationState.ts | 17 +- .../provider/acp/AcpRegistrySupport.test.ts | 8 +- .../src/provider/acp/AcpRegistrySupport.ts | 68 ++-- .../opencode2/OpenCode2Client.live.test.ts | 6 +- apps/server/src/telemetry/Identify.test.ts | 18 +- apps/server/src/usage/cliproxyApi.test.ts | 11 +- apps/server/src/usage/cliproxyApi.ts | 35 +- .../ManagedEndpointProvider.test.ts | 28 +- packages/ssh/src/command.test.ts | 25 ++ packages/ssh/src/command.ts | 21 +- packages/ssh/src/runnerProcess.test.ts | 15 +- packages/ssh/src/tunnel.test.ts | 21 +- packages/ssh/src/tunnel.ts | 53 +-- 42 files changed, 904 insertions(+), 635 deletions(-) diff --git a/apps/desktop/src/electron/ElectronProtocol.test.ts b/apps/desktop/src/electron/ElectronProtocol.test.ts index f68b843e7c17..3f0be7cc7b6d 100644 --- a/apps/desktop/src/electron/ElectronProtocol.test.ts +++ b/apps/desktop/src/electron/ElectronProtocol.test.ts @@ -1,9 +1,11 @@ import { assert, describe, it } from "@effect/vitest"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as TestClock from "effect/testing/TestClock"; import { beforeEach, vi } from "vite-plus/test"; const { handleMock, netFetchMock, unhandleMock } = vi.hoisted(() => ({ @@ -174,7 +176,11 @@ describe("ElectronProtocol", () => { targetOrigin: new URL("http://127.0.0.1:5733/"), clerkFrontendApiHostname: undefined, }); - return yield* Effect.promise(() => handler!(new Request("t3code-dev://app/"))); + const fiber = yield* Effect.forkChild( + Effect.promise(() => handler!(new Request("t3code-dev://app/"))), + ); + yield* TestClock.adjust("50 millis"); + return yield* Fiber.join(fiber); }), ); @@ -183,6 +189,50 @@ describe("ElectronProtocol", () => { }).pipe(Effect.provide(layerProtocol)), ); + it.effect("rejects with the last renderer target failure after 50ms and 150ms retries", () => + Effect.gen(function* () { + let handler: ((request: Request) => Promise) | undefined; + handleMock.mockImplementation((_scheme, nextHandler) => { + handler = nextHandler; + }); + const lastFailure = new Error("connect ECONNREFUSED 127.0.0.1:5733 (3)"); + netFetchMock + .mockRejectedValueOnce(new Error("connect ECONNREFUSED 127.0.0.1:5733 (1)")) + .mockRejectedValueOnce(new Error("connect ECONNREFUSED 127.0.0.1:5733 (2)")) + .mockRejectedValueOnce(lastFailure); + + const rejection = yield* Effect.scoped( + Effect.gen(function* () { + const protocol = yield* ElectronProtocol.ElectronProtocol; + yield* protocol.registerDesktopProtocol({ + scheme: "t3code-dev", + targetOrigin: new URL("http://127.0.0.1:5733/"), + clerkFrontendApiHostname: undefined, + }); + const fiber = yield* Effect.forkChild( + Effect.promise(() => + handler!(new Request("t3code-dev://app/")).then( + () => null, + (error: unknown) => error, + ), + ), + ); + yield* TestClock.adjust("49 millis"); + assert.equal(netFetchMock.mock.calls.length, 1); + yield* TestClock.adjust("1 millis"); + assert.equal(netFetchMock.mock.calls.length, 2); + yield* TestClock.adjust("149 millis"); + assert.equal(netFetchMock.mock.calls.length, 2); + yield* TestClock.adjust("1 millis"); + return yield* Fiber.join(fiber); + }), + ); + + assert.strictEqual(rejection, lastFailure); + assert.equal(netFetchMock.mock.calls.length, 3); + }).pipe(Effect.provide(layerProtocol)), + ); + it.effect("preserves protocol registration failures", () => Effect.gen(function* () { const cause = new Error("protocol registration failed"); diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index fdb3da89b59b..c82f41f21996 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -3,11 +3,10 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- fetchWithTransientRetry is plain async code, outside Effect. -import * as NodeTimersPromises from "node:timers/promises"; import * as Path from "effect/Path"; import * as Mime from "effect/http/Mime"; import * as Ref from "effect/Ref"; +import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; @@ -151,11 +150,29 @@ const registerDesktopSchemePrivileges = Effect.sync(registerDesktopSchemePrivile export const layerSchemePrivileges = Layer.effectDiscard(registerDesktopSchemePrivileges); -async function proxyRequest( +class ElectronProtocolFetchError extends Schema.TaggedError()( + "ElectronProtocolFetchError", + { cause: Schema.Defect() }, +) {} + +const netFetch = (url: string, init: RequestInit) => + Effect.tryPromise({ + try: () => Electron.net.fetch(url, init), + catch: (cause) => new ElectronProtocolFetchError({ cause }), + }); + +// The dev renderer target can briefly refuse connections while Vite restarts: +// retry idempotent requests after 50ms, then 150ms, and keep the last failure. +const fetchWithTransientRetry = (url: string, init: RequestInit) => + netFetch(url, init).pipe( + Effect.retry({ schedule: Schedule.exponential("50 millis", 3), times: 2 }), + ); + +const proxyRequest = Effect.fn("desktop.protocol.proxyRequest")(function* ( request: Request, targetOrigin: URL, contentSecurityPolicy: string, -): Promise { +) { const requestUrl = new URL(request.url); if (requestUrl.host !== DESKTOP_HOST) { return new Response(null, { status: 404 }); @@ -191,12 +208,10 @@ async function proxyRequest( } const response = request.method === "GET" || request.method === "HEAD" - ? await fetchWithTransientRetry(targetUrl.toString(), init) - : await Electron.net.fetch(targetUrl.toString(), init); + ? yield* fetchWithTransientRetry(targetUrl.toString(), init) + : yield* netFetch(targetUrl.toString(), init); return withContentSecurityPolicy(response, contentSecurityPolicy); -} - -const TRANSIENT_FETCH_RETRY_DELAYS_MS = [0, 50, 150] as const; +}); // Serves the packaged web client without a backend: files resolve within the // asset directory, and any other path falls back to index.html so the SPA @@ -239,24 +254,6 @@ const serveDesktopAsset = Effect.fn("desktop.protocol.serveAsset")(function* ( }); }); -async function fetchWithTransientRetry(url: string, init: RequestInit): Promise { - let lastError: unknown; - - for (const delayMs of TRANSIENT_FETCH_RETRY_DELAYS_MS) { - if (delayMs > 0) { - await NodeTimersPromises.setTimeout(delayMs); - } - - try { - return await Electron.net.fetch(url, init); - } catch (error) { - lastError = error; - } - } - - throw lastError; -} - /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const registered = yield* Ref.make(false); @@ -279,7 +276,12 @@ export const make = Effect.gen(function* () { contentSecurityPolicy, ); } - return proxyRequest(request, input.targetOrigin, contentSecurityPolicy); + // Reject with net.fetch's own error, as an unproxied fetch would. + return runPromise( + proxyRequest(request, input.targetOrigin, contentSecurityPolicy).pipe( + Effect.catchTag("ElectronProtocolFetchError", (error) => Effect.die(error.cause)), + ), + ); }); }, catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }), diff --git a/apps/desktop/src/ssh/DesktopSshEnvironment.ts b/apps/desktop/src/ssh/DesktopSshEnvironment.ts index 26e307b0c250..08dd48b718f9 100644 --- a/apps/desktop/src/ssh/DesktopSshEnvironment.ts +++ b/apps/desktop/src/ssh/DesktopSshEnvironment.ts @@ -18,6 +18,7 @@ import { } from "@t3tools/ssh/errors"; import * as SshTunnel from "@t3tools/ssh/tunnel"; import * as Context from "effect/Context"; +import type * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; @@ -29,6 +30,7 @@ import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts"; export type DesktopSshEnvironmentRuntimeServices = | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | Path.Path | HttpClient.HttpClient diff --git a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts index a075bdab33d9..b7cf965f57ca 100644 --- a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts +++ b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts @@ -1,3 +1,4 @@ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import { assert, it, vi } from "@effect/vitest"; import { CheckpointRef, CheckpointScopeId, RunId, ThreadId } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -59,6 +60,7 @@ function layerFor(input: { }), ), ), + Layer.provideMerge(NodeCrypto.layer), ); } @@ -80,7 +82,7 @@ it.effect("computes V2 run diffs from projected checkpoint scopes", () => { }); assert.deepEqual(diffCheckpoints.mock.calls[0]?.[0], { cwd: "/repo", - fromCheckpointRef: checkpointRefForScopeOrdinal({ + fromCheckpointRef: yield* checkpointRefForScopeOrdinal({ scopeId: firstScopeId, ordinalWithinScope: 0, }), diff --git a/apps/server/src/checkpointing/CheckpointDiffQuery.ts b/apps/server/src/checkpointing/CheckpointDiffQuery.ts index da0321f38a48..37414f35bfb6 100644 --- a/apps/server/src/checkpointing/CheckpointDiffQuery.ts +++ b/apps/server/src/checkpointing/CheckpointDiffQuery.ts @@ -15,6 +15,7 @@ import { type ThreadId, } from "@t3tools/contracts"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; @@ -77,6 +78,7 @@ function buildTurnDiffResult( export const make = Effect.gen(function* () { const threads = yield* ThreadManagement.ThreadManagementService; const checkpointStore = yield* CheckpointStore.CheckpointStore; + const crypto = yield* Crypto.Crypto; const getTurnDiff: CheckpointDiffQuery["Service"]["getTurnDiff"] = Effect.fn("getTurnDiff")( function* (input) { @@ -155,21 +157,20 @@ export const make = Effect.gen(function* () { }); } + // The root scope is shared by every run in this thread. Its runId + // tracks the latest owner, while ordinal zero stays the baseline. + const firstScope = + input.fromTurnCount === 0 + ? projection.checkpointScopes.find((scope) => scope.kind === "root_run") + : undefined; const fromCheckpointRef = input.fromTurnCount === 0 - ? (() => { - // The root scope is shared by every run in this thread. Its - // runId tracks the latest owner, while ordinal zero stays the baseline. - const firstScope = projection.checkpointScopes.find( - (scope) => scope.kind === "root_run", - ); - return firstScope === undefined - ? undefined - : checkpointRefForScopeOrdinal({ - scopeId: firstScope.id, - ordinalWithinScope: 0, - }); - })() + ? firstScope === undefined + ? undefined + : yield* checkpointRefForScopeOrdinal({ + scopeId: firstScope.id, + ordinalWithinScope: 0, + }).pipe(Effect.provideService(Crypto.Crypto, crypto)) : readyCheckpoints.find((checkpoint) => checkpoint.appRunOrdinal === input.fromTurnCount) ?.ref; if (fromCheckpointRef === undefined) { diff --git a/apps/server/src/device/AgentDeviceTarget.test.ts b/apps/server/src/device/AgentDeviceTarget.test.ts index 46862f87d1b0..4dfd5e20d1c8 100644 --- a/apps/server/src/device/AgentDeviceTarget.test.ts +++ b/apps/server/src/device/AgentDeviceTarget.test.ts @@ -41,7 +41,9 @@ console.log(readFileSync(args[args.indexOf('--config') + 1], 'utf8')); if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, ); const shim = yield* ensureAgentDeviceShim({ entryPath, stateDir: dir }); - const files = ["mini", "android"].map((host) => agentDeviceConfigPath(dir, host, path)); + const files = yield* Effect.forEach(["mini", "android"], (host) => + agentDeviceConfigPath(dir, host, path), + ); for (const [index, file] of files.entries()) yield* writeAgentDeviceConfig(file, { baseUrl: `http://127.0.0.1:${1000 + index}`, @@ -73,8 +75,8 @@ if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, }); expect((yield* Effect.promise(() => invoke(files[0]!))).daemonAuthToken).toBe("new"); expect(yield* fs.readFileString(files[1]!)).toBe(second); - expect(agentDeviceSession("thread", "mini", "same-id")).not.toBe( - agentDeviceSession("thread", "android", "same-id"), + expect(yield* agentDeviceSession("thread", "mini", "same-id")).not.toBe( + yield* agentDeviceSession("thread", "android", "same-id"), ); for (const args of [ ["snapshot"], diff --git a/apps/server/src/device/AgentDeviceTarget.ts b/apps/server/src/device/AgentDeviceTarget.ts index b346a5b8e8c3..d9c6dda12a67 100644 --- a/apps/server/src/device/AgentDeviceTarget.ts +++ b/apps/server/src/device/AgentDeviceTarget.ts @@ -1,7 +1,7 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; -import * as Schema from "effect/Schema"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; +import * as Schema from "effect/Schema"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -13,15 +13,20 @@ const encodeEndpoint = Schema.encodeEffect( ), ); -const key = (value: string) => - NodeCrypto.createHash("sha256").update(value).digest("hex").slice(0, 24); +const key = Effect.fn("AgentDeviceTarget.key")(function* (value: string) { + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto + .digest("SHA-256", new TextEncoder().encode(value)) + .pipe(Effect.orDie); + return Hex.encode(digest).slice(0, 24); +}); /** A stable file per host lets forwarded endpoints change without retargeting other commands. */ export const agentDeviceConfigPath = (stateDir: string, hostId: string, path: Path.Path) => - path.join(stateDir, "device", "hosts", `${key(hostId)}.json`); + key(hostId).pipe(Effect.map((hash) => path.join(stateDir, "device", "hosts", `${hash}.json`))); export const agentDeviceSession = (threadId: string, hostId: string, deviceId: string) => - `t3-${key(JSON.stringify([threadId, hostId, deviceId]))}`; + key(JSON.stringify([threadId, hostId, deviceId])).pipe(Effect.map((hash) => `t3-${hash}`)); export const writeAgentDeviceConfig = Effect.fn("AgentDeviceTarget.writeConfig")(function* ( file: string, diff --git a/apps/server/src/device/DeviceMultiHost.test.ts b/apps/server/src/device/DeviceMultiHost.test.ts index 0a96f97defba..63502353238c 100644 --- a/apps/server/src/device/DeviceMultiHost.test.ts +++ b/apps/server/src/device/DeviceMultiHost.test.ts @@ -2,6 +2,7 @@ import { expect, it } from "@effect/vitest"; import { ThreadId } from "@t3tools/contracts"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as Effect from "effect/Effect"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as ServerSettings from "../serverSettings.ts"; @@ -77,7 +78,7 @@ it.effect("keeps hosts independent when serials collide and another host fails", order.push("write finished"); return "/host-config.json"; }), - ).pipe(Effect.provideService(HttpClient.HttpClient, http)); + ).pipe(Effect.provide(NodeCrypto.layer), Effect.provideService(HttpClient.HttpClient, http)); expect(yield* service.agentReadinessIfSupported("b")).not.toBeNull(); const listed = yield* service.list; expect(listed.devices.map((device) => device.hostId).sort()).toEqual(["a", "b"]); diff --git a/apps/server/src/device/DeviceService.test.ts b/apps/server/src/device/DeviceService.test.ts index 3c942a2a3a36..55a7ac28a67c 100644 --- a/apps/server/src/device/DeviceService.test.ts +++ b/apps/server/src/device/DeviceService.test.ts @@ -7,6 +7,7 @@ import { ThreadId, type DeviceServiceState, } from "@t3tools/contracts"; +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Deferred from "effect/Deferred"; @@ -135,6 +136,7 @@ const fixture = Effect.fn("fixture")(function* ( undefined, installTool, ).pipe( + Effect.provide(NodeCrypto.layer), Effect.provideService(DeviceHost.DeviceHost, host), Effect.provideService( ServerSettings.ServerSettingsService, @@ -482,6 +484,7 @@ it.effect.each(["shutdown", "close"] as const)( }), ); const service = yield* DeviceService.makeWithHosts(new Map([[host.id, host]])).pipe( + Effect.provide(NodeCrypto.layer), Effect.provideService(HttpClient.HttpClient, http), ); const input = { threadId, deviceId, platform: "ios" as const }; @@ -574,6 +577,7 @@ it.effect.each([ }), ); const service = yield* DeviceService.makeWithHosts(new Map([[host.id, host]])).pipe( + Effect.provide(NodeCrypto.layer), Effect.provideService(HttpClient.HttpClient, http), ); yield* service.list; diff --git a/apps/server/src/device/DeviceService.ts b/apps/server/src/device/DeviceService.ts index e402db235a1c..4226755d48eb 100644 --- a/apps/server/src/device/DeviceService.ts +++ b/apps/server/src/device/DeviceService.ts @@ -46,6 +46,7 @@ import { writeAgentDeviceConfig, } from "./AgentDeviceTarget.ts"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -186,6 +187,7 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* installTool?: (tool: "hub" | "agent") => Effect.Effect, ) { const settings = yield* ServerSettings.ServerSettingsService; + const crypto = yield* Crypto.Crypto; const lifecycleLock = yield* Semaphore.make(1); const readDeviceSettings = settings.getSettings.pipe( Effect.map((value) => ({ @@ -943,12 +945,12 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* return yield* configureAgent(input.hostId, ready); }), ); - return [ - "--config", - configPath, - "--session", - agentDeviceSession(input.threadId, input.hostId, input.deviceId), - ]; + const session = yield* agentDeviceSession( + input.threadId, + input.hostId, + input.deviceId, + ).pipe(Effect.provideService(Crypto.Crypto, crypto)); + return ["--config", configPath, "--session", session]; }), state: SynchronizedRef.get(stateRef).pipe(Effect.map(({ state }) => state)), subscribe: PubSub.subscribe(statePubSub), @@ -998,9 +1000,17 @@ export const make = Effect.gen(function* () { const hosts = new Map([ [localHost.id, localHost], ]); - const configureAgent = (hostId: DeviceHostId, ready: DeviceHost.DeviceHostAgentReady) => { - const file = agentDeviceConfigPath(config.stateDir, hostId, path); - return writeAgentDeviceConfig(file, ready.agentDevice).pipe( + const crypto = yield* Crypto.Crypto; + const configPath = (hostId: DeviceHostId) => + agentDeviceConfigPath(config.stateDir, hostId, path).pipe( + Effect.provideService(Crypto.Crypto, crypto), + ); + const configureAgent = (hostId: DeviceHostId, ready: DeviceHost.DeviceHostAgentReady) => + Effect.gen(function* () { + const file = yield* configPath(hostId); + yield* writeAgentDeviceConfig(file, ready.agentDevice); + return file; + }).pipe( Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), Effect.mapError( @@ -1011,9 +1021,7 @@ export const make = Effect.gen(function* () { cause, }), ), - Effect.as(file), ); - }; const probeContext = yield* Effect.context>>(); const localTargetContext = @@ -1089,9 +1097,7 @@ export const make = Effect.gen(function* () { ({ id, scope }) => Effect.gen(function* () { yield* Scope.close(scope, Exit.void); - yield* fs - .remove(agentDeviceConfigPath(config.stateDir, id, path), { force: true }) - .pipe(Effect.ignore); + yield* fs.remove(yield* configPath(id), { force: true }).pipe(Effect.ignore); }), { concurrency: 4, discard: true }, ); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 1c6339ffb4aa..785c4b1c9ec1 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -32,6 +32,7 @@ import { assert, describe, it } from "@effect/vitest"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; @@ -957,6 +958,7 @@ describe("ClaudeAdapterV2 Auto-accept edits", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, queryRunner: { allocateSessionId: Effect.succeed("native-thread-claude-accept-edits"), @@ -1199,6 +1201,7 @@ describe("ClaudeAdapterV2 resume compaction", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, queryRunner: { allocateSessionId: Effect.succeed("native-thread-claude-resume"), @@ -1418,6 +1421,7 @@ describe("ClaudeAdapterV2 attachments", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, queryRunner: { allocateSessionId: Effect.succeed("native-thread-claude-attachments"), @@ -1558,6 +1562,7 @@ describe("ClaudeAdapterV2 attachments", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, queryRunner: { allocateSessionId: Effect.succeed("native-thread-claude-unsupported-attachment"), @@ -1647,6 +1652,7 @@ describe("ClaudeAdapterV2 native fork", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, queryRunner: { allocateSessionId: Effect.succeed("source-native-session"), @@ -1819,6 +1825,7 @@ describe("ClaudeAdapterV2 native session identity", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, queryRunner: { allocateSessionId: Effect.succeed("native-session-identity"), @@ -2108,6 +2115,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: (request) => @@ -3892,6 +3900,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: (request) => @@ -4137,6 +4146,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: () => Effect.void, @@ -4389,11 +4399,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { const continuationAttempt = RunAttemptId.make("attempt-claude-wake-callback-3"); const planToolUseId = "toolu_01WakePlanExitPlanMode"; const planMarkdown = "# Wake plan\n\n1. Report the background result."; - const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => - claudeSdkFrame({ + const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { + return claudeSdkFrame({ ...frame, - user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), + user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), }); + }); const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`); const planToolUse = claudeSdkFrame({ type: "assistant", @@ -4446,10 +4457,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { runtimePolicy, }), ); - yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, firstAttempt)); + yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt)); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000740", result: "STARTED" }), firstAttempt, ), @@ -4508,7 +4519,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { // The prompt's own turn follows and echoes its uuid. yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000743", text: "USER_REPLY", @@ -4518,7 +4529,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000744", result: "USER_REPLY", @@ -4688,11 +4699,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { const firstAttempt = RunAttemptId.make("attempt-claude-wake-approval-1"); const userAttempt = RunAttemptId.make("attempt-claude-wake-approval-2"); const bashToolUseId = "toolu_01WakeApprovalBash"; - const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => - claudeSdkFrame({ + const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { + return claudeSdkFrame({ ...frame, - user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), + user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), }); + }); yield* harness.runtime.startTurn( makeClaudeTestTurnInput({ @@ -4707,14 +4719,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000780", text: "One." }), firstAttempt, ), ); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000781", result: "One." }), firstAttempt, ), @@ -4811,11 +4823,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { const now = yield* DateTime.now; const firstAttempt = RunAttemptId.make("attempt-claude-echo-debris-1"); const secondAttempt = RunAttemptId.make("attempt-claude-echo-debris-2"); - const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => - claudeSdkFrame({ + const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { + return claudeSdkFrame({ ...frame, - user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), + user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), }); + }); yield* harness.runtime.startTurn( makeClaudeTestTurnInput({ @@ -4830,14 +4843,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { // The first turn echoes on its first frame: this process echoes early. yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000760", text: "One." }), firstAttempt, ), ); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000761", result: "One." }), firstAttempt, ), @@ -4864,14 +4877,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { yield* Queue.offer(harness.sdkMessages, staleTaskNotificationResult); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000762", text: "Two." }), secondAttempt, ), ); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000763", result: "Two." }), secondAttempt, ), @@ -4909,11 +4922,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { const now = yield* DateTime.now; const firstAttempt = RunAttemptId.make("attempt-claude-echo-1"); const secondAttempt = RunAttemptId.make("attempt-claude-echo-2"); - const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => - claudeSdkFrame({ + const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { + return claudeSdkFrame({ ...frame, - user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), + user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), }); + }); const assistantTexts = () => harness.events.flatMap((event) => event.type === "message.updated" && event.message.role === "assistant" @@ -4935,18 +4949,18 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); assert.equal( harness.offeredMessages[0]?.uuid, - ClaudeAdapterV2.claudePromptUuid(firstAttempt), + yield* ClaudeAdapterV2.claudePromptUuid(firstAttempt), ); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000701", text: "One." }), firstAttempt, ), ); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000702", result: "One." }), firstAttempt, ), @@ -5995,11 +6009,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { const userAttempt = RunAttemptId.make("attempt-claude-wake-subagent-2"); const continuationAttempt = RunAttemptId.make("attempt-claude-wake-subagent-3"); const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`); - const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => - claudeSdkFrame({ + const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { + return claudeSdkFrame({ ...frame, - user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), + user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), }); + }); yield* harness.runtime.startTurn( makeClaudeTestTurnInput({ @@ -6011,10 +6026,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachments: [], }), ); - yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, firstAttempt)); + yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt)); yield* Queue.offer( harness.sdkMessages, - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000770", result: "STARTED" }), firstAttempt, ), @@ -6083,14 +6098,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { session_id: WAKE_NATIVE_SESSION, }), wakeResult, - stamp( + yield* stamp( makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000776", text: "USER_REPLY", }), userAttempt, ), - stamp( + yield* stamp( makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000777", result: "USER_REPLY" }), userAttempt, ), @@ -7018,6 +7033,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: (request) => @@ -7201,6 +7217,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: (request) => @@ -7436,6 +7453,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: (request) => @@ -7628,6 +7646,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: () => Effect.void }, queryRunner: { @@ -7794,6 +7813,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: () => Effect.void, @@ -7923,6 +7943,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: (request) => @@ -8117,6 +8138,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { attachmentsDir, fileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator, continuationRequests: { offer: () => Effect.void, diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts index 25f0972f412d..2f73c0db11e6 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts @@ -1580,7 +1580,11 @@ async function recordClaudeStreamingQuery(input: { // Like the adapter, give each prompt a uuid Claude echoes on its turn. const message = ClaudeAdapterV2.makeClaudeUserMessage({ text: prompt, - uuid: ClaudeAdapterV2.claudePromptUuid(`${input.sessionId}:prompt:${index + 1}`), + uuid: await Effect.runPromise( + ClaudeAdapterV2.claudePromptUuid(`${input.sessionId}:prompt:${index + 1}`).pipe( + Effect.provide(NodeServices.layer), + ), + ), }); input.entries.push({ type: "expect_outbound", diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 132f7c72d4f8..9af1c0efde4b 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1,6 +1,3 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; - import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; import { dynamicToolTitle, @@ -74,6 +71,7 @@ import { import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; +import * as Hex from "effect/encoding/Hex"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -1421,11 +1419,17 @@ const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input: // Stable per run attempt, so a replayed prompt offer matches its recording. // Claude echoes it back as user_message_uuid on the turn that answers it. -export function claudePromptUuid(attemptId: string): NonNullable { - const hex = NodeCrypto.createHash("sha256").update(`t3-claude-prompt:${attemptId}`).digest("hex"); +export const claudePromptUuid = Effect.fn("claudePromptUuid")(function* (attemptId: string) { + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto + .digest("SHA-256", new TextEncoder().encode(`t3-claude-prompt:${attemptId}`)) + .pipe(Effect.orDie); + const hex = Hex.encode(digest); const variant = ((Number.parseInt(hex[16]!, 16) & 0x3) | 0x8).toString(16); - return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`; -} + const uuid: NonNullable = + `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`; + return uuid; +}); type ClaudeAssistantContentBlock = SDKAssistantMessage["message"]["content"][number]; type ClaudeToolUseContentBlock = Extract< @@ -2999,6 +3003,7 @@ export interface ClaudeAdapterV2Options { readonly attachmentsDir: string; readonly fileSystem: FileSystem.FileSystem; readonly path: Path.Path; + readonly crypto: Crypto.Crypto; readonly idAllocator: IdAllocator.IdAllocatorV2Shape; readonly queryRunner: ClaudeAgentSdkQueryRunnerShape; readonly scopedLimitNames?: Ref.Ref; @@ -3014,7 +3019,7 @@ export interface ClaudeAdapterV2Options { export function makeClaudeAdapterV2( adapterOptions: ClaudeAdapterV2Options, ): ProviderAdapter.ProviderAdapterV2Shape { - const { attachmentsDir, fileSystem, path, idAllocator, queryRunner } = adapterOptions; + const { attachmentsDir, fileSystem, path, crypto, idAllocator, queryRunner } = adapterOptions; const continuationRequests = adapterOptions.continuationRequests ?? { offer: () => Effect.void, }; @@ -7243,6 +7248,11 @@ export function makeClaudeAdapterV2( const startedAt = yield* DateTime.now; const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread); const nativeTurnId = `turn:${turnInput.attemptId}`; + const promptUuid = isClaudeProviderContinuationTurn(turnInput) + ? null + : yield* claudePromptUuid(turnInput.attemptId).pipe( + Effect.provideService(Crypto.Crypto, crypto), + ); const providerTurnId = idAllocator.derive.providerTurn({ driver: CLAUDE_PROVIDER, nativeTurnId, @@ -7299,9 +7309,7 @@ export function makeClaudeAdapterV2( subagentsByToolUseId: new Map(), subagentNodesByTaskId: new Map(), pendingSubagentLaunchesByToolUseId: new Map(), - promptUuid: isClaudeProviderContinuationTurn(turnInput) - ? null - : claudePromptUuid(turnInput.attemptId), + promptUuid, promptEcho: isClaudeProviderContinuationTurn(turnInput) ? "confirmed" : "pending", gatedFramesBeforeEcho: 0, heldRootFrames: [], @@ -7310,20 +7318,20 @@ export function makeClaudeAdapterV2( // produced instead of prompting it again: drain the buffered wake // messages into this turn and let any still-streaming messages // follow live. The continuation prompt text never reaches the CLI. - const isContinuationTurn = context.promptUuid === null; - const userMessage = isContinuationTurn - ? null - : yield* makeClaudeUserMessageWithAttachments({ - text: applyClaudePromptEffortPrefix( - turnInput.message.text, - compileClaudeModelSelection(turnInput.modelSelection).promptEffort, - ), - attachments: turnInput.message.attachments, - attachmentsDir, - fileSystem, - skillNames: yield* userInvocableSkillNames(turnInput.runtimePolicy.cwd), - uuid: claudePromptUuid(turnInput.attemptId), - }); + const userMessage = + promptUuid === null + ? null + : yield* makeClaudeUserMessageWithAttachments({ + text: applyClaudePromptEffortPrefix( + turnInput.message.text, + compileClaudeModelSelection(turnInput.modelSelection).promptEffort, + ), + attachments: turnInput.message.attachments, + attachmentsDir, + fileSystem, + skillNames: yield* userInvocableSkillNames(turnInput.runtimePolicy.cwd), + uuid: promptUuid, + }); const querySession = yield* openQuery(turnInput, nativeThreadId); yield* Ref.set(activeTurn, context); yield* emitProviderEvent({ @@ -7907,6 +7915,7 @@ export function makeClaudeAdapterV2( export type ClaudeAdapterV2DriverEnv = | ClaudeAgentSdkQueryRunner + | Crypto.Crypto | FileSystem.FileSystem | IdAllocator.IdAllocatorV2 | Path.Path @@ -7927,6 +7936,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment); const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment); const path = yield* Path.Path; + const crypto = yield* Crypto.Crypto; const binaryPath = yield* resolveClaudeSdkExecutablePath( expandHomePath(config.binaryPath), claudeEnvironment, @@ -7938,6 +7948,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( attachmentsDir: serverConfig.attachmentsDir, fileSystem, path, + crypto, idAllocator, queryRunner, continuationRequests, @@ -7971,6 +7982,7 @@ export const ClaudeAdapterV2Driver: ProviderAdapterDriver< const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; + const crypto = yield* Crypto.Crypto; const hostEnvironment = yield* HostProcessEnvironment; const idAllocator = yield* IdAllocator.IdAllocatorV2; const queryRunner = yield* ClaudeAgentSdkQueryRunner; @@ -7984,6 +7996,7 @@ const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* attachmentsDir: serverConfig.attachmentsDir, fileSystem, path, + crypto, idAllocator, queryRunner, continuationRequests, @@ -7994,6 +8007,7 @@ const layer: Layer.Layer< ProviderAdapter.ProviderAdapterV2, never, | ClaudeAgentSdkQueryRunner + | Crypto.Crypto | FileSystem.FileSystem | IdAllocator.IdAllocatorV2 | Path.Path diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index b6bf7ecd11c6..245e76af2958 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -35,6 +35,7 @@ import * as CodexClient from "effect-codex-app-server/client"; import * as CodexReplay from "effect-codex-app-server/replay"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Predicate from "effect/Predicate"; import * as FileSystem from "effect/FileSystem"; @@ -801,183 +802,195 @@ describe("CodexAdapterV2 process spawning", () => { }); describe("CodexAdapterV2 dynamic tool projection", () => { - it("uses the CUA call title while leaving other MCP titles as tool arguments", () => { - const call = { - type: "mcpToolCall" as const, - id: "inspect", - server: "cua_repl", - tool: "js", - status: "completed" as const, - arguments: { - code: "await game.getAXStateAndScreenshot();", - title: "Inspect Saga music screen", - }, - result: { content: [] }, - }; - assert.equal( - CodexAdapterV2.projectCodexDynamicToolItem(call).title, - "Inspect Saga music screen", - ); - assert.equal( - CodexAdapterV2.projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title, - "js", - ); - assert.equal( - CodexAdapterV2.projectCodexDynamicToolItem({ ...call, server: "github" }).title, - "js", - ); - }); + it.effect("uses the CUA call title while leaving other MCP titles as tool arguments", () => + Effect.gen(function* () { + const call = { + type: "mcpToolCall" as const, + id: "inspect", + server: "cua_repl", + tool: "js", + status: "completed" as const, + arguments: { + code: "await game.getAXStateAndScreenshot();", + title: "Inspect Saga music screen", + }, + result: { content: [] }, + }; + assert.equal( + (yield* CodexAdapterV2.projectCodexDynamicToolItem(call)).title, + "Inspect Saga music screen", + ); + assert.equal( + (yield* CodexAdapterV2.projectCodexDynamicToolItem({ ...call, arguments: { title: " " } })) + .title, + "js", + ); + assert.equal( + (yield* CodexAdapterV2.projectCodexDynamicToolItem({ ...call, server: "github" })).title, + "js", + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it.each(["inProgress", "completed", "failed"] as const)( + it.effect.each(["inProgress", "completed", "failed"] as const)( "presents ordinary MCP calls when %s", - (status) => { - const projection = CodexAdapterV2.projectCodexDynamicToolItem({ + (status) => + Effect.gen(function* () { + const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ + type: "mcpToolCall", + id: "weather-call", + server: "weather", + tool: "get_weather", + status, + arguments: { city: "Berlin" }, + }); + assert.equal(projection.title, "get weather"); + assert.deepEqual(projection.toolSource, { + key: "mcp:weather", + name: "weather", + kind: "integration", + }); + assert.deepEqual(projection.input, { city: "Berlin" }); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("uses Codex connector names without reading a display title from arguments", () => + Effect.gen(function* () { + const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ type: "mcpToolCall", - id: "weather-call", - server: "weather", - tool: "get_weather", - status, - arguments: { city: "Berlin" }, + id: "connector-call", + server: "_apps", + tool: "connector_get_weather", + status: "completed", + arguments: { title: "Argument, not display metadata" }, + appContext: { + connectorId: "weather-app", + appName: "Weather", + actionName: "Check weather", + }, + result: { + content: [], + _meta: { source: { logoUrl: "https://example.com/weather.png" } }, + }, }); - assert.equal(projection.title, "get weather"); - assert.deepEqual(projection.toolSource, { - key: "mcp:weather", - name: "weather", - kind: "integration", + assert.equal(projection.title, "Check weather"); + assert.equal(projection.toolSource?.name, "Weather"); + assert.deepEqual(projection.toolIcon, { + _tag: "themed-logo", + logoUrl: "https://example.com/weather.png", }); - assert.deepEqual(projection.input, { city: "Berlin" }); - }, + assert.deepEqual(projection.toolSource?.icon, projection.toolIcon); + }).pipe(Effect.provide(NodeServices.layer)), ); - it("uses Codex connector names without reading a display title from arguments", () => { - const projection = CodexAdapterV2.projectCodexDynamicToolItem({ - type: "mcpToolCall", - id: "connector-call", - server: "_apps", - tool: "connector_get_weather", - status: "completed", - arguments: { title: "Argument, not display metadata" }, - appContext: { - connectorId: "weather-app", - appName: "Weather", - actionName: "Check weather", - }, - result: { - content: [], - _meta: { source: { logoUrl: "https://example.com/weather.png" } }, - }, - }); - assert.equal(projection.title, "Check weather"); - assert.equal(projection.toolSource?.name, "Weather"); - assert.deepEqual(projection.toolIcon, { - _tag: "themed-logo", - logoUrl: "https://example.com/weather.png", - }); - assert.deepEqual(projection.toolSource?.icon, projection.toolIcon); - }); - - it("preserves native browser and app icons alongside MCP tool output", () => { - const browser = CodexAdapterV2.projectCodexDynamicToolItem({ - type: "mcpToolCall", - id: "browser", - server: "browser", - tool: "open", - status: "completed", - arguments: {}, - result: { - content: [], - _meta: { - "codex/toolSurface": { - kind: "browserUse", - browserFamily: "Chrome", - screenshot: { - pageUrl: "https://example.com/docs", - faviconUrl: "https://example.com/icon.png", + it.effect("preserves native browser and app icons alongside MCP tool output", () => + Effect.gen(function* () { + const browser = yield* CodexAdapterV2.projectCodexDynamicToolItem({ + type: "mcpToolCall", + id: "browser", + server: "browser", + tool: "open", + status: "completed", + arguments: {}, + result: { + content: [], + _meta: { + "codex/toolSurface": { + kind: "browserUse", + browserFamily: "Chrome", + screenshot: { + pageUrl: "https://example.com/docs", + faviconUrl: "https://example.com/icon.png", + }, }, }, }, - }, - }); - assert.equal(browser.toolSurface, "browser"); - assert.deepEqual(browser.toolIcon, { - _tag: "website", - pageUrl: "https://example.com/docs", - faviconUrl: "https://example.com/icon.png", - }); - assert.equal(browser.toolSource?.name, "Chrome"); - const app = CodexAdapterV2.projectCodexDynamicToolItem({ - type: "mcpToolCall", - id: "app", - server: "computer", - tool: "click", - status: "completed", - arguments: {}, - result: { - content: [], - _meta: { - "codex/toolSurface": { - kind: "computerUse", - app: { kind: "appId", appId: "com.apple.finder" }, + }); + assert.equal(browser.toolSurface, "browser"); + assert.deepEqual(browser.toolIcon, { + _tag: "website", + pageUrl: "https://example.com/docs", + faviconUrl: "https://example.com/icon.png", + }); + assert.equal(browser.toolSource?.name, "Chrome"); + const app = yield* CodexAdapterV2.projectCodexDynamicToolItem({ + type: "mcpToolCall", + id: "app", + server: "computer", + tool: "click", + status: "completed", + arguments: {}, + result: { + content: [], + _meta: { + "codex/toolSurface": { + kind: "computerUse", + app: { kind: "appId", appId: "com.apple.finder" }, + }, }, }, - }, - }); - assert.deepEqual(app.toolIcon, { - _tag: "native-app", - app: { _tag: "app-id", appId: "com.apple.finder" }, - }); - assert.equal(app.toolSource?.name, "Finder"); - }); + }); + assert.deepEqual(app.toolIcon, { + _tag: "native-app", + app: { _tag: "app-id", appId: "com.apple.finder" }, + }); + assert.equal(app.toolSource?.name, "Finder"); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("preserves MCP arguments and prefers structured output", () => { - const projection = CodexAdapterV2.projectCodexDynamicToolItem({ - type: "mcpToolCall", - id: "call-create-threads", - server: "t3-code", - tool: "create_threads", - status: "completed", - arguments: { - threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], - }, - result: { - content: [{ type: "text", text: '{"threads":[{"threadId":"thread:mcp:fixture:0"}]}' }], - structuredContent: { - threads: [{ threadId: "thread:mcp:fixture:0" }], + it.effect("preserves MCP arguments and prefers structured output", () => + Effect.gen(function* () { + const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ + type: "mcpToolCall", + id: "call-create-threads", + server: "t3-code", + tool: "create_threads", + status: "completed", + arguments: { + threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], }, - }, - }); + result: { + content: [{ type: "text", text: '{"threads":[{"threadId":"thread:mcp:fixture:0"}]}' }], + structuredContent: { + threads: [{ threadId: "thread:mcp:fixture:0" }], + }, + }, + }); - assert.deepEqual(projection, { - toolName: "t3-code.create_threads", - input: { - threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], - }, - output: { - threads: [{ threadId: "thread:mcp:fixture:0" }], - }, - status: "completed", - }); - }); + assert.deepEqual(projection, { + toolName: "t3-code.create_threads", + input: { + threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], + }, + output: { + threads: [{ threadId: "thread:mcp:fixture:0" }], + }, + status: "completed", + }); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("preserves namespaced dynamic tool output", () => { - const projection = CodexAdapterV2.projectCodexDynamicToolItem({ - type: "dynamicToolCall", - id: "call-dynamic", - namespace: "workspace", - tool: "inspect", - status: "failed", - arguments: { path: "package.json" }, - contentItems: [{ type: "inputText", text: "inspection failed" }], - success: false, - }); + it.effect("preserves namespaced dynamic tool output", () => + Effect.gen(function* () { + const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ + type: "dynamicToolCall", + id: "call-dynamic", + namespace: "workspace", + tool: "inspect", + status: "failed", + arguments: { path: "package.json" }, + contentItems: [{ type: "inputText", text: "inspection failed" }], + success: false, + }); - assert.deepEqual(projection, { - toolName: "workspace.inspect", - input: { path: "package.json" }, - output: [{ type: "inputText", text: "inspection failed" }], - status: "failed", - }); - }); + assert.deepEqual(projection, { + toolName: "workspace.inspect", + input: { path: "package.json" }, + output: [{ type: "inputText", text: "inspection failed" }], + status: "failed", + }); + }).pipe(Effect.provide(NodeServices.layer)), + ); }); describe("CodexAdapterV2 native protocol logging", () => { @@ -1703,6 +1716,7 @@ describe("CodexAdapterV2 session initialize", () => { ), ), }, + crypto: yield* Crypto.Crypto, fileSystem: yield* FileSystem.FileSystem, idAllocator: yield* IdAllocator.IdAllocatorV2, serverConfig: yield* makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie), @@ -1899,6 +1913,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { settings: DEFAULT_CODEX_SETTINGS, environment: {}, clientFactory, + crypto: yield* Crypto.Crypto, fileSystem, idAllocator, serverConfig, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 194751bd66dc..4603731c3365 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -66,6 +66,7 @@ import * as CodexClient from "effect-codex-app-server/client"; import * as CodexErrors from "effect-codex-app-server/errors"; import * as CodexSchema from "effect-codex-app-server/schema"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -479,26 +480,29 @@ function codexDynamicToolOutput( return item.success === false ? { success: false } : undefined; } -export function projectCodexDynamicToolItem( - item: CodexDynamicToolItem, -): CodexDynamicToolProjection { - const output = - item.type === "mcpToolCall" ? codexMcpToolOutput(item) : codexDynamicToolOutput(item); - const toolName = - item.type === "mcpToolCall" - ? `${item.server}.${item.tool}` - : [trimText(item.namespace), item.tool].filter(Boolean).join("."); - const presentation = item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}; - const title = dynamicToolTitle(toolName, item.arguments) ?? presentation.title; - const projection: CodexDynamicToolProjection = { - ...presentation, - toolName, - ...(title ? { title } : {}), - input: item.arguments, - status: codexItemStatus(item.status).turnItem, - }; - return output === undefined ? projection : { ...projection, output }; -} +export const projectCodexDynamicToolItem = Effect.fn("CodexAdapterV2.projectDynamicToolItem")( + function* ( + item: CodexDynamicToolItem, + ): Effect.fn.Return { + const output = + item.type === "mcpToolCall" ? codexMcpToolOutput(item) : codexDynamicToolOutput(item); + const toolName = + item.type === "mcpToolCall" + ? `${item.server}.${item.tool}` + : [trimText(item.namespace), item.tool].filter(Boolean).join("."); + const presentation: McpToolPresentation = + item.type === "mcpToolCall" ? yield* mcpToolPresentation(item) : {}; + const title = dynamicToolTitle(toolName, item.arguments) ?? presentation.title; + const projection: CodexDynamicToolProjection = { + ...presentation, + toolName, + ...(title ? { title } : {}), + input: item.arguments, + status: codexItemStatus(item.status).turnItem, + }; + return output === undefined ? projection : { ...projection, output }; + }, +); function codexNativeItemRef(nativeItemId: string) { return { @@ -1513,6 +1517,7 @@ export const layerAppServerClientFactory: Layer.Layer< export type CodexAdapterV2DriverEnv = | CodexAppServerClientFactory + | Crypto.Crypto | FileSystem.FileSystem | IdAllocatorV2 | Path.Path @@ -1525,6 +1530,7 @@ export const createCodexAdapterV2 = ( Effect.gen(function* () { const clientFactory = yield* CodexAppServerClientFactory; const continuationRequests = yield* ProviderContinuationRequests; + const crypto = yield* Crypto.Crypto; const fileSystem = yield* FileSystem.FileSystem; const hostEnvironment = yield* HostProcessEnvironment; const idAllocator = yield* IdAllocatorV2; @@ -1555,6 +1561,7 @@ export const createCodexAdapterV2 = ( settings, environment: mergeProviderInstanceEnvironment(environment, hostEnvironment), clientFactory, + crypto, fileSystem, idAllocator, serverConfig, @@ -1573,12 +1580,13 @@ export const CodexAdapterV2Driver: ProviderAdapterDriver = Layer.effect( ProviderAdapterV2, Effect.gen(function* () { const clientFactory = yield* CodexAppServerClientFactory; const continuationRequests = yield* ProviderContinuationRequests; + const crypto = yield* Crypto.Crypto; const fileSystem = yield* FileSystem.FileSystem; const hostEnvironment = yield* HostProcessEnvironment; const idAllocator = yield* IdAllocatorV2; @@ -1589,6 +1597,7 @@ const layer: Layer.Layer< settings: DEFAULT_CODEX_SETTINGS, environment: hostEnvironment, clientFactory, + crypto, fileSystem, idAllocator, serverConfig, @@ -1609,6 +1618,7 @@ export interface CodexAdapterV2Options { * Codex with a current access token. */ readonly resolveRuntime?: Effect.Effect; + readonly crypto: Crypto.Crypto; readonly fileSystem: FileSystem.FileSystem; readonly idAllocator: IdAllocatorV2Shape; readonly serverConfig: ServerConfig["Service"]; @@ -1623,7 +1633,7 @@ export interface CodexAdapterV2Options { } export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): ProviderAdapterV2Shape { - const { clientFactory, fileSystem, idAllocator, serverConfig } = adapterOptions; + const { clientFactory, crypto, fileSystem, idAllocator, serverConfig } = adapterOptions; const continuationRequests = adapterOptions.continuationRequests; return ProviderAdapterV2.of({ @@ -3480,7 +3490,9 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi nativeItemId: item.id, }); const { ordinal, startedAt } = yield* resolveItemPosition(context, item.id); - const projection = projectCodexDynamicToolItem(item); + const projection = yield* projectCodexDynamicToolItem(item).pipe( + Effect.provideService(Crypto.Crypto, crypto), + ); const node: OrchestrationV2ExecutionNode = { id: nodeId, threadId: context.projectionThreadId, diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index 5b3259bab078..46792897e789 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -3,6 +3,7 @@ * driven through the real adapter and `@opencode/client` against a replayed * HTTP server. Frames reuse the shapes recorded against 2.0.18. */ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import { assert, it } from "@effect/vitest"; import { CheckpointId, @@ -2766,21 +2767,25 @@ describe("OpenCode2 adapter", () => { }).pipe(Effect.scoped), ); - it("names each thread's MCP server within OpenCode's limits, one name per thread", () => { - const project = "thread:project:ce04e4e2-6c29-4ff0-a1d7-b089dd63e258"; - const ids = [ - `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c4`, - `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c5`, - "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around1", - "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around2", - ]; - const names = ids.map(t3McpServerName); - for (const name of names) assert.match(name, /^t3-code-[A-Za-z0-9_-]{1,56}$/); - assert.equal(new Set(names).size, ids.length); - assert.deepEqual(ids.map(t3McpServerName), names); - // A name that already fits stays readable. - assert.equal(t3McpServerName(threadId), "t3-code-thread_opencode2-adapter"); - }); + it.effect("names each thread's MCP server within OpenCode's limits, one name per thread", () => + Effect.gen(function* () { + const project = "thread:project:ce04e4e2-6c29-4ff0-a1d7-b089dd63e258"; + const ids = [ + `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c4`, + `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c5`, + "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around1", + "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around2", + ]; + const names = yield* Effect.forEach(ids, t3McpServerName); + for (const name of names) assert.match(name, /^t3-code-[A-Za-z0-9_-]{1,56}$/); + assert.equal(new Set(names).size, ids.length); + assert.deepEqual(yield* Effect.forEach(ids, t3McpServerName), names); + // A digested name is the one the synchronous node:crypto version produced. + assert.equal(names[0], "t3-code-63abb5df2b188bdd"); + // A name that already fits stays readable. + assert.equal(yield* t3McpServerName(threadId), "t3-code-thread_opencode2-adapter"); + }).pipe(Effect.provide(NodeCrypto.layer)), + ); it.effect("reads user and assistant text from the session's message list", () => Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts index d7c0e4c5ea18..e8fd1a0258f5 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts @@ -284,7 +284,13 @@ function layerRegistry( Effect.map((adapter) => ProviderAdapterRegistry.layerFromAdapters([adapter])), ), ).pipe( - Layer.provide(Layer.mergeAll(layerReplayServerConfig(transcript.scenario), IdAllocator.layer)), + Layer.provide( + Layer.mergeAll( + layerReplayServerConfig(transcript.scenario), + IdAllocator.layer, + NodeServices.layer, + ), + ), ); } @@ -322,7 +328,13 @@ export const openCode2ReplayRuntime = ( }, }); }).pipe( - Effect.provide(Layer.mergeAll(layerReplayServerConfig("opencode2_adapter"), IdAllocator.layer)), + Effect.provide( + Layer.mergeAll( + layerReplayServerConfig("opencode2_adapter"), + IdAllocator.layer, + NodeServices.layer, + ), + ), ); export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts index 24879ecc2815..7716b75baaf9 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts @@ -21,8 +21,6 @@ * * @module orchestration-v2/Adapters/OpenCode2AdapterV2 */ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import { AbsolutePath, @@ -60,7 +58,9 @@ import { import type * as Cause from "effect/Cause"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; @@ -454,12 +454,15 @@ const rule = (action: string, effect: Rule["effect"]): Rule => ({ action, resour * tool namespace limit), and its router rejects adding one over 100, so a * thread id that does not fit is replaced by a digest of it. */ -export const t3McpServerName = (threadId: string) => { +export const t3McpServerName = Effect.fn("t3McpServerName")(function* (threadId: string) { const name = `t3-code-${threadId.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}`; if (name.length <= 64) return name; - const digest = NodeCrypto.createHash("sha256").update(threadId).digest("hex"); - return `t3-code-${digest.slice(0, 16)}`; -}; + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto + .digest("SHA-256", new TextEncoder().encode(threadId)) + .pipe(Effect.orDie); + return `t3-code-${Hex.encode(digest).slice(0, 16)}`; +}); /** * The rules that keep T3's MCP servers to their own thread, after the mode's: @@ -467,19 +470,19 @@ export const t3McpServerName = (threadId: string) => { * this thread's own is allowed again, in every mode. A subagent's session * inherits the thread's. */ -const mcpRules = (threadId: string | null): ReadonlyArray => - threadId === null +const mcpRules = (mcpServerName: string | null): ReadonlyArray => + mcpServerName === null ? [] : [ { action: "t3-code-*", resource: "*", effect: "deny" }, - { action: `${t3McpServerName(threadId)}_*`, resource: "*", effect: "allow" }, + { action: `${mcpServerName}_*`, resource: "*", effect: "allow" }, ]; const sessionRules = ( policy: RulesPolicy, paths: ReadonlyArray, grants: ReadonlyArray, - threadId: string | null, + mcpServerName: string | null, ): ReadonlyArray => [ ...(policy.runtimeMode === "full-access" ? [rule("*", "allow")] @@ -493,7 +496,7 @@ const sessionRules = ( // are never denied: the free tier refuses sessions whose rules deny them. ...(policy.interactionMode === "plan" ? [rule("edit", "deny")] : []), ...paths, - ...mcpRules(threadId), + ...mcpRules(mcpServerName), ]; const sameRules = (left: ReadonlyArray | undefined, right: ReadonlyArray) => @@ -829,7 +832,10 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr const idAllocator = yield* IdAllocator.IdAllocatorV2; const serverConfig = yield* ServerConfig.ServerConfig; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; + const crypto = yield* Crypto.Crypto; const driver = OPENCODE_PROVIDER; + const mcpServerNameFor = (threadId: string) => + t3McpServerName(threadId).pipe(Effect.provideService(Crypto.Crypto, crypto)); /** * Lends the instance's server to a session until its scope closes. A spawned @@ -2978,7 +2984,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr policy, paths, policy.runtimeMode === "full-access" ? [] : thread.grants, - appThreadId, + appThreadId === null ? null : yield* mcpServerNameFor(appThreadId), ); }); @@ -3235,7 +3241,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr ) { const mcpSession = McpProviderSession.readMcpProviderSession(turnInput.threadId); const directory = turnInput.runtimePolicy.cwd ?? serverConfig.cwd; - const name = t3McpServerName(turnInput.threadId); + const name = yield* mcpServerNameFor(turnInput.threadId); // An external server may not reach T3's MCP endpoint, as with 1.x. const wanted = mcpSession === undefined || connection.external diff --git a/apps/server/src/orchestration-v2/CheckpointCaptureService.test.ts b/apps/server/src/orchestration-v2/CheckpointCaptureService.test.ts index 31f5b0004701..91d197dc82dd 100644 --- a/apps/server/src/orchestration-v2/CheckpointCaptureService.test.ts +++ b/apps/server/src/orchestration-v2/CheckpointCaptureService.test.ts @@ -1,3 +1,4 @@ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import { assert, it } from "@effect/vitest"; import { CheckpointId, @@ -274,6 +275,7 @@ it.layer(layerProjectionStoreTest)("CheckpointCaptureServiceV2", (it) => { Layer.provide( Layer.mergeAll( IdAllocator.layer, + NodeCrypto.layer, Layer.mock(CheckpointStore.CheckpointStore)({ isGitRepository: () => Effect.succeed(true), captureCheckpoint: () => Effect.void, diff --git a/apps/server/src/orchestration-v2/CheckpointScopeOwnership.test.ts b/apps/server/src/orchestration-v2/CheckpointScopeOwnership.test.ts index c47c77795f50..4935ffb9fbd4 100644 --- a/apps/server/src/orchestration-v2/CheckpointScopeOwnership.test.ts +++ b/apps/server/src/orchestration-v2/CheckpointScopeOwnership.test.ts @@ -1,3 +1,4 @@ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import { assert, it } from "@effect/vitest"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -24,6 +25,7 @@ const layerProjection = Layer.mergeAll( ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)), SqlitePersistence.layerMemory, IdAllocator.layer, + NodeCrypto.layer, ); it.effect("resolves the thread baseline after a second root run replaces scope ownership", () => Effect.gen(function* () { @@ -38,6 +40,11 @@ it.effect("resolves the thread baseline after a second root run replaces scope o const providerThreadId = ProviderThreadId.make("provider-thread:audit-root-scope"); const providerInstanceId = ProviderInstanceId.make("codex"); const scopeId = yield* ids.allocate.checkpointScope({ threadId, name: "root" }); + const checkpointRefs = [ + yield* checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 0 }), + yield* checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 1 }), + yield* checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 2 }), + ]; const baselineId = CheckpointId.make("checkpoint:audit-root-scope:0"); const firstCheckpointId = CheckpointId.make("checkpoint:audit-root-scope:1"); const secondCheckpointId = CheckpointId.make("checkpoint:audit-root-scope:2"); @@ -154,7 +161,7 @@ it.effect("resolves the thread baseline after a second root run replaces scope o input.ordinal === 0 ? null : input.ordinal === 1 ? baselineId : firstCheckpointId, ordinalWithinScope: input.ordinal, appRunOrdinal: input.appRunOrdinal, - ref: checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: input.ordinal }), + ref: checkpointRefs[input.ordinal]!, status: "ready" as const, files: [], capturedAt: now, @@ -214,14 +221,8 @@ it.effect("resolves the thread baseline after a second root run replaces scope o Layer.mock(CheckpointStore.CheckpointStore)({ diffCheckpoints: (input) => { assert.equal(input.cwd, "/prepared-repo"); - assert.equal( - input.fromCheckpointRef, - checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 0 }), - ); - assert.equal( - input.toCheckpointRef, - checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 2 }), - ); + assert.equal(input.fromCheckpointRef, checkpointRefs[0]); + assert.equal(input.toCheckpointRef, checkpointRefs[2]); return Effect.succeed("two-run diff"); }, }), diff --git a/apps/server/src/orchestration-v2/CheckpointService.test.ts b/apps/server/src/orchestration-v2/CheckpointService.test.ts index 56f21c24d8e2..cef73dd1cbbc 100644 --- a/apps/server/src/orchestration-v2/CheckpointService.test.ts +++ b/apps/server/src/orchestration-v2/CheckpointService.test.ts @@ -1,3 +1,4 @@ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import { assert, it, vi } from "@effect/vitest"; import { CheckpointScopeId, @@ -58,6 +59,7 @@ it.effect.each([false, true, "interrupt"] as const)( }), ), ), + Layer.provideMerge(NodeCrypto.layer), ); return Effect.gen(function* () { @@ -88,7 +90,7 @@ it.effect.each([false, true, "interrupt"] as const)( assert.equal(baseline.ordinalWithinScope, 2); assert.equal( baseline.ref, - CheckpointService.checkpointRefForScopeOrdinal({ + yield* CheckpointService.checkpointRefForScopeOrdinal({ scopeId: scope.id, ordinalWithinScope: 2, }), diff --git a/apps/server/src/orchestration-v2/CheckpointService.ts b/apps/server/src/orchestration-v2/CheckpointService.ts index fcc612e580fb..cb514529349b 100644 --- a/apps/server/src/orchestration-v2/CheckpointService.ts +++ b/apps/server/src/orchestration-v2/CheckpointService.ts @@ -9,12 +9,12 @@ import { RunId, ThreadId, } from "@t3tools/contracts"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Base64Url from "effect/encoding/Base64Url"; +import * as Hex from "effect/encoding/Hex"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; @@ -158,15 +158,18 @@ export class CheckpointServiceV2 extends Context.Service< CheckpointServiceV2Shape >()("t3/orchestration-v2/CheckpointService/CheckpointServiceV2") {} -export function checkpointRefForScopeOrdinal(input: { - readonly scopeId: CheckpointScopeId; - readonly ordinalWithinScope: number; -}): CheckpointRef { - const scopeKey = NodeCrypto.createHash("sha256").update(input.scopeId).digest("hex").slice(0, 32); - return CheckpointRef.make( - `${CHECKPOINT_REFS_PREFIX}/${Base64Url.encode(scopeKey)}/ordinal/${input.ordinalWithinScope}`, - ); -} +export const checkpointRefForScopeOrdinal = Effect.fn("checkpointRefForScopeOrdinal")( + function* (input: { readonly scopeId: CheckpointScopeId; readonly ordinalWithinScope: number }) { + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto + .digest("SHA-256", new TextEncoder().encode(input.scopeId)) + .pipe(Effect.orDie); + const scopeKey = Hex.encode(digest).slice(0, 32); + return CheckpointRef.make( + `${CHECKPOINT_REFS_PREFIX}/${Base64Url.encode(scopeKey)}/ordinal/${input.ordinalWithinScope}`, + ); + }, +); function checkpointIdForScopeOrdinal( idAllocator: IdAllocator.IdAllocatorV2Shape, @@ -243,11 +246,14 @@ function makeCheckpoint(input: { export const layer: Layer.Layer< CheckpointServiceV2, never, - CheckpointStore.CheckpointStore | IdAllocator.IdAllocatorV2 + CheckpointStore.CheckpointStore | Crypto.Crypto | IdAllocator.IdAllocatorV2 > = Layer.effect( CheckpointServiceV2, Effect.gen(function* () { const checkpointStore = yield* CheckpointStore.CheckpointStore; + const crypto = yield* Crypto.Crypto; + const checkpointRefFor = (input: Parameters[0]) => + checkpointRefForScopeOrdinal(input).pipe(Effect.provideService(Crypto.Crypto, crypto)); const idAllocator = yield* IdAllocator.IdAllocatorV2; const workspaceLocks = yield* KeyedLock.make(); const withWorkspaceLock = (cwd: string, effect: Effect.Effect) => @@ -266,7 +272,7 @@ export const layer: Layer.Layer< return; } - const checkpointRef = checkpointRefForScopeOrdinal({ + const checkpointRef = yield* checkpointRefFor({ scopeId: input.scope.id, ordinalWithinScope: input.ordinalWithinScope, }); @@ -299,7 +305,7 @@ export const layer: Layer.Layer< withWorkspaceLock( input.scope.cwd, Effect.gen(function* () { - const checkpointRef = checkpointRefForScopeOrdinal({ + const checkpointRef = yield* checkpointRefFor({ scopeId: input.scope.id, ordinalWithinScope: input.ordinalWithinScope, }); @@ -363,11 +369,11 @@ export const layer: Layer.Layer< ordinalWithinScope: input.ordinalWithinScope - 1, }) : null; - const checkpointRef = checkpointRefForScopeOrdinal({ + const checkpointRef = yield* checkpointRefFor({ scopeId: input.scope.id, ordinalWithinScope: input.ordinalWithinScope, }); - const previousCheckpointRef = checkpointRefForScopeOrdinal({ + const previousCheckpointRef = yield* checkpointRefFor({ scopeId: input.scope.id, ordinalWithinScope: Math.max(0, input.ordinalWithinScope - 1), }); diff --git a/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts b/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts index 61e07308bfc1..a6cac12037e2 100644 --- a/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts +++ b/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts @@ -15,6 +15,7 @@ import { type OrchestrationV2DomainEvent, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; @@ -137,6 +138,7 @@ it.effect.each(["child completion", "scheduled message", "user steering"] as con attachmentsDir: cwd, fileSystem: yield* FileSystem.FileSystem, path: yield* Path.Path, + crypto: yield* Crypto.Crypto, idAllocator: yield* IdAllocator.IdAllocatorV2, queryRunner: { allocateSessionId: Effect.succeed(sessionId), diff --git a/apps/server/src/orchestration-v2/ProviderTurnControlService.ts b/apps/server/src/orchestration-v2/ProviderTurnControlService.ts index 4214a0c83d6e..4421da12da6f 100644 --- a/apps/server/src/orchestration-v2/ProviderTurnControlService.ts +++ b/apps/server/src/orchestration-v2/ProviderTurnControlService.ts @@ -7,13 +7,12 @@ import { RunAttemptId, ThreadId, } from "@t3tools/contracts"; +import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- the stop wait polls against a performance.now() deadline, so it sleeps in real time. -import * as NodeTimersPromises from "node:timers/promises"; import * as ProjectionStore from "./ProjectionStore.ts"; import * as ProviderSessionManager from "./ProviderSessionManager.ts"; @@ -189,17 +188,24 @@ export const layer: Layer.Layer< }); // Give native terminal ingestion time to finish before the Stop // follow-up repairs a run whose provider no longer reports on it. - const deadline = performance.now() + 2_000; - while (loaded.providerTurn.status === "running" && performance.now() < deadline) { - const current = yield* projections.getProviderControlContext(input.threadId, input); - if ( - current.providerTurn?.status !== "running" && - current.attempt?.status !== "running" + // The wait is real time: ingestion runs on other fibers and never + // advances a test clock, so a test clock would hold Stop forever. + yield* Effect.gen(function* () { + const deadline = (yield* Clock.currentTimeMillis) + 2_000; + while ( + loaded.providerTurn.status === "running" && + (yield* Clock.currentTimeMillis) < deadline ) { - break; + const current = yield* projections.getProviderControlContext(input.threadId, input); + if ( + current.providerTurn?.status !== "running" && + current.attempt?.status !== "running" + ) { + return; + } + yield* Effect.sleep("10 millis"); } - yield* Effect.promise(() => NodeTimersPromises.setTimeout(10)); - } + }).pipe(Effect.provideService(Clock.Clock, Clock.Clock.defaultValue())); }).pipe( Effect.mapError((cause) => isProviderTurnControlError(cause) diff --git a/apps/server/src/preview/PreviewBrowser.test.ts b/apps/server/src/preview/PreviewBrowser.test.ts index c5888d0557f5..6e984c2301e8 100644 --- a/apps/server/src/preview/PreviewBrowser.test.ts +++ b/apps/server/src/preview/PreviewBrowser.test.ts @@ -2,9 +2,11 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Deferred from "effect/Deferred"; +import * as Crypto from "effect/Crypto"; import type * as Duration from "effect/Duration"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; @@ -12,8 +14,6 @@ import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import * as NodeZlib from "node:zlib"; import * as PreviewBrowser from "./PreviewBrowser.ts"; @@ -88,6 +88,9 @@ const makeHarness = Effect.fn("test.makePreviewBrowser")(function* ( const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-preview-browser-test-" }); const archive = options.archive ?? browserArchive; + const crypto = yield* Crypto.Crypto; + const sha256 = + options.sha256 ?? Hex.encode(yield* crypto.digest("SHA-256", archive).pipe(Effect.orDie)); const requests: Array = []; const browser = yield* PreviewBrowser.makePreviewBrowser({ baseDir, @@ -98,7 +101,7 @@ const makeHarness = Effect.fn("test.makePreviewBrowser")(function* ( platform: "fixture", url: "https://storage.googleapis.com/chrome-headless-shell-fixture.zip", bytes: archive.byteLength, - sha256: options.sha256 ?? NodeCrypto.createHash("sha256").update(archive).digest("hex"), + sha256, }, ...(options.wait === undefined ? {} : { wait: options.wait }), }).pipe( diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index e65eac27f52a..ac256ef3dcd0 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -6,8 +6,10 @@ import { HostProcessIsExecutable, HostProcessPlatform, } from "@t3tools/shared/hostProcess"; +import * as Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; @@ -21,8 +23,6 @@ import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import * as AntigravityInstallation from "./AntigravityInstallation.ts"; import { ANTIGRAVITY_AUTH_BROWSER_MARKER } from "./antigravityAuthSupport.ts"; @@ -71,14 +71,15 @@ const executableName = hostPlatform === "win32" ? "agy_acp_server.exe" : "agy_ac const harnessName = hostPlatform === "win32" ? "localharness_external.exe" : "localharness_external"; -function releaseAsset( +const releaseAsset = Effect.fn("test.antigravityReleaseAsset")(function* ( archive: Uint8Array = completeArchive, platform: NodeJS.Platform = hostPlatform, ) { + const crypto = yield* Crypto.Crypto; return { version: "fixture-new", url: "https://dl.google.com/antigravity-test.zip", - sha256: NodeCrypto.createHash("sha256").update(archive).digest("hex"), + sha256: Hex.encode(yield* crypto.digest("SHA-256", archive).pipe(Effect.orDie)), archiveBytes: archive.byteLength, executable: { name: platform === "win32" ? "agy_acp_server.exe" : "agy_acp_server.par", @@ -89,7 +90,7 @@ function releaseAsset( bytes: Buffer.byteLength(harnessContents), }, } satisfies AntigravityReleaseAsset; -} +}); const writeRelease = Effect.fn("test.writeAntigravityRelease")(function* ( managedDirectory: string, @@ -147,11 +148,12 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( options.baseDir ?? (yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-test-" })); const platform = options.platform ?? hostPlatform; const archive = options.archive ?? completeArchive; - const asset = options.asset === undefined ? releaseAsset(archive, platform) : options.asset; + const asset = + options.asset === undefined ? yield* releaseAsset(archive, platform) : options.asset; const managedDirectory = path.join(baseDir, "tools", "antigravity-acp", `${platform}-x64`); if (options.previous) { yield* writeRelease(managedDirectory, { - ...releaseAsset(archive, platform), + ...(yield* releaseAsset(archive, platform)), sha256: previousReleaseId, version: previousVersion, }); @@ -302,9 +304,9 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { expect(yield* fs.readFileString(selected.executablePath)).toBe(serverContents); expect(yield* fs.readFileString(selected.harnessPath)).toBe(harnessContents); expect(yield* fs.readDirectory(path.join(installation.managedDirectory, "versions"))).toEqual( - expect.arrayContaining([previousReleaseId, releaseAsset().sha256]), + expect.arrayContaining([previousReleaseId, (yield* releaseAsset()).sha256]), ); - expect(requests).toEqual([releaseAsset().url]); + expect(requests).toEqual([(yield* releaseAsset()).url]); }), ); @@ -470,12 +472,12 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { ); it.effect.each([ - { name: "checksum mismatch", asset: { ...releaseAsset(), sha256: "2".repeat(64) } }, - { name: "short download", archive: completeArchive.subarray(0, -1), asset: releaseAsset() }, + { name: "checksum mismatch", completeAsset: { sha256: "2".repeat(64) } }, + { name: "short download", archive: completeArchive.subarray(0, -1), completeAsset: {} }, { name: "oversized download", archive: Buffer.concat([completeArchive, Buffer.from("extra")]), - asset: releaseAsset(), + completeAsset: {}, }, { name: "wrong Content-Length", contentLength: completeArchive.byteLength + 1 }, { name: "missing harness", archive: Buffer.from(zipFixtures.missingHarness, "base64") }, @@ -483,10 +485,14 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { { name: "path traversal", archive: Buffer.from(zipFixtures.traversal, "base64") }, { name: "symbolic link", archive: Buffer.from(zipFixtures.symlink, "base64") }, { name: "oversized member", archive: Buffer.from(zipFixtures.oversizedMember, "base64") }, - ])("rejects $name before runtime validation", (options) => + ])("rejects $name before runtime validation", ({ completeAsset, ...options }) => Effect.gen(function* () { + // Pin the asset to the complete archive so the download itself is what disagrees. + const asset = + completeAsset === undefined ? undefined : { ...(yield* releaseAsset()), ...completeAsset }; const { installation, validations, stagingReleased, fs, path } = yield* makeHarness({ ...options, + ...(asset === undefined ? {} : { asset }), previous: true, }); yield* installation.start; @@ -894,7 +900,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const archive = Buffer.from(zipFixtures.windows, "base64"); - const asset = releaseAsset(archive, "win32"); + const asset = yield* releaseAsset(archive, "win32"); let denyPointerRename = true; const renameTargets: string[] = []; const { installation, requests, validations } = yield* makeHarness({ diff --git a/apps/server/src/provider/CodexInstallation.test.ts b/apps/server/src/provider/CodexInstallation.test.ts index 8a7dc99f423f..88bc67690125 100644 --- a/apps/server/src/provider/CodexInstallation.test.ts +++ b/apps/server/src/provider/CodexInstallation.test.ts @@ -6,16 +6,16 @@ import { HostProcessEnvironment, HostProcessPlatform, } from "@t3tools/shared/hostProcess"; +import * as Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Exit from "effect/Exit"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/http"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import * as CodexInstallation from "./CodexInstallation.ts"; const archive = Buffer.from( @@ -26,7 +26,6 @@ const asset = { version: "0.156.1", target: "aarch64-apple-darwin", url: "https://github.com/openai/codex/releases/download/test/package.tar.gz", - sha256: NodeCrypto.createHash("sha256").update(archive).digest("hex"), archiveBytes: archive.length, }; const makeHarness = Effect.fn("test.makeCodexInstallation")(function* ( @@ -53,9 +52,11 @@ const makeHarness = Effect.fn("test.makeCodexInstallation")(function* ( ); } let downloads = 0; + const crypto = yield* Crypto.Crypto; + const sha256 = Hex.encode(yield* crypto.digest("SHA-256", archive).pipe(Effect.orDie)); const installation = yield* CodexInstallation.makeCodexInstallation({ baseDir, - releaseAsset: asset, + releaseAsset: { ...asset, sha256 }, validate: () => Effect.void, ...input.options, }).pipe( diff --git a/apps/server/src/provider/CodexToolPresentation.ts b/apps/server/src/provider/CodexToolPresentation.ts index 9a19e20135e6..42561286caec 100644 --- a/apps/server/src/provider/CodexToolPresentation.ts +++ b/apps/server/src/provider/CodexToolPresentation.ts @@ -1,10 +1,11 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import type { ToolActivityIcon, ToolActivityNativeAppReference, ToolActivitySource, } from "@t3tools/contracts"; +import * as Crypto from "effect/Crypto"; +import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import type * as EffectCodexSchema from "effect-codex-app-server/schema"; import { @@ -43,12 +44,17 @@ function normalizedSourceKeyPart(value: string): string { return value.trim().toLowerCase(); } -function nativeAppSourceKey(appId: string): string { +const nativeAppSourceKey = Effect.fn("CodexToolPresentation.nativeAppSourceKey")(function* ( + appId: string, +) { const key = `native-app:${appId.toLowerCase()}`; if (key.length <= 512) return key; - const digest = NodeCrypto.createHash("sha256").update(key).digest("hex"); + const crypto = yield* Crypto.Crypto; + const digest = Hex.encode( + yield* crypto.digest("SHA-256", new TextEncoder().encode(key)).pipe(Effect.orDie), + ); return `${key.slice(0, 512 - digest.length - 1)}:${digest}`; -} +}); function browserDisplayName(value: unknown): string | undefined { const normalized = normalizedDisplayName(value)?.toLowerCase(); @@ -124,105 +130,107 @@ export interface McpToolPresentation { readonly toolSource?: ToolActivitySource; } -export function mcpToolPresentation( - item: Extract, -): McpToolPresentation { - const result = asUnknownRecord(item.result); - const metadata = asUnknownRecord(result?._meta); - const surface = asUnknownRecord(metadata?.["codex/toolSurface"]); - const sourceMetadata = asUnknownRecord(metadata?.source); - const appContext = asUnknownRecord(item.appContext); - const sourceLogo = themedLogoIcon(surface, sourceMetadata, appContext); - if (surface?.kind === "browserUse") { - const screenshot = asUnknownRecord(surface.screenshot); - const browserUse = asUnknownRecord(metadata?.browser_use); - const openTabs = Array.isArray(surface.openTabs) ? surface.openTabs : []; - const latestOpenTab = openTabs - .toReversed() - .map(asUnknownRecord) - .find((tab) => normalizedHttpUrl(tab?.url) !== undefined); - const selectedPage = [ - { record: screenshot, url: screenshot?.pageUrl }, - { record: browserUse, url: browserUse?.url }, - { record: latestOpenTab, url: latestOpenTab?.url }, - ] - .map((candidate) => ({ ...candidate, pageUrl: normalizedHttpUrl(candidate.url) })) - .find((candidate) => candidate.pageUrl !== undefined); - const pageUrl = selectedPage?.pageUrl; - const faviconUrl = normalizedImageUrl( - selectedPage?.record?.faviconUrl ?? selectedPage?.record?.favIconUrl, - ); - const faviconUrlDark = normalizedImageUrl( - selectedPage?.record?.faviconUrlDark ?? selectedPage?.record?.favIconUrlDark, - ); - const name = - browserDisplayName(appContext?.appName) ?? - browserDisplayName(surface.browserFamily) ?? - browserDisplayName(surface.backend) ?? - "Browser"; - const nativeBrowserIcon = browserNativeAppReference(name); - const sourceIcon = - sourceLogo ?? - (nativeBrowserIcon ? ({ _tag: "native-app", app: nativeBrowserIcon } as const) : undefined); - const sourceKeyPart = normalizedSourceKeyPart(name) || "browser"; - return { - toolSurface: "browser", - ...(pageUrl - ? { - toolIcon: { - _tag: "website", - pageUrl, - ...(faviconUrl ? { faviconUrl } : {}), - ...(faviconUrlDark ? { faviconUrlDark } : {}), - } as const, - } - : {}), - toolSource: { - key: `browser-use:${sourceKeyPart}`, - name, - kind: name === "Browser" ? "browser" : "integration", - ...(sourceIcon ? { icon: sourceIcon } : {}), - }, - }; - } - if (surface?.kind === "computerUse") { - const app = nativeAppReference(surface.app); - const args = asUnknownRecord(item.arguments); - const argumentAppName = - normalizedDisplayName(args?.appName) ?? - normalizedDisplayName(args?.application) ?? - normalizedDisplayName(typeof args?.app === "string" ? args.app : undefined); - const name = - normalizedDisplayName(appContext?.appName) ?? - argumentAppName ?? - (app?._tag === "display-name" ? app.displayName : undefined) ?? - (app?._tag === "app-id" ? appDisplayNameFromId(app.appId) : undefined) ?? - "Computer Use"; - const sourceIcon = sourceLogo ?? (app ? ({ _tag: "native-app", app } as const) : undefined); - const sourceKey = app - ? app._tag === "app-id" - ? nativeAppSourceKey(app.appId) - : `native-app-name:${normalizedSourceKeyPart(app.displayName)}` - : "computer-use"; - return { - toolSurface: "computer", - ...(app ? { toolIcon: { _tag: "native-app", app } as const } : {}), - toolSource: { - key: sourceKey, - name, - kind: "computer", - ...(sourceIcon ? { icon: sourceIcon } : {}), - }, - }; - } +export const mcpToolPresentation = Effect.fn("CodexToolPresentation.mcpToolPresentation")( + function* ( + item: Extract, + ): Effect.fn.Return { + const result = asUnknownRecord(item.result); + const metadata = asUnknownRecord(result?._meta); + const surface = asUnknownRecord(metadata?.["codex/toolSurface"]); + const sourceMetadata = asUnknownRecord(metadata?.source); + const appContext = asUnknownRecord(item.appContext); + const sourceLogo = themedLogoIcon(surface, sourceMetadata, appContext); + if (surface?.kind === "browserUse") { + const screenshot = asUnknownRecord(surface.screenshot); + const browserUse = asUnknownRecord(metadata?.browser_use); + const openTabs = Array.isArray(surface.openTabs) ? surface.openTabs : []; + const latestOpenTab = openTabs + .toReversed() + .map(asUnknownRecord) + .find((tab) => normalizedHttpUrl(tab?.url) !== undefined); + const selectedPage = [ + { record: screenshot, url: screenshot?.pageUrl }, + { record: browserUse, url: browserUse?.url }, + { record: latestOpenTab, url: latestOpenTab?.url }, + ] + .map((candidate) => ({ ...candidate, pageUrl: normalizedHttpUrl(candidate.url) })) + .find((candidate) => candidate.pageUrl !== undefined); + const pageUrl = selectedPage?.pageUrl; + const faviconUrl = normalizedImageUrl( + selectedPage?.record?.faviconUrl ?? selectedPage?.record?.favIconUrl, + ); + const faviconUrlDark = normalizedImageUrl( + selectedPage?.record?.faviconUrlDark ?? selectedPage?.record?.favIconUrlDark, + ); + const name = + browserDisplayName(appContext?.appName) ?? + browserDisplayName(surface.browserFamily) ?? + browserDisplayName(surface.backend) ?? + "Browser"; + const nativeBrowserIcon = browserNativeAppReference(name); + const sourceIcon = + sourceLogo ?? + (nativeBrowserIcon ? ({ _tag: "native-app", app: nativeBrowserIcon } as const) : undefined); + const sourceKeyPart = normalizedSourceKeyPart(name) || "browser"; + return { + toolSurface: "browser", + ...(pageUrl + ? { + toolIcon: { + _tag: "website", + pageUrl, + ...(faviconUrl ? { faviconUrl } : {}), + ...(faviconUrlDark ? { faviconUrlDark } : {}), + } as const, + } + : {}), + toolSource: { + key: `browser-use:${sourceKeyPart}`, + name, + kind: name === "Browser" ? "browser" : "integration", + ...(sourceIcon ? { icon: sourceIcon } : {}), + }, + }; + } + if (surface?.kind === "computerUse") { + const app = nativeAppReference(surface.app); + const args = asUnknownRecord(item.arguments); + const argumentAppName = + normalizedDisplayName(args?.appName) ?? + normalizedDisplayName(args?.application) ?? + normalizedDisplayName(typeof args?.app === "string" ? args.app : undefined); + const name = + normalizedDisplayName(appContext?.appName) ?? + argumentAppName ?? + (app?._tag === "display-name" ? app.displayName : undefined) ?? + (app?._tag === "app-id" ? appDisplayNameFromId(app.appId) : undefined) ?? + "Computer Use"; + const sourceIcon = sourceLogo ?? (app ? ({ _tag: "native-app", app } as const) : undefined); + const sourceKey = app + ? app._tag === "app-id" + ? yield* nativeAppSourceKey(app.appId) + : `native-app-name:${normalizedSourceKeyPart(app.displayName)}` + : "computer-use"; + return { + toolSurface: "computer", + ...(app ? { toolIcon: { _tag: "native-app", app } as const } : {}), + toolSource: { + key: sourceKey, + name, + kind: "computer", + ...(sourceIcon ? { icon: sourceIcon } : {}), + }, + }; + } - return integrationToolPresentation({ - serverName: appContext?.connectorId ?? item.server, - toolName: item.tool, - title: appContext?.actionName, - serverDisplayName: appContext?.appName, - source: sourceMetadata, - iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined, - iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined, - }); -} + return integrationToolPresentation({ + serverName: appContext?.connectorId ?? item.server, + toolName: item.tool, + title: appContext?.actionName, + serverDisplayName: appContext?.appName, + source: sourceMetadata, + iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined, + iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined, + }); + }, +); diff --git a/apps/server/src/provider/OpenCodeProvider.test.ts b/apps/server/src/provider/OpenCodeProvider.test.ts index 1b51bb66b3a1..1341e851a80c 100644 --- a/apps/server/src/provider/OpenCodeProvider.test.ts +++ b/apps/server/src/provider/OpenCodeProvider.test.ts @@ -1,6 +1,4 @@ import * as NodeAssert from "node:assert/strict"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; @@ -82,7 +80,8 @@ it.effect("reads Go limits with the instance's XDG credentials and preserves res NodeAssert.equal(limits.unavailable, undefined); NodeAssert.equal( limits.credentialFingerprint, - NodeCrypto.createHash("sha256").update("opencode-go\0instance-key").digest("hex"), + // SHA-256 of "opencode-go\0instance-key". + "aba48e85c981a8edc1c9fb4575121accc7235fa55d0f8689f66f153de5566a37", ); NodeAssert.deepEqual( limits.windows.map(({ kind, usedPercent, resetsAt: reset }) => ({ diff --git a/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts b/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts index ad98a0fa416f..dee416d776ca 100644 --- a/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts +++ b/apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts @@ -1,11 +1,11 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import type { AcpRegistrySettings, ProviderInstanceEnvironment, ProviderInstanceId, } from "@t3tools/contracts"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Ref from "effect/Ref"; @@ -17,8 +17,13 @@ import { writeFileStringAtomically } from "../../atomicWrite.ts"; const decodeState = Schema.decodeUnknownEffect( Schema.fromJsonString(Schema.Struct({ binding: Schema.String, authenticated: Schema.Boolean })), ); -const hash = (value: unknown) => - NodeCrypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +const hash = Effect.fn("AcpRegistryAuthenticationState.hash")(function* (value: unknown) { + const crypto = yield* Crypto.Crypto; + const json = yield* encodeJson(value).pipe(Effect.orDie); + const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(json)).pipe(Effect.orDie); + return Hex.encode(digest); +}); /** Remember explicit sign-in success, never discovery success or the agent's credentials. */ export const makeAcpRegistryAuthenticationState = Effect.fn("makeAcpRegistryAuthenticationState")( @@ -31,10 +36,10 @@ export const makeAcpRegistryAuthenticationState = Effect.fn("makeAcpRegistryAuth }) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const filePath = path.join(input.cacheDir, `acp-auth-${hash(input.instanceId)}.json`); + const filePath = path.join(input.cacheDir, `acp-auth-${yield* hash(input.instanceId)}.json`); // Cosmetic settings and model discovery can rebuild the driver without // changing the account. Credential overrides and profile paths cannot. - const binding = hash({ + const binding = yield* hash({ ...(input.settings.source === "local" ? { source: "local", commandArgs: input.settings.commandArgs } : {}), diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.test.ts b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts index e348d79dfb63..959fab6e256e 100644 --- a/apps/server/src/provider/acp/AcpRegistrySupport.test.ts +++ b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts @@ -16,8 +16,6 @@ import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as TestClock from "effect/testing/TestClock"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; import * as AcpRegistrySupport from "./AcpRegistrySupport.ts"; @@ -588,7 +586,8 @@ describe("AcpRegistrySupport", () => { archive: archiveUrl, cmd: "./bin/example-agent", args: ["acp"], - sha256: NodeCrypto.createHash("sha256").update(binaryBytes).digest("hex"), + // SHA-256 of binaryBytes. + sha256: "97b29a636d7ddf7bf3567ae4d48c0f2a9b03943fd808b6efbf74f1dd3db131b7", }, }, }); @@ -647,7 +646,8 @@ describe("AcpRegistrySupport", () => { archive: archiveUrl, cmd: "./bin/example-agent", args: ["acp"], - sha256: NodeCrypto.createHash("sha256").update(binaryBytes).digest("hex"), + // SHA-256 of binaryBytes. + sha256: "e1eb7c905b91c400af2ec858628aec22866bfda1383b93ceb73ae01d9769fb31", }, }, }); diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.ts b/apps/server/src/provider/acp/AcpRegistrySupport.ts index 6a27896f4784..2f6af2201295 100644 --- a/apps/server/src/provider/acp/AcpRegistrySupport.ts +++ b/apps/server/src/provider/acp/AcpRegistrySupport.ts @@ -25,7 +25,9 @@ import { import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Duration from "effect/Duration"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; @@ -37,8 +39,6 @@ import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash and randomUUID; Effect's Crypto has digest and randomUUIDv4 as Effects. -import * as NodeCrypto from "node:crypto"; import { collectUint8StreamText } from "../../stream/collectUint8StreamText.ts"; import * as ServerSettings from "../../serverSettings.ts"; @@ -598,11 +598,15 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct AcpRegistryCatalog["Service"], never, | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | ServerSettings.ServerSettingsService > { + const crypto = yield* Crypto.Crypto; + const sha256Hex = (data: Uint8Array) => + crypto.digest("SHA-256", data).pipe(Effect.map(Hex.encode), Effect.orDie); const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const httpClient = yield* HttpClient.HttpClient; @@ -732,17 +736,20 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct .readFileString(registryCachePath) .pipe(Effect.flatMap(decodeRegistryText), Effect.option); - const writeRegistryCache = (text: string) => { - const temporaryPath = `${registryCachePath}.${process.pid}-${NodeCrypto.randomUUID()}.tmp`; - return fileSystem - .makeDirectory(registryDirectory, { recursive: true }) - .pipe( - Effect.andThen(fileSystem.writeFileString(temporaryPath, text)), - Effect.andThen(fileSystem.rename(temporaryPath, registryCachePath)), - Effect.ensuring(fileSystem.remove(temporaryPath, { force: true }).pipe(Effect.ignore)), - Effect.ignore, - ); - }; + const writeRegistryCache = (text: string) => + crypto.randomUUIDv4.pipe( + Effect.flatMap((id) => { + const temporaryPath = `${registryCachePath}.${process.pid}-${id}.tmp`; + return fileSystem + .makeDirectory(registryDirectory, { recursive: true }) + .pipe( + Effect.andThen(fileSystem.writeFileString(temporaryPath, text)), + Effect.andThen(fileSystem.rename(temporaryPath, registryCachePath)), + Effect.ensuring(fileSystem.remove(temporaryPath, { force: true }).pipe(Effect.ignore)), + ); + }), + Effect.ignore, + ); const fetchRegistry = Effect.fn("AcpRegistryCatalog.fetchRegistry")(function* () { yield* assertHttpsUrl(registryUrl, "ACP Registry index URL must use HTTPS."); @@ -929,17 +936,12 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct ); }); - const packageReceiptPath = ( - agentId: string, - distribution: "npx" | "uvx", - managerPath: string, - ) => { - const managerId = NodeCrypto.createHash("sha256") - .update(`${distribution}\0${managerPath}`) - .digest("hex") - .slice(0, 16); - return path.join(packageReceiptsDirectory, `${agentId}-${managerId}.json`); - }; + const packageReceiptPath = (agentId: string, distribution: "npx" | "uvx", managerPath: string) => + sha256Hex(new TextEncoder().encode(`${distribution}\0${managerPath}`)).pipe( + Effect.map((digest) => + path.join(packageReceiptsDirectory, `${agentId}-${digest.slice(0, 16)}.json`), + ), + ); const readPackageReceipt = Effect.fn("AcpRegistryCatalog.readPackageReceipt")(function* ( agent: AcpRegistryAgent, @@ -948,7 +950,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct managerPath: string, ) { const receipt = yield* fileSystem - .readFileString(packageReceiptPath(agent.id, distribution, managerPath)) + .readFileString(yield* packageReceiptPath(agent.id, distribution, managerPath)) .pipe( Effect.map(decodePackageInstallReceipt), Effect.orElseSucceed(() => Option.none()), @@ -992,7 +994,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const writePackageReceipt = Effect.fn("AcpRegistryCatalog.writePackageReceipt")( function* (receipt: AcpRegistryPackageInstallReceipt) { - const receiptPath = packageReceiptPath( + const receiptPath = yield* packageReceiptPath( receipt.agentId, receipt.distribution, receipt.managerPath, @@ -1410,7 +1412,6 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct ), }), ); - const hash = NodeCrypto.createHash("sha256"); let downloadedBytes = 0; yield* response.stream.pipe( Stream.tap((chunk) => { @@ -1423,7 +1424,6 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct ); } downloadedBytes += chunk.byteLength; - hash.update(chunk); return Effect.void; }), Stream.run(fileSystem.sink(archivePath)), @@ -1448,7 +1448,17 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct }), ); if (target.sha256 !== undefined) { - const actual = hash.digest("hex"); + const actual = yield* fileSystem.readFile(archivePath).pipe( + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "download_failed", + detail: `Could not read ACP Registry agent ${agent.id} ${agent.version} download.`, + cause, + }), + ), + Effect.flatMap(sha256Hex), + ); if (actual !== target.sha256.toLowerCase()) { return yield* new AcpRegistryError({ reason: "checksum_mismatch", diff --git a/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts b/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts index ae42600c94ac..21c01be67185 100644 --- a/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts +++ b/apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts @@ -10,6 +10,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { AbsolutePath, Location, Model, Provider } from "@opencode/client/effect"; import { assert, it } from "@effect/vitest"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Base64Url from "effect/encoding/Base64Url"; import * as Fiber from "effect/Fiber"; @@ -21,8 +22,6 @@ import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; import { FetchHttpClient } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous randomBytes; Effect's Crypto.randomBytes is an Effect. -import * as NodeCrypto from "node:crypto"; import { describe } from "vite-plus/test"; import * as OpenCode2Client from "./OpenCode2Client.ts"; @@ -37,7 +36,8 @@ const startServer = Effect.fn("OpenCode2ClientLive.startServer")(function* (bina const directory = path.join(root, "work"); yield* fs.makeDirectory(directory); // Non-ASCII on purpose: OpenCode decodes Basic credentials as UTF-8. - const password = `${Base64Url.encode(NodeCrypto.randomBytes(32))}-pässwörd€`; + const crypto = yield* Crypto.Crypto; + const password = `${Base64Url.encode(yield* crypto.randomBytes(32))}-pässwörd€`; const child = yield* spawner.spawn( ChildProcess.make(binary, ["serve", "--hostname=127.0.0.1", "--port=0"], { cwd: directory, diff --git a/apps/server/src/telemetry/Identify.test.ts b/apps/server/src/telemetry/Identify.test.ts index 5b9e8f221876..b556499547aa 100644 --- a/apps/server/src/telemetry/Identify.test.ts +++ b/apps/server/src/telemetry/Identify.test.ts @@ -1,8 +1,8 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; @@ -18,8 +18,12 @@ interface CapturedLog { readonly annotations: Readonly>; } -const sha256 = (value: string) => - NodeCrypto.createHash("sha256").update(value, "utf8").digest("hex"); +const sha256 = Effect.fn("test.sha256")(function* (value: string) { + const crypto = yield* Crypto.Crypto; + return Hex.encode( + yield* crypto.digest("SHA-256", new TextEncoder().encode(value)).pipe(Effect.orDie), + ); +}); const makeCaptureLogger = (logs: CapturedLog[]) => Logger.make(({ fiber, message }) => { @@ -49,7 +53,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { path.join(config.baseDir, "home"), ); - assert.equal(identifier, sha256(anonymousId)); + assert.equal(identifier, yield* sha256(anonymousId)); }).pipe( Effect.provide( ServerConfig.layerTest(process.cwd(), { @@ -133,7 +137,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); - assert.equal(identifier, sha256(anonymousId)); + assert.equal(identifier, yield* sha256(anonymousId)); assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError")); assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityReadError")); const allLogs = logs @@ -176,7 +180,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); - assert.equal(identifier, sha256(anonymousId)); + assert.equal(identifier, yield* sha256(anonymousId)); const decodeLog = findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError"); assert.isDefined(decodeLog); assert.equal( diff --git a/apps/server/src/usage/cliproxyApi.test.ts b/apps/server/src/usage/cliproxyApi.test.ts index 7e6e727ddbad..5de08f9a0dc9 100644 --- a/apps/server/src/usage/cliproxyApi.test.ts +++ b/apps/server/src/usage/cliproxyApi.test.ts @@ -1,10 +1,11 @@ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; -import { creditRedeemRequestId, makeCliproxyApi } from "./cliproxyApi.ts"; +import { makeCliproxyApi } from "./cliproxyApi.ts"; const config = { kind: "cliproxy", @@ -106,7 +107,10 @@ function fixture( ); return { requests, - api: makeCliproxyApi.pipe(Effect.provideService(HttpClient.HttpClient, http)), + api: makeCliproxyApi.pipe( + Effect.provideService(HttpClient.HttpClient, http), + Effect.provide(NodeCrypto.layer), + ), }; } @@ -222,7 +226,8 @@ describe("CLIProxyAPI built-in management API", () => { expect(redemptions[0]?.body?.data).toBe(redemptions[1]?.body?.data); expect(redemptions[0]?.body?.data).toBe( encodeJson({ - redeem_request_id: creditRedeemRequestId("account-b", "credit-b"), + // UUIDv5 of "account-b:credit-b"; must stay stable so retries deduplicate. + redeem_request_id: "519d5243-011a-5b7b-91f3-44d85f095705", credit_id: "credit-b", }), ); diff --git a/apps/server/src/usage/cliproxyApi.ts b/apps/server/src/usage/cliproxyApi.ts index 9ee91331d300..3804b9649105 100644 --- a/apps/server/src/usage/cliproxyApi.ts +++ b/apps/server/src/usage/cliproxyApi.ts @@ -1,6 +1,3 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; - import { ProviderDriverKind, UsageLimitSourceError, @@ -8,8 +5,10 @@ import { type UsageLimitSourceAccount, type UsageLimitSourceConfig, } from "@t3tools/contracts"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as Schema from "effect/Schema"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; @@ -100,21 +99,31 @@ const decodeConsumeResponse = Schema.decodeUnknownEffect( const CODEX_BASE = "https://chatgpt.com/backend-api/wham"; const CREDIT_URL = `${CODEX_BASE}/rate-limit-reset-credits`; +// 6f1c2a9e-2d4b-4c1e-9a7f-3b8d5e0c1a42 +const CREDIT_REDEEM_NAMESPACE = new Uint8Array([ + 0x6f, 0x1c, 0x2a, 0x9e, 0x2d, 0x4b, 0x4c, 0x1e, 0x9a, 0x7f, 0x3b, 0x8d, 0x5e, 0x0c, 0x1a, 0x42, +]); + // UUIDv5 per account and credit also deduplicates retries across T3 environments. -export function creditRedeemRequestId(accountId: string, creditId: string): string { - const bytes = NodeCrypto.createHash("sha1") - .update(Buffer.from("6f1c2a9e2d4b4c1e9a7f3b8d5e0c1a42", "hex")) - .update(`${accountId}:${creditId}`) - .digest() - .subarray(0, 16); +const creditRedeemRequestId = Effect.fn("CliproxyApi.creditRedeemRequestId")(function* ( + accountId: string, + creditId: string, +) { + const crypto = yield* Crypto.Crypto; + const name = new TextEncoder().encode(`${accountId}:${creditId}`); + const input = new Uint8Array(CREDIT_REDEEM_NAMESPACE.length + name.length); + input.set(CREDIT_REDEEM_NAMESPACE); + input.set(name, CREDIT_REDEEM_NAMESPACE.length); + const bytes = (yield* crypto.digest("SHA-1", input).pipe(Effect.orDie)).slice(0, 16); bytes[6] = (bytes[6]! & 0x0f) | 0x50; bytes[8] = (bytes[8]! & 0x3f) | 0x80; - const hex = bytes.toString("hex"); + const hex = Hex.encode(bytes); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; -} +}); export const makeCliproxyApi = Effect.gen(function* () { const client = yield* HttpClient.HttpClient; + const crypto = yield* Crypto.Crypto; const management = Effect.fn("CliproxyApi.management")(function* ( config: UsageLimitSourceConfig, @@ -324,10 +333,10 @@ export const makeCliproxyApi = Effect.gen(function* () { }); } const body = yield* apiCall(config, account, `${CREDIT_URL}/consume`, { - redeem_request_id: creditRedeemRequestId( + redeem_request_id: yield* creditRedeemRequestId( account.id_token?.chatgpt_account_id ?? account.id, creditId, - ), + ).pipe(Effect.provideService(Crypto.Crypto, crypto)), credit_id: creditId, }); const response = yield* decodeConsumeResponse(body); diff --git a/infra/relay/src/environments/ManagedEndpointProvider.test.ts b/infra/relay/src/environments/ManagedEndpointProvider.test.ts index 31c6953f0457..b48a01cc6d03 100644 --- a/infra/relay/src/environments/ManagedEndpointProvider.test.ts +++ b/infra/relay/src/environments/ManagedEndpointProvider.test.ts @@ -1,5 +1,3 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; @@ -366,20 +364,26 @@ function layerProvider( ); } +// First 16 hex chars of SHA-256(`dev_julius:${userId}:${environmentId}`), pinned so a +// change to the endpoint naming scheme fails here instead of silently matching. +const MANAGED_ENDPOINT_HASHES: Record = { + "user_ABC:env_ABC": "d101ac68108a423e", + "user_ABC:env_shared": "d7e6356aaf8863aa", + "user_DEF:env_shared": "a2fc84ac1b8c1c35", +}; + +function expectedManagedEndpointHash(environmentId: string, userId: string): string { + const hash = MANAGED_ENDPOINT_HASHES[`${userId}:${environmentId}`]; + if (hash === undefined) throw new Error(`No pinned hash for ${userId}:${environmentId}`); + return hash; +} + function expectedManagedHostname(environmentId: string, userId = "user_ABC"): string { - const hash = NodeCrypto.createHash("sha256") - .update(`dev_julius:${userId}:${environmentId}`) - .digest("hex") - .slice(0, 16); - return `dev-julius-${hash}.t3code.test`; + return `dev-julius-${expectedManagedEndpointHash(environmentId, userId)}.t3code.test`; } function expectedManagedTunnelName(environmentId: string, userId = "user_ABC"): string { - const hash = NodeCrypto.createHash("sha256") - .update(`dev_julius:${userId}:${environmentId}`) - .digest("hex") - .slice(0, 16); - return `t3coderelay-managedendpoint-dev-julius-${hash}`; + return `t3coderelay-managedendpoint-dev-julius-${expectedManagedEndpointHash(environmentId, userId)}`; } describe("ManagedEndpointProvider", () => { diff --git a/packages/ssh/src/command.test.ts b/packages/ssh/src/command.test.ts index 2837470652d9..d6da8b417f9c 100644 --- a/packages/ssh/src/command.test.ts +++ b/packages/ssh/src/command.test.ts @@ -14,6 +14,7 @@ import { baseSshArgs, getLastNonEmptyOutputLine, parseSshResolveOutput, + remoteStateKey, runSshCommand, } from "./command.ts"; import { SshCommandError } from "./errors.ts"; @@ -98,6 +99,30 @@ describe("ssh command", () => { }), ); + // Remote servers store state under this key, so it must not change across releases. + it.effect("derives a stable remote state key", () => + Effect.gen(function* () { + assert.equal( + yield* remoteStateKey({ + alias: "devbox", + hostname: "devbox.example.com", + username: "julius", + port: 2222, + }), + "711bc738002d72fd", + ); + assert.equal( + yield* remoteStateKey({ + alias: "fixture", + hostname: "fixture", + username: null, + port: null, + }), + "326264c4f08c8a0c", + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("reads the last non-empty ssh output line", () => Effect.sync(() => { assert.equal( diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index b0fda9a14334..8c9e2d304618 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -1,10 +1,9 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- uses Node's synchronous createHash; Effect's Crypto.digest is an Effect. -import * as NodeCrypto from "node:crypto"; - import type { DesktopSshEnvironmentTarget } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Crypto from "effect/Crypto"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; +import * as Hex from "effect/encoding/Hex"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; @@ -73,12 +72,16 @@ export function targetConnectionKey(target: DesktopSshEnvironmentTarget): string return `${target.alias}\u0000${target.hostname}\u0000${target.username ?? ""}\u0000${target.port ?? ""}`; } -export function remoteStateKey(target: DesktopSshEnvironmentTarget): string { - return NodeCrypto.createHash("sha256") - .update(targetConnectionKey(target)) - .digest("hex") - .slice(0, 16); -} +/** Names the remote state directory for a target: the first 16 hex chars of its SHA-256 key. */ +export const remoteStateKey = Effect.fn("ssh/command.remoteStateKey")(function* ( + target: DesktopSshEnvironmentTarget, +): Effect.fn.Return { + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto + .digest("SHA-256", encoder.encode(targetConnectionKey(target))) + .pipe(Effect.orDie); + return Hex.encode(digest).slice(0, 16); +}); function buildSshHostSpec(target: DesktopSshEnvironmentTarget): string { const destination = target.alias.trim() || target.hostname.trim(); diff --git a/packages/ssh/src/runnerProcess.test.ts b/packages/ssh/src/runnerProcess.test.ts index 4add0cb5784c..bc6064628843 100644 --- a/packages/ssh/src/runnerProcess.test.ts +++ b/packages/ssh/src/runnerProcess.test.ts @@ -11,6 +11,7 @@ import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; import * as NodeNet from "node:net"; +import { remoteStateKey } from "./command.ts"; import { buildRemoteStopScript, buildRemoteT3RunnerScript } from "./tunnel.ts"; const Started = Schema.Struct({ @@ -186,12 +187,14 @@ server.listen(0, "127.0.0.1", () => { for (const [name, contents] of Object.entries(savedState)) { yield* fs.writeFileString(path.join(fixture, name), contents); } - const script = buildRemoteStopScript({ - alias: "fixture", - hostname: "fixture", - username: null, - port: null, - }); + const script = buildRemoteStopScript( + yield* remoteStateKey({ + alias: "fixture", + hostname: "fixture", + username: null, + port: null, + }), + ); // Redirect only the state directory. Never use the developer's SSH state. const isolatedScript = script.replace( /^STATE_DIR=.*$/mu, diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 3cf8c62c5b50..d0943d1e382f 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -233,12 +233,7 @@ describe("ssh tunnel scripts", () => { }); it("uses the remote t3 runner for launch and pairing scripts", () => { - const target = { - alias: "devbox", - hostname: "devbox.example.com", - username: "julius", - port: 2222, - } as const; + const stateKey = "711bc738002d72fd"; const launch = SshTunnel.buildRemoteLaunchScript(ARCHIVE); const devLaunch = SshTunnel.buildRemoteLaunchScript({ ...NODE_SCRIPT, @@ -265,28 +260,28 @@ describe("ssh tunnel scripts", () => { assert.include(launch, "It wrote nothing to %s"); assert.include(launch, "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'"); assert.include( - SshTunnel.buildRemotePairingScript(target, ARCHIVE), + SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), '"$RUNNER_FILE" auth pairing create --base-dir "$PAIRING_BASE_DIR" --json', ); assert.include( - SshTunnel.buildRemotePairingScript(target, ARCHIVE), + SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), 'PAIRING_BASE_DIR="$DEFAULT_SERVER_HOME"', ); - assert.notInclude(SshTunnel.buildRemotePairingScript(target, ARCHIVE), "server-home"); + assert.notInclude(SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), "server-home"); assert.include( - SshTunnel.buildRemotePairingScript(target, ARCHIVE), + SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'", ); assert.include( - SshTunnel.buildRemoteStopScript(target), + SshTunnel.buildRemoteStopScript(stateKey), 'if [ "$REMOTE_MANAGED" != "external" ] && [ -n "$REMOTE_PID" ]', ); assert.include( - SshTunnel.buildRemoteStopScript(target), + SshTunnel.buildRemoteStopScript(stateKey), 'kill "$REMOTE_PID" 2>/dev/null || true', ); assert.include( - SshTunnel.buildRemoteStopScript(target), + SshTunnel.buildRemoteStopScript(stateKey), 'rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"', ); assert.include( diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index c77fe13cb818..6d70e381c04a 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -12,6 +12,7 @@ import * as NetService from "@t3tools/shared/Net"; import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson"; import { satisfiesSemverRange } from "@t3tools/shared/semver"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; @@ -98,6 +99,7 @@ interface SshTunnelEntry { type SshEnvironmentEffectContext = | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | Path.Path | HttpClient.HttpClient @@ -837,25 +839,22 @@ export function buildRemoteLaunchScript(input?: RemoteT3RunnerOptions): string { }); } -export function buildRemotePairingScript( - target: DesktopSshEnvironmentTarget, - input?: RemoteT3RunnerOptions, -): string { +export function buildRemotePairingScript(stateKey: string, input?: RemoteT3RunnerOptions): string { return applyScriptPlaceholders(REMOTE_PAIRING_SCRIPT, { - T3_STATE_KEY: remoteStateKey(target), + T3_STATE_KEY: stateKey, T3_RUNNER_SCRIPT: stripTrailingNewlines(buildRemoteT3RunnerScript(input)), }); } -export function buildRemoteStopScript(target: DesktopSshEnvironmentTarget): string { +export function buildRemoteStopScript(stateKey: string): string { return applyScriptPlaceholders(REMOTE_STOP_SCRIPT, { - T3_STATE_KEY: remoteStateKey(target), + T3_STATE_KEY: stateKey, }); } -function buildRemoteLogTailScript(target: DesktopSshEnvironmentTarget): string { +function buildRemoteLogTailScript(stateKey: string): string { return applyScriptPlaceholders(REMOTE_LOG_TAIL_SCRIPT, { - T3_STATE_KEY: remoteStateKey(target), + T3_STATE_KEY: stateKey, }); } @@ -867,15 +866,16 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo ): Effect.fn.Return< { readonly remotePort: number; readonly remoteServerKind: "external" | "managed" | null }, SshCommandError | SshInvalidTargetError | SshLaunchError, - ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path + ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path > { + const stateKey = yield* remoteStateKey(target); yield* Effect.logInfo("ssh.remoteServer.launch.start", { ...sshTargetLogFields(target), ...sshRunnerLogFields(runner), - stateKey: remoteStateKey(target), + stateKey, }); const result = yield* runSshCommand(target, { - remoteCommandArgs: ["sh", "-l", "-s", "--", remoteStateKey(target)], + remoteCommandArgs: ["sh", "-l", "-s", "--", stateKey], stdin: buildRemoteLaunchScript(runner), timeoutMs: isNodeScriptRunner(runner) ? REMOTE_LAUNCH_TIMEOUT_MS @@ -910,7 +910,7 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo ...sshTargetLogFields(target), remotePort: parsed.remotePort, remoteServerKind: parsed.serverKind ?? null, - stateKey: remoteStateKey(target), + stateKey, }); return { remotePort: parsed.remotePort, @@ -928,15 +928,16 @@ export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingT readonly credential: string; }, SshCommandError | SshInvalidTargetError | SshPairingError, - ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path + ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path > { + const stateKey = yield* remoteStateKey(target); yield* Effect.logDebug("ssh.remoteServer.pairingToken.start", { ...sshTargetLogFields(target), - stateKey: remoteStateKey(target), + stateKey, }); const result = yield* runSshCommand(target, { remoteCommandArgs: ["sh", "-s"], - stdin: buildRemotePairingScript(target, runner), + stdin: buildRemotePairingScript(stateKey, runner), // Pairing may be the first command on a cold remote, so it can install // the archive on the way. ...(isNodeScriptRunner(runner) ? {} : { timeoutMs: REMOTE_ARCHIVE_LAUNCH_TIMEOUT_MS }), @@ -968,7 +969,7 @@ export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingT } yield* Effect.logDebug("ssh.remoteServer.pairingToken.created", { ...sshTargetLogFields(target), - stateKey: remoteStateKey(target), + stateKey, }); return { credential: parsed.credential, @@ -981,22 +982,23 @@ const stopRemoteServer = Effect.fn("ssh/tunnel.stopRemoteServer")(function* ( ): Effect.fn.Return< void, SshCommandError | SshInvalidTargetError, - ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path + ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path > { + const stateKey = yield* remoteStateKey(target); yield* Effect.logInfo("ssh.remoteServer.stop.start", { ...sshTargetLogFields(target), - stateKey: remoteStateKey(target), + stateKey, }); yield* runSshCommand(target, { remoteCommandArgs: ["sh", "-s"], - stdin: buildRemoteStopScript(target), + stdin: buildRemoteStopScript(stateKey), ...(input?.authSecret === undefined ? {} : { authSecret: input.authSecret }), ...(input?.batchMode === undefined ? {} : { batchMode: input.batchMode }), ...(input?.interactiveAuth === undefined ? {} : { interactiveAuth: input.interactiveAuth }), }); yield* Effect.logInfo("ssh.remoteServer.stop.succeeded", { ...sshTargetLogFields(target), - stateKey: remoteStateKey(target), + stateKey, }); }); @@ -1006,11 +1008,11 @@ const readRemoteServerLogTail = Effect.fn("ssh/tunnel.readRemoteServerLogTail")( ): Effect.fn.Return< string, SshCommandError | SshInvalidTargetError, - ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path + ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path > { const result = yield* runSshCommand(target, { remoteCommandArgs: ["sh", "-s"], - stdin: buildRemoteLogTailScript(target), + stdin: buildRemoteLogTailScript(yield* remoteStateKey(target)), timeoutMs: 10_000, ...(input?.authSecret === undefined ? {} : { authSecret: input.authSecret }), ...(input?.batchMode === undefined ? {} : { batchMode: input.batchMode }), @@ -1109,6 +1111,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: SshTunnelEntry, SshCommandError | SshInvalidTargetError | SshReadinessError, | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | Path.Path | HttpClient.HttpClient @@ -1538,6 +1541,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma ); tunnels.set(input.key, tunnelEntry); const spawnerService = yield* ChildProcessSpawner.ChildProcessSpawner; + const cryptoService = yield* Crypto.Crypto; const fileSystemService = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; yield* Scope.addFinalizer( @@ -1577,6 +1581,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma }, ).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawnerService), + Effect.provideService(Crypto.Crypto, cryptoService), Effect.provideService(FileSystem.FileSystem, fileSystemService), Effect.provideService(Path.Path, pathService), ); @@ -1757,7 +1762,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma }); /** - * @effect-expect-leaking ChildProcessSpawner | FileSystem | HttpClient | NetService | Path | SshPasswordPrompt + * @effect-expect-leaking ChildProcessSpawner | Crypto | FileSystem | HttpClient | NetService | Path | SshPasswordPrompt */ export class SshEnvironmentManager extends Context.Service< SshEnvironmentManager, From 01971443d60af537832825d0a1e80855aa2d9ae5 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:30:56 -0700 Subject: [PATCH 5/6] fix(desktop): catch the fetch error tag with catchTags Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/desktop/src/electron/ElectronProtocol.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index c82f41f21996..7b2bf8a59fc9 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -279,7 +279,9 @@ export const make = Effect.gen(function* () { // Reject with net.fetch's own error, as an unproxied fetch would. return runPromise( proxyRequest(request, input.targetOrigin, contentSecurityPolicy).pipe( - Effect.catchTag("ElectronProtocolFetchError", (error) => Effect.die(error.cause)), + Effect.catchTags({ + ElectronProtocolFetchError: (error) => Effect.die(error.cause), + }), ), ); }); From ce147bfb5a2f8647d2c97f7b1dae630630c677d0 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:36:50 -0700 Subject: [PATCH 6/6] fix(server): hash ACP registry archives while streaming Reading the downloaded archive back to verify its SHA-256 loaded up to 1 GiB into memory. Hash each chunk as it streams with @noble/hashes, since Effect's Crypto only digests a whole buffer. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/server/package.json | 1 + .../src/provider/acp/AcpRegistrySupport.ts | 17 ++++++----------- pnpm-lock.yaml | 3 +++ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 1c86a4b402da..a323511d3e86 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -34,6 +34,7 @@ "@effect/platform-node-shared": "catalog:", "@ff-labs/fff-node": "0.9.4", "@napi-rs/keyring": "^1.3.0", + "@noble/hashes": "catalog:", "@opencode-ai/sdk": "^1.3.15", "@opencode/client": "2.0.23", "@opencode/protocol": "2.0.23", diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.ts b/apps/server/src/provider/acp/AcpRegistrySupport.ts index 2f6af2201295..5eb8842f5667 100644 --- a/apps/server/src/provider/acp/AcpRegistrySupport.ts +++ b/apps/server/src/provider/acp/AcpRegistrySupport.ts @@ -37,6 +37,7 @@ import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; +import { sha256 } from "@noble/hashes/sha2"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; @@ -1412,6 +1413,9 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct ), }), ); + // Hash while streaming: archives can be up to MAX_ARCHIVE_BYTES, and + // Effect's Crypto only digests a whole buffer. + const hash = sha256.create(); let downloadedBytes = 0; yield* response.stream.pipe( Stream.tap((chunk) => { @@ -1424,6 +1428,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct ); } downloadedBytes += chunk.byteLength; + hash.update(chunk); return Effect.void; }), Stream.run(fileSystem.sink(archivePath)), @@ -1448,17 +1453,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct }), ); if (target.sha256 !== undefined) { - const actual = yield* fileSystem.readFile(archivePath).pipe( - Effect.mapError( - (cause) => - new AcpRegistryError({ - reason: "download_failed", - detail: `Could not read ACP Registry agent ${agent.id} ${agent.version} download.`, - cause, - }), - ), - Effect.flatMap(sha256Hex), - ); + const actual = Hex.encode(hash.digest()); if (actual !== target.sha256.toLowerCase()) { return yield* new AcpRegistryError({ reason: "checksum_mismatch", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a4be5acb3d8..f8418904ef4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -540,6 +540,9 @@ importers: '@napi-rs/keyring': specifier: ^1.3.0 version: 1.3.0 + '@noble/hashes': + specifier: 'catalog:' + version: 1.8.0 '@opencode-ai/sdk': specifier: ^1.3.15 version: 1.15.13