| author | |
| committer | |
| log | 78552a8c6162c9b5248231bc70be8bffdb50cbd3 |
| tree | ee48d1dc45597e71f184354e297bb29f872bb833 |
| parent | b38aa18365fe314e079d99670932bedfc7407e5a |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>72 files changed, 965 insertions(+), 651 deletions(-)
apps/desktop/src/electron/ElectronProtocol.test.ts+51-1| ... | @@ -1,9 +1,11 @@ | ... | @@ -1,9 +1,11 @@ |
| 1 | import { assert, describe, it } from "@effect/vitest"; | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as Cause from "effect/Cause"; | 2 | import * as Cause from "effect/Cause"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Fiber from "effect/Fiber"; | ||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 5 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 8 | import * as TestClock from "effect/testing/TestClock"; | ||
| 7 | import { beforeEach, vi } from "vite-plus/test"; | 9 | import { beforeEach, vi } from "vite-plus/test"; |
| 8 | 10 | ||
| 9 | const { handleMock, netFetchMock, unhandleMock } = vi.hoisted(() => ({ | 11 | const { handleMock, netFetchMock, unhandleMock } = vi.hoisted(() => ({ |
| ... | @@ -174,7 +176,11 @@ describe("ElectronProtocol", () => { | ... | @@ -174,7 +176,11 @@ describe("ElectronProtocol", () => { |
| 174 | targetOrigin: new URL("http://127.0.0.1:5733/"), | 176 | targetOrigin: new URL("http://127.0.0.1:5733/"), |
| 175 | clerkFrontendApiHostname: undefined, | 177 | clerkFrontendApiHostname: undefined, |
| 176 | }); | 178 | }); |
| 177 | return yield* Effect.promise(() => handler!(new Request("t3code-dev://app/"))); | 179 | const fiber = yield* Effect.forkChild( |
| 180 | Effect.promise(() => handler!(new Request("t3code-dev://app/"))), | ||
| 181 | ); | ||
| 182 | yield* TestClock.adjust("50 millis"); | ||
| 183 | return yield* Fiber.join(fiber); | ||
| 178 | }), | 184 | }), |
| 179 | ); | 185 | ); |
| 180 | 186 | ||
| ... | @@ -183,6 +189,50 @@ describe("ElectronProtocol", () => { | ... | @@ -183,6 +189,50 @@ describe("ElectronProtocol", () => { |
| 183 | }).pipe(Effect.provide(layerProtocol)), | 189 | }).pipe(Effect.provide(layerProtocol)), |
| 184 | ); | 190 | ); |
| 185 | 191 | ||
| 192 | it.effect("rejects with the last renderer target failure after 50ms and 150ms retries", () => | ||
| 193 | Effect.gen(function* () { | ||
| 194 | let handler: ((request: Request) => Promise<Response>) | undefined; | ||
| 195 | handleMock.mockImplementation((_scheme, nextHandler) => { | ||
| 196 | handler = nextHandler; | ||
| 197 | }); | ||
| 198 | const lastFailure = new Error("connect ECONNREFUSED 127.0.0.1:5733 (3)"); | ||
| 199 | netFetchMock | ||
| 200 | .mockRejectedValueOnce(new Error("connect ECONNREFUSED 127.0.0.1:5733 (1)")) | ||
| 201 | .mockRejectedValueOnce(new Error("connect ECONNREFUSED 127.0.0.1:5733 (2)")) | ||
| 202 | .mockRejectedValueOnce(lastFailure); | ||
| 203 | |||
| 204 | const rejection = yield* Effect.scoped( | ||
| 205 | Effect.gen(function* () { | ||
| 206 | const protocol = yield* ElectronProtocol.ElectronProtocol; | ||
| 207 | yield* protocol.registerDesktopProtocol({ | ||
| 208 | scheme: "t3code-dev", | ||
| 209 | targetOrigin: new URL("http://127.0.0.1:5733/"), | ||
| 210 | clerkFrontendApiHostname: undefined, | ||
| 211 | }); | ||
| 212 | const fiber = yield* Effect.forkChild( | ||
| 213 | Effect.promise(() => | ||
| 214 | handler!(new Request("t3code-dev://app/")).then( | ||
| 215 | () => null, | ||
| 216 | (error: unknown) => error, | ||
| 217 | ), | ||
| 218 | ), | ||
| 219 | ); | ||
| 220 | yield* TestClock.adjust("49 millis"); | ||
| 221 | assert.equal(netFetchMock.mock.calls.length, 1); | ||
| 222 | yield* TestClock.adjust("1 millis"); | ||
| 223 | assert.equal(netFetchMock.mock.calls.length, 2); | ||
| 224 | yield* TestClock.adjust("149 millis"); | ||
| 225 | assert.equal(netFetchMock.mock.calls.length, 2); | ||
| 226 | yield* TestClock.adjust("1 millis"); | ||
| 227 | return yield* Fiber.join(fiber); | ||
| 228 | }), | ||
| 229 | ); | ||
| 230 | |||
| 231 | assert.strictEqual(rejection, lastFailure); | ||
| 232 | assert.equal(netFetchMock.mock.calls.length, 3); | ||
| 233 | }).pipe(Effect.provide(layerProtocol)), | ||
| 234 | ); | ||
| 235 | |||
| 186 | it.effect("preserves protocol registration failures", () => | 236 | it.effect("preserves protocol registration failures", () => |
| 187 | Effect.gen(function* () { | 237 | Effect.gen(function* () { |
| 188 | const cause = new Error("protocol registration failed"); | 238 | const cause = new Error("protocol registration failed"); |
apps/desktop/src/electron/ElectronProtocol.ts+32-27| ... | @@ -3,10 +3,10 @@ import * as Effect from "effect/Effect"; | ... | @@ -3,10 +3,10 @@ import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; | 5 | import * as Option from "effect/Option"; |
| 6 | import * as NodeTimersPromises from "node:timers/promises"; | ||
| 7 | import * as Path from "effect/Path"; | 6 | import * as Path from "effect/Path"; |
| 8 | import * as Mime from "effect/http/Mime"; | 7 | import * as Mime from "effect/http/Mime"; |
| 9 | import * as Ref from "effect/Ref"; | 8 | import * as Ref from "effect/Ref"; |
| 9 | import * as Schedule from "effect/Schedule"; | ||
| 10 | import * as Schema from "effect/Schema"; | 10 | import * as Schema from "effect/Schema"; |
| 11 | import * as Scope from "effect/Scope"; | 11 | import * as Scope from "effect/Scope"; |
| 12 | 12 | ||
| ... | @@ -150,11 +150,29 @@ const registerDesktopSchemePrivileges = Effect.sync(registerDesktopSchemePrivile | ... | @@ -150,11 +150,29 @@ const registerDesktopSchemePrivileges = Effect.sync(registerDesktopSchemePrivile |
| 150 | 150 | ||
| 151 | export const layerSchemePrivileges = Layer.effectDiscard(registerDesktopSchemePrivileges); | 151 | export const layerSchemePrivileges = Layer.effectDiscard(registerDesktopSchemePrivileges); |
| 152 | 152 | ||
| 153 | async function proxyRequest( | 153 | class ElectronProtocolFetchError extends Schema.TaggedError<ElectronProtocolFetchError>()( |
| 154 | "ElectronProtocolFetchError", | ||
| 155 | { cause: Schema.Defect() }, | ||
| 156 | ) {} | ||
| 157 | |||
| 158 | const netFetch = (url: string, init: RequestInit) => | ||
| 159 | Effect.tryPromise({ | ||
| 160 | try: () => Electron.net.fetch(url, init), | ||
| 161 | catch: (cause) => new ElectronProtocolFetchError({ cause }), | ||
| 162 | }); | ||
| 163 | |||
| 164 | // The dev renderer target can briefly refuse connections while Vite restarts: | ||
| 165 | // retry idempotent requests after 50ms, then 150ms, and keep the last failure. | ||
| 166 | const fetchWithTransientRetry = (url: string, init: RequestInit) => | ||
| 167 | netFetch(url, init).pipe( | ||
| 168 | Effect.retry({ schedule: Schedule.exponential("50 millis", 3), times: 2 }), | ||
| 169 | ); | ||
| 170 | |||
| 171 | const proxyRequest = Effect.fn("desktop.protocol.proxyRequest")(function* ( | ||
| 154 | request: Request, | 172 | request: Request, |
| 155 | targetOrigin: URL, | 173 | targetOrigin: URL, |
| 156 | contentSecurityPolicy: string, | 174 | contentSecurityPolicy: string, |
| 157 | ): Promise<Response> { | 175 | ) { |
| 158 | const requestUrl = new URL(request.url); | 176 | const requestUrl = new URL(request.url); |
| 159 | if (requestUrl.host !== DESKTOP_HOST) { | 177 | if (requestUrl.host !== DESKTOP_HOST) { |
| 160 | return new Response(null, { status: 404 }); | 178 | return new Response(null, { status: 404 }); |
| ... | @@ -190,12 +208,10 @@ async function proxyRequest( | ... | @@ -190,12 +208,10 @@ async function proxyRequest( |
| 190 | } | 208 | } |
| 191 | const response = | 209 | const response = |
| 192 | request.method === "GET" || request.method === "HEAD" | 210 | request.method === "GET" || request.method === "HEAD" |
| 193 | ? await fetchWithTransientRetry(targetUrl.toString(), init) | 211 | ? yield* fetchWithTransientRetry(targetUrl.toString(), init) |
| 194 | : await Electron.net.fetch(targetUrl.toString(), init); | 212 | : yield* netFetch(targetUrl.toString(), init); |
| 195 | return withContentSecurityPolicy(response, contentSecurityPolicy); | 213 | return withContentSecurityPolicy(response, contentSecurityPolicy); |
| 196 | } | 214 | }); |
| 197 | |||
| 198 | const TRANSIENT_FETCH_RETRY_DELAYS_MS = [0, 50, 150] as const; | ||
| 199 | 215 | ||
| 200 | // Serves the packaged web client without a backend: files resolve within the | 216 | // Serves the packaged web client without a backend: files resolve within the |
| 201 | // asset directory, and any other path falls back to index.html so the SPA | 217 | // asset directory, and any other path falls back to index.html so the SPA |
| ... | @@ -238,24 +254,6 @@ const serveDesktopAsset = Effect.fn("desktop.protocol.serveAsset")(function* ( | ... | @@ -238,24 +254,6 @@ const serveDesktopAsset = Effect.fn("desktop.protocol.serveAsset")(function* ( |
| 238 | }); | 254 | }); |
| 239 | }); | 255 | }); |
| 240 | 256 | ||
| 241 | async function fetchWithTransientRetry(url: string, init: RequestInit): Promise<Response> { | ||
| 242 | let lastError: unknown; | ||
| 243 | |||
| 244 | for (const delayMs of TRANSIENT_FETCH_RETRY_DELAYS_MS) { | ||
| 245 | if (delayMs > 0) { | ||
| 246 | await NodeTimersPromises.setTimeout(delayMs); | ||
| 247 | } | ||
| 248 | |||
| 249 | try { | ||
| 250 | return await Electron.net.fetch(url, init); | ||
| 251 | } catch (error) { | ||
| 252 | lastError = error; | ||
| 253 | } | ||
| 254 | } | ||
| 255 | |||
| 256 | throw lastError; | ||
| 257 | } | ||
| 258 | |||
| 259 | /** @public Service construction is part of the canonical Effect module API. */ | 257 | /** @public Service construction is part of the canonical Effect module API. */ |
| 260 | export const make = Effect.gen(function* () { | 258 | export const make = Effect.gen(function* () { |
| 261 | const registered = yield* Ref.make(false); | 259 | const registered = yield* Ref.make(false); |
| ... | @@ -278,7 +276,14 @@ export const make = Effect.gen(function* () { | ... | @@ -278,7 +276,14 @@ export const make = Effect.gen(function* () { |
| 278 | contentSecurityPolicy, | 276 | contentSecurityPolicy, |
| 279 | ); | 277 | ); |
| 280 | } | 278 | } |
| 281 | return proxyRequest(request, input.targetOrigin, contentSecurityPolicy); | 279 | // Reject with net.fetch's own error, as an unproxied fetch would. |
| 280 | return runPromise( | ||
| 281 | proxyRequest(request, input.targetOrigin, contentSecurityPolicy).pipe( | ||
| 282 | Effect.catchTags({ | ||
| 283 | ElectronProtocolFetchError: (error) => Effect.die(error.cause), | ||
| 284 | }), | ||
| 285 | ), | ||
| 286 | ); | ||
| 282 | }); | 287 | }); |
| 283 | }, | 288 | }, |
| 284 | catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }), | 289 | catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }), |
apps/desktop/src/ssh/DesktopSshEnvironment.ts+2| ... | @@ -18,6 +18,7 @@ import { | ... | @@ -18,6 +18,7 @@ import { |
| 18 | } from "@t3tools/ssh/errors"; | 18 | } from "@t3tools/ssh/errors"; |
| 19 | import * as SshTunnel from "@t3tools/ssh/tunnel"; | 19 | import * as SshTunnel from "@t3tools/ssh/tunnel"; |
| 20 | import * as Context from "effect/Context"; | 20 | import * as Context from "effect/Context"; |
| 21 | import type * as Crypto from "effect/Crypto"; | ||
| 21 | import * as Effect from "effect/Effect"; | 22 | import * as Effect from "effect/Effect"; |
| 22 | import * as FileSystem from "effect/FileSystem"; | 23 | import * as FileSystem from "effect/FileSystem"; |
| 23 | import * as Layer from "effect/Layer"; | 24 | import * as Layer from "effect/Layer"; |
| ... | @@ -29,6 +30,7 @@ import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts"; | ... | @@ -29,6 +30,7 @@ import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts"; |
| 29 | 30 | ||
| 30 | export type DesktopSshEnvironmentRuntimeServices = | 31 | export type DesktopSshEnvironmentRuntimeServices = |
| 31 | | ChildProcessSpawner.ChildProcessSpawner | 32 | | ChildProcessSpawner.ChildProcessSpawner |
| 33 | | Crypto.Crypto | ||
| 32 | | FileSystem.FileSystem | 34 | | FileSystem.FileSystem |
| 33 | | Path.Path | 35 | | Path.Path |
| 34 | | HttpClient.HttpClient | 36 | | HttpClient.HttpClient |
apps/server/package.json+1| ... | @@ -34,6 +34,7 @@ | ... | @@ -34,6 +34,7 @@ |
| 34 | "@effect/platform-node-shared": "catalog:", | 34 | "@effect/platform-node-shared": "catalog:", |
| 35 | "@ff-labs/fff-node": "0.9.4", | 35 | "@ff-labs/fff-node": "0.9.4", |
| 36 | "@napi-rs/keyring": "^1.3.0", | 36 | "@napi-rs/keyring": "^1.3.0", |
| 37 | "@noble/hashes": "catalog:", | ||
| 37 | "@opencode-ai/sdk": "^1.3.15", | 38 | "@opencode-ai/sdk": "^1.3.15", |
| 38 | "@opencode/client": "2.0.23", | 39 | "@opencode/client": "2.0.23", |
| 39 | "@opencode/protocol": "2.0.23", | 40 | "@opencode/protocol": "2.0.23", |
apps/server/src/auth/ReusableDevAuth.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no timingSafeEqual. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | import { AuthSessionId } from "@t3tools/contracts"; | 3 | import { AuthSessionId } from "@t3tools/contracts"; |
| 3 | import * as DateTime from "effect/DateTime"; | 4 | import * as DateTime from "effect/DateTime"; |
apps/server/src/auth/utils.ts+1| ... | @@ -4,6 +4,7 @@ import type { | ... | @@ -4,6 +4,7 @@ import type { |
| 4 | AuthClientPresentationMetadata, | 4 | AuthClientPresentationMetadata, |
| 5 | } from "@t3tools/contracts"; | 5 | } from "@t3tools/contracts"; |
| 6 | import type * as HttpServerRequest from "effect/http/HttpServerRequest"; | 6 | import type * as HttpServerRequest from "effect/http/HttpServerRequest"; |
| 7 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. | ||
| 7 | import * as NodeCrypto from "node:crypto"; | 8 | import * as NodeCrypto from "node:crypto"; |
| 8 | import * as Base64Url from "effect/encoding/Base64Url"; | 9 | import * as Base64Url from "effect/encoding/Base64Url"; |
| 9 | import * as Result from "effect/Result"; | 10 | import * as Result from "effect/Result"; |
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+3-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { assert, it, vi } from "@effect/vitest"; | 2 | import { assert, it, vi } from "@effect/vitest"; |
| 2 | import { CheckpointRef, CheckpointScopeId, RunId, ThreadId } from "@t3tools/contracts"; | 3 | import { CheckpointRef, CheckpointScopeId, RunId, ThreadId } from "@t3tools/contracts"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| ... | @@ -59,6 +60,7 @@ function layerFor(input: { | ... | @@ -59,6 +60,7 @@ function layerFor(input: { |
| 59 | }), | 60 | }), |
| 60 | ), | 61 | ), |
| 61 | ), | 62 | ), |
| 63 | Layer.provideMerge(NodeCrypto.layer), | ||
| 62 | ); | 64 | ); |
| 63 | } | 65 | } |
| 64 | 66 | ||
| ... | @@ -80,7 +82,7 @@ it.effect("computes V2 run diffs from projected checkpoint scopes", () => { | ... | @@ -80,7 +82,7 @@ it.effect("computes V2 run diffs from projected checkpoint scopes", () => { |
| 80 | }); | 82 | }); |
| 81 | assert.deepEqual(diffCheckpoints.mock.calls[0]?.[0], { | 83 | assert.deepEqual(diffCheckpoints.mock.calls[0]?.[0], { |
| 82 | cwd: "/repo", | 84 | cwd: "/repo", |
| 83 | fromCheckpointRef: checkpointRefForScopeOrdinal({ | 85 | fromCheckpointRef: yield* checkpointRefForScopeOrdinal({ |
| 84 | scopeId: firstScopeId, | 86 | scopeId: firstScopeId, |
| 85 | ordinalWithinScope: 0, | 87 | ordinalWithinScope: 0, |
| 86 | }), | 88 | }), |
apps/server/src/checkpointing/CheckpointDiffQuery.ts+14-13| ... | @@ -15,6 +15,7 @@ import { | ... | @@ -15,6 +15,7 @@ import { |
| 15 | type ThreadId, | 15 | type ThreadId, |
| 16 | } from "@t3tools/contracts"; | 16 | } from "@t3tools/contracts"; |
| 17 | import * as Context from "effect/Context"; | 17 | import * as Context from "effect/Context"; |
| 18 | import * as Crypto from "effect/Crypto"; | ||
| 18 | import * as Effect from "effect/Effect"; | 19 | import * as Effect from "effect/Effect"; |
| 19 | import * as Layer from "effect/Layer"; | 20 | import * as Layer from "effect/Layer"; |
| 20 | import * as Schema from "effect/Schema"; | 21 | import * as Schema from "effect/Schema"; |
| ... | @@ -77,6 +78,7 @@ function buildTurnDiffResult( | ... | @@ -77,6 +78,7 @@ function buildTurnDiffResult( |
| 77 | export const make = Effect.gen(function* () { | 78 | export const make = Effect.gen(function* () { |
| 78 | const threads = yield* ThreadManagement.ThreadManagementService; | 79 | const threads = yield* ThreadManagement.ThreadManagementService; |
| 79 | const checkpointStore = yield* CheckpointStore.CheckpointStore; | 80 | const checkpointStore = yield* CheckpointStore.CheckpointStore; |
| 81 | const crypto = yield* Crypto.Crypto; | ||
| 80 | 82 | ||
| 81 | const getTurnDiff: CheckpointDiffQuery["Service"]["getTurnDiff"] = Effect.fn("getTurnDiff")( | 83 | const getTurnDiff: CheckpointDiffQuery["Service"]["getTurnDiff"] = Effect.fn("getTurnDiff")( |
| 82 | function* (input) { | 84 | function* (input) { |
| ... | @@ -155,21 +157,20 @@ export const make = Effect.gen(function* () { | ... | @@ -155,21 +157,20 @@ export const make = Effect.gen(function* () { |
| 155 | }); | 157 | }); |
| 156 | } | 158 | } |
| 157 | 159 | ||
| 160 | // The root scope is shared by every run in this thread. Its runId | ||
| 161 | // tracks the latest owner, while ordinal zero stays the baseline. | ||
| 162 | const firstScope = | ||
| 163 | input.fromTurnCount === 0 | ||
| 164 | ? projection.checkpointScopes.find((scope) => scope.kind === "root_run") | ||
| 165 | : undefined; | ||
| 158 | const fromCheckpointRef = | 166 | const fromCheckpointRef = |
| 159 | input.fromTurnCount === 0 | 167 | input.fromTurnCount === 0 |
| 160 | ? (() => { | 168 | ? firstScope === undefined |
| 161 | // The root scope is shared by every run in this thread. Its | 169 | ? undefined |
| 162 | // runId tracks the latest owner, while ordinal zero stays the baseline. | 170 | : yield* checkpointRefForScopeOrdinal({ |
| 163 | const firstScope = projection.checkpointScopes.find( | 171 | scopeId: firstScope.id, |
| 164 | (scope) => scope.kind === "root_run", | 172 | ordinalWithinScope: 0, |
| 165 | ); | 173 | }).pipe(Effect.provideService(Crypto.Crypto, crypto)) |
| 166 | return firstScope === undefined | ||
| 167 | ? undefined | ||
| 168 | : checkpointRefForScopeOrdinal({ | ||
| 169 | scopeId: firstScope.id, | ||
| 170 | ordinalWithinScope: 0, | ||
| 171 | }); | ||
| 172 | })() | ||
| 173 | : readyCheckpoints.find((checkpoint) => checkpoint.appRunOrdinal === input.fromTurnCount) | 174 | : readyCheckpoints.find((checkpoint) => checkpoint.appRunOrdinal === input.fromTurnCount) |
| 174 | ?.ref; | 175 | ?.ref; |
| 175 | if (fromCheckpointRef === undefined) { | 176 | if (fromCheckpointRef === undefined) { |
apps/server/src/cloud/CloudLink.ts+1| ... | @@ -4,6 +4,7 @@ | ... | @@ -4,6 +4,7 @@ |
| 4 | * and mint requests, and keeping the managed tunnel registered, recovered and | 4 | * and mint requests, and keeping the managed tunnel registered, recovered and |
| 5 | * released. HTTP handlers, server startup and shutdown all go through it. | 5 | * released. HTTP handlers, server startup and shutdown all go through it. |
| 6 | */ | 6 | */ |
| 7 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createPublicKey. | ||
| 7 | import * as NodeCrypto from "node:crypto"; | 8 | import * as NodeCrypto from "node:crypto"; |
| 8 | import { | 9 | import { |
| 9 | AuthStandardClientScopes, | 10 | AuthStandardClientScopes, |
apps/server/src/cloud/environmentKeys.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; | 4 | import * as Option from "effect/Option"; |
apps/server/src/device/AgentDeviceTarget.test.ts+5-3| ... | @@ -41,7 +41,9 @@ console.log(readFileSync(args[args.indexOf('--config') + 1], 'utf8')); | ... | @@ -41,7 +41,9 @@ console.log(readFileSync(args[args.indexOf('--config') + 1], 'utf8')); |
| 41 | if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, | 41 | if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, |
| 42 | ); | 42 | ); |
| 43 | const shim = yield* ensureAgentDeviceShim({ entryPath, stateDir: dir }); | 43 | const shim = yield* ensureAgentDeviceShim({ entryPath, stateDir: dir }); |
| 44 | const files = ["mini", "android"].map((host) => agentDeviceConfigPath(dir, host, path)); | 44 | const files = yield* Effect.forEach(["mini", "android"], (host) => |
| 45 | agentDeviceConfigPath(dir, host, path), | ||
| 46 | ); | ||
| 45 | for (const [index, file] of files.entries()) | 47 | for (const [index, file] of files.entries()) |
| 46 | yield* writeAgentDeviceConfig(file, { | 48 | yield* writeAgentDeviceConfig(file, { |
| 47 | baseUrl: `http://127.0.0.1:${1000 + index}`, | 49 | baseUrl: `http://127.0.0.1:${1000 + index}`, |
| ... | @@ -73,8 +75,8 @@ if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, | ... | @@ -73,8 +75,8 @@ if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, |
| 73 | }); | 75 | }); |
| 74 | expect((yield* Effect.promise(() => invoke(files[0]!))).daemonAuthToken).toBe("new"); | 76 | expect((yield* Effect.promise(() => invoke(files[0]!))).daemonAuthToken).toBe("new"); |
| 75 | expect(yield* fs.readFileString(files[1]!)).toBe(second); | 77 | expect(yield* fs.readFileString(files[1]!)).toBe(second); |
| 76 | expect(agentDeviceSession("thread", "mini", "same-id")).not.toBe( | 78 | expect(yield* agentDeviceSession("thread", "mini", "same-id")).not.toBe( |
| 77 | agentDeviceSession("thread", "android", "same-id"), | 79 | yield* agentDeviceSession("thread", "android", "same-id"), |
| 78 | ); | 80 | ); |
| 79 | for (const args of [ | 81 | for (const args of [ |
| 80 | ["snapshot"], | 82 | ["snapshot"], |
apps/server/src/device/AgentDeviceTarget.ts+12-6| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | 1 | import * as Crypto from "effect/Crypto"; |
| 2 | import * as Schema from "effect/Schema"; | ||
| 3 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Hex from "effect/encoding/Hex"; | ||
| 4 | import * as Schema from "effect/Schema"; | ||
| 4 | import * as FileSystem from "effect/FileSystem"; | 5 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Path from "effect/Path"; | 6 | import * as Path from "effect/Path"; |
| 6 | 7 | ||
| ... | @@ -12,15 +13,20 @@ const encodeEndpoint = Schema.encodeEffect( | ... | @@ -12,15 +13,20 @@ const encodeEndpoint = Schema.encodeEffect( |
| 12 | ), | 13 | ), |
| 13 | ); | 14 | ); |
| 14 | 15 | ||
| 15 | const key = (value: string) => | 16 | const key = Effect.fn("AgentDeviceTarget.key")(function* (value: string) { |
| 16 | NodeCrypto.createHash("sha256").update(value).digest("hex").slice(0, 24); | 17 | const crypto = yield* Crypto.Crypto; |
| 18 | const digest = yield* crypto | ||
| 19 | .digest("SHA-256", new TextEncoder().encode(value)) | ||
| 20 | .pipe(Effect.orDie); | ||
| 21 | return Hex.encode(digest).slice(0, 24); | ||
| 22 | }); | ||
| 17 | 23 | ||
| 18 | /** A stable file per host lets forwarded endpoints change without retargeting other commands. */ | 24 | /** A stable file per host lets forwarded endpoints change without retargeting other commands. */ |
| 19 | export const agentDeviceConfigPath = (stateDir: string, hostId: string, path: Path.Path) => | 25 | export const agentDeviceConfigPath = (stateDir: string, hostId: string, path: Path.Path) => |
| 20 | path.join(stateDir, "device", "hosts", `${key(hostId)}.json`); | 26 | key(hostId).pipe(Effect.map((hash) => path.join(stateDir, "device", "hosts", `${hash}.json`))); |
| 21 | 27 | ||
| 22 | export const agentDeviceSession = (threadId: string, hostId: string, deviceId: string) => | 28 | export const agentDeviceSession = (threadId: string, hostId: string, deviceId: string) => |
| 23 | `t3-${key(JSON.stringify([threadId, hostId, deviceId]))}`; | 29 | key(JSON.stringify([threadId, hostId, deviceId])).pipe(Effect.map((hash) => `t3-${hash}`)); |
| 24 | 30 | ||
| 25 | export const writeAgentDeviceConfig = Effect.fn("AgentDeviceTarget.writeConfig")(function* ( | 31 | export const writeAgentDeviceConfig = Effect.fn("AgentDeviceTarget.writeConfig")(function* ( |
| 26 | file: string, | 32 | file: string, |
apps/server/src/device/DeviceMultiHost.test.ts+2-1| ... | @@ -2,6 +2,7 @@ import { expect, it } from "@effect/vitest"; | ... | @@ -2,6 +2,7 @@ import { expect, it } from "@effect/vitest"; |
| 2 | import { ThreadId } from "@t3tools/contracts"; | 2 | import { ThreadId } from "@t3tools/contracts"; |
| 3 | import * as Deferred from "effect/Deferred"; | 3 | import * as Deferred from "effect/Deferred"; |
| 4 | import * as Fiber from "effect/Fiber"; | 4 | import * as Fiber from "effect/Fiber"; |
| 5 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 5 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 6 | import { HttpClient, HttpClientResponse } from "effect/http"; | 7 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 7 | import * as ServerSettings from "../serverSettings.ts"; | 8 | import * as ServerSettings from "../serverSettings.ts"; |
| ... | @@ -77,7 +78,7 @@ it.effect("keeps hosts independent when serials collide and another host fails", | ... | @@ -77,7 +78,7 @@ it.effect("keeps hosts independent when serials collide and another host fails", |
| 77 | order.push("write finished"); | 78 | order.push("write finished"); |
| 78 | return "/host-config.json"; | 79 | return "/host-config.json"; |
| 79 | }), | 80 | }), |
| 80 | ).pipe(Effect.provideService(HttpClient.HttpClient, http)); | 81 | ).pipe(Effect.provide(NodeCrypto.layer), Effect.provideService(HttpClient.HttpClient, http)); |
| 81 | expect(yield* service.agentReadinessIfSupported("b")).not.toBeNull(); | 82 | expect(yield* service.agentReadinessIfSupported("b")).not.toBeNull(); |
| 82 | const listed = yield* service.list; | 83 | const listed = yield* service.list; |
| 83 | expect(listed.devices.map((device) => device.hostId).sort()).toEqual(["a", "b"]); | 84 | expect(listed.devices.map((device) => device.hostId).sort()).toEqual(["a", "b"]); |
apps/server/src/device/DeviceService.test.ts+4| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | ThreadId, | 7 | ThreadId, |
| 8 | type DeviceServiceState, | 8 | type DeviceServiceState, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 10 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 11 | import * as Exit from "effect/Exit"; | 12 | import * as Exit from "effect/Exit"; |
| 12 | import * as Deferred from "effect/Deferred"; | 13 | import * as Deferred from "effect/Deferred"; |
| ... | @@ -135,6 +136,7 @@ const fixture = Effect.fn("fixture")(function* ( | ... | @@ -135,6 +136,7 @@ const fixture = Effect.fn("fixture")(function* ( |
| 135 | undefined, | 136 | undefined, |
| 136 | installTool, | 137 | installTool, |
| 137 | ).pipe( | 138 | ).pipe( |
| 139 | Effect.provide(NodeCrypto.layer), | ||
| 138 | Effect.provideService(DeviceHost.DeviceHost, host), | 140 | Effect.provideService(DeviceHost.DeviceHost, host), |
| 139 | Effect.provideService( | 141 | Effect.provideService( |
| 140 | ServerSettings.ServerSettingsService, | 142 | ServerSettings.ServerSettingsService, |
| ... | @@ -482,6 +484,7 @@ it.effect.each(["shutdown", "close"] as const)( | ... | @@ -482,6 +484,7 @@ it.effect.each(["shutdown", "close"] as const)( |
| 482 | }), | 484 | }), |
| 483 | ); | 485 | ); |
| 484 | const service = yield* DeviceService.makeWithHosts(new Map([[host.id, host]])).pipe( | 486 | const service = yield* DeviceService.makeWithHosts(new Map([[host.id, host]])).pipe( |
| 487 | Effect.provide(NodeCrypto.layer), | ||
| 485 | Effect.provideService(HttpClient.HttpClient, http), | 488 | Effect.provideService(HttpClient.HttpClient, http), |
| 486 | ); | 489 | ); |
| 487 | const input = { threadId, deviceId, platform: "ios" as const }; | 490 | const input = { threadId, deviceId, platform: "ios" as const }; |
| ... | @@ -574,6 +577,7 @@ it.effect.each([ | ... | @@ -574,6 +577,7 @@ it.effect.each([ |
| 574 | }), | 577 | }), |
| 575 | ); | 578 | ); |
| 576 | const service = yield* DeviceService.makeWithHosts(new Map([[host.id, host]])).pipe( | 579 | const service = yield* DeviceService.makeWithHosts(new Map([[host.id, host]])).pipe( |
| 580 | Effect.provide(NodeCrypto.layer), | ||
| 577 | Effect.provideService(HttpClient.HttpClient, http), | 581 | Effect.provideService(HttpClient.HttpClient, http), |
| 578 | ); | 582 | ); |
| 579 | yield* service.list; | 583 | yield* service.list; |
apps/server/src/device/DeviceService.ts+20-14| ... | @@ -46,6 +46,7 @@ import { | ... | @@ -46,6 +46,7 @@ import { |
| 46 | writeAgentDeviceConfig, | 46 | writeAgentDeviceConfig, |
| 47 | } from "./AgentDeviceTarget.ts"; | 47 | } from "./AgentDeviceTarget.ts"; |
| 48 | import * as Context from "effect/Context"; | 48 | import * as Context from "effect/Context"; |
| 49 | import * as Crypto from "effect/Crypto"; | ||
| 49 | import * as DateTime from "effect/DateTime"; | 50 | import * as DateTime from "effect/DateTime"; |
| 50 | import * as Duration from "effect/Duration"; | 51 | import * as Duration from "effect/Duration"; |
| 51 | import * as Effect from "effect/Effect"; | 52 | import * as Effect from "effect/Effect"; |
| ... | @@ -186,6 +187,7 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* | ... | @@ -186,6 +187,7 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* |
| 186 | installTool?: (tool: "hub" | "agent") => Effect.Effect<unknown, DeviceError>, | 187 | installTool?: (tool: "hub" | "agent") => Effect.Effect<unknown, DeviceError>, |
| 187 | ) { | 188 | ) { |
| 188 | const settings = yield* ServerSettings.ServerSettingsService; | 189 | const settings = yield* ServerSettings.ServerSettingsService; |
| 190 | const crypto = yield* Crypto.Crypto; | ||
| 189 | const lifecycleLock = yield* Semaphore.make(1); | 191 | const lifecycleLock = yield* Semaphore.make(1); |
| 190 | const readDeviceSettings = settings.getSettings.pipe( | 192 | const readDeviceSettings = settings.getSettings.pipe( |
| 191 | Effect.map((value) => ({ | 193 | Effect.map((value) => ({ |
| ... | @@ -943,12 +945,12 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* | ... | @@ -943,12 +945,12 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* |
| 943 | return yield* configureAgent(input.hostId, ready); | 945 | return yield* configureAgent(input.hostId, ready); |
| 944 | }), | 946 | }), |
| 945 | ); | 947 | ); |
| 946 | return [ | 948 | const session = yield* agentDeviceSession( |
| 947 | "--config", | 949 | input.threadId, |
| 948 | configPath, | 950 | input.hostId, |
| 949 | "--session", | 951 | input.deviceId, |
| 950 | agentDeviceSession(input.threadId, input.hostId, input.deviceId), | 952 | ).pipe(Effect.provideService(Crypto.Crypto, crypto)); |
| 951 | ]; | 953 | return ["--config", configPath, "--session", session]; |
| 952 | }), | 954 | }), |
| 953 | state: SynchronizedRef.get(stateRef).pipe(Effect.map(({ state }) => state)), | 955 | state: SynchronizedRef.get(stateRef).pipe(Effect.map(({ state }) => state)), |
| 954 | subscribe: PubSub.subscribe(statePubSub), | 956 | subscribe: PubSub.subscribe(statePubSub), |
| ... | @@ -998,9 +1000,17 @@ export const make = Effect.gen(function* () { | ... | @@ -998,9 +1000,17 @@ export const make = Effect.gen(function* () { |
| 998 | const hosts = new Map<DeviceHostId, DeviceHost.DeviceHost["Service"]>([ | 1000 | const hosts = new Map<DeviceHostId, DeviceHost.DeviceHost["Service"]>([ |
| 999 | [localHost.id, localHost], | 1001 | [localHost.id, localHost], |
| 1000 | ]); | 1002 | ]); |
| 1001 | const configureAgent = (hostId: DeviceHostId, ready: DeviceHost.DeviceHostAgentReady) => { | 1003 | const crypto = yield* Crypto.Crypto; |
| 1002 | const file = agentDeviceConfigPath(config.stateDir, hostId, path); | 1004 | const configPath = (hostId: DeviceHostId) => |
| 1003 | return writeAgentDeviceConfig(file, ready.agentDevice).pipe( | 1005 | agentDeviceConfigPath(config.stateDir, hostId, path).pipe( |
| 1006 | Effect.provideService(Crypto.Crypto, crypto), | ||
| 1007 | ); | ||
| 1008 | const configureAgent = (hostId: DeviceHostId, ready: DeviceHost.DeviceHostAgentReady) => | ||
| 1009 | Effect.gen(function* () { | ||
| 1010 | const file = yield* configPath(hostId); | ||
| 1011 | yield* writeAgentDeviceConfig(file, ready.agentDevice); | ||
| 1012 | return file; | ||
| 1013 | }).pipe( | ||
| 1004 | Effect.provideService(FileSystem.FileSystem, fs), | 1014 | Effect.provideService(FileSystem.FileSystem, fs), |
| 1005 | Effect.provideService(Path.Path, path), | 1015 | Effect.provideService(Path.Path, path), |
| 1006 | Effect.mapError( | 1016 | Effect.mapError( |
| ... | @@ -1011,9 +1021,7 @@ export const make = Effect.gen(function* () { | ... | @@ -1011,9 +1021,7 @@ export const make = Effect.gen(function* () { |
| 1011 | cause, | 1021 | cause, |
| 1012 | }), | 1022 | }), |
| 1013 | ), | 1023 | ), |
| 1014 | Effect.as(file), | ||
| 1015 | ); | 1024 | ); |
| 1016 | }; | ||
| 1017 | const probeContext = | 1025 | const probeContext = |
| 1018 | yield* Effect.context<Effect.Services<ReturnType<typeof SshDeviceHost.probe>>>(); | 1026 | yield* Effect.context<Effect.Services<ReturnType<typeof SshDeviceHost.probe>>>(); |
| 1019 | const localTargetContext = | 1027 | const localTargetContext = |
| ... | @@ -1089,9 +1097,7 @@ export const make = Effect.gen(function* () { | ... | @@ -1089,9 +1097,7 @@ export const make = Effect.gen(function* () { |
| 1089 | ({ id, scope }) => | 1097 | ({ id, scope }) => |
| 1090 | Effect.gen(function* () { | 1098 | Effect.gen(function* () { |
| 1091 | yield* Scope.close(scope, Exit.void); | 1099 | yield* Scope.close(scope, Exit.void); |
| 1092 | yield* fs | 1100 | yield* fs.remove(yield* configPath(id), { force: true }).pipe(Effect.ignore); |
| 1093 | .remove(agentDeviceConfigPath(config.stateDir, id, path), { force: true }) | ||
| 1094 | .pipe(Effect.ignore); | ||
| 1095 | }), | 1101 | }), |
| 1096 | { concurrency: 4, discard: true }, | 1102 | { concurrency: 4, discard: true }, |
| 1097 | ); | 1103 | ); |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+54-32| ... | @@ -32,6 +32,7 @@ import { assert, describe, it } from "@effect/vitest"; | ... | @@ -32,6 +32,7 @@ import { assert, describe, it } from "@effect/vitest"; |
| 32 | import * as Context from "effect/Context"; | 32 | import * as Context from "effect/Context"; |
| 33 | import * as DateTime from "effect/DateTime"; | 33 | import * as DateTime from "effect/DateTime"; |
| 34 | import * as Deferred from "effect/Deferred"; | 34 | import * as Deferred from "effect/Deferred"; |
| 35 | import * as Crypto from "effect/Crypto"; | ||
| 35 | import * as Effect from "effect/Effect"; | 36 | import * as Effect from "effect/Effect"; |
| 36 | import * as Exit from "effect/Exit"; | 37 | import * as Exit from "effect/Exit"; |
| 37 | import * as FileSystem from "effect/FileSystem"; | 38 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -957,6 +958,7 @@ describe("ClaudeAdapterV2 Auto-accept edits", () => { | ... | @@ -957,6 +958,7 @@ describe("ClaudeAdapterV2 Auto-accept edits", () => { |
| 957 | attachmentsDir, | 958 | attachmentsDir, |
| 958 | fileSystem, | 959 | fileSystem, |
| 959 | path: yield* Path.Path, | 960 | path: yield* Path.Path, |
| 961 | crypto: yield* Crypto.Crypto, | ||
| 960 | idAllocator, | 962 | idAllocator, |
| 961 | queryRunner: { | 963 | queryRunner: { |
| 962 | allocateSessionId: Effect.succeed("native-thread-claude-accept-edits"), | 964 | allocateSessionId: Effect.succeed("native-thread-claude-accept-edits"), |
| ... | @@ -1199,6 +1201,7 @@ describe("ClaudeAdapterV2 resume compaction", () => { | ... | @@ -1199,6 +1201,7 @@ describe("ClaudeAdapterV2 resume compaction", () => { |
| 1199 | attachmentsDir, | 1201 | attachmentsDir, |
| 1200 | fileSystem, | 1202 | fileSystem, |
| 1201 | path: yield* Path.Path, | 1203 | path: yield* Path.Path, |
| 1204 | crypto: yield* Crypto.Crypto, | ||
| 1202 | idAllocator, | 1205 | idAllocator, |
| 1203 | queryRunner: { | 1206 | queryRunner: { |
| 1204 | allocateSessionId: Effect.succeed("native-thread-claude-resume"), | 1207 | allocateSessionId: Effect.succeed("native-thread-claude-resume"), |
| ... | @@ -1418,6 +1421,7 @@ describe("ClaudeAdapterV2 attachments", () => { | ... | @@ -1418,6 +1421,7 @@ describe("ClaudeAdapterV2 attachments", () => { |
| 1418 | attachmentsDir, | 1421 | attachmentsDir, |
| 1419 | fileSystem, | 1422 | fileSystem, |
| 1420 | path: yield* Path.Path, | 1423 | path: yield* Path.Path, |
| 1424 | crypto: yield* Crypto.Crypto, | ||
| 1421 | idAllocator, | 1425 | idAllocator, |
| 1422 | queryRunner: { | 1426 | queryRunner: { |
| 1423 | allocateSessionId: Effect.succeed("native-thread-claude-attachments"), | 1427 | allocateSessionId: Effect.succeed("native-thread-claude-attachments"), |
| ... | @@ -1558,6 +1562,7 @@ describe("ClaudeAdapterV2 attachments", () => { | ... | @@ -1558,6 +1562,7 @@ describe("ClaudeAdapterV2 attachments", () => { |
| 1558 | attachmentsDir, | 1562 | attachmentsDir, |
| 1559 | fileSystem, | 1563 | fileSystem, |
| 1560 | path: yield* Path.Path, | 1564 | path: yield* Path.Path, |
| 1565 | crypto: yield* Crypto.Crypto, | ||
| 1561 | idAllocator, | 1566 | idAllocator, |
| 1562 | queryRunner: { | 1567 | queryRunner: { |
| 1563 | allocateSessionId: Effect.succeed("native-thread-claude-unsupported-attachment"), | 1568 | allocateSessionId: Effect.succeed("native-thread-claude-unsupported-attachment"), |
| ... | @@ -1647,6 +1652,7 @@ describe("ClaudeAdapterV2 native fork", () => { | ... | @@ -1647,6 +1652,7 @@ describe("ClaudeAdapterV2 native fork", () => { |
| 1647 | attachmentsDir, | 1652 | attachmentsDir, |
| 1648 | fileSystem, | 1653 | fileSystem, |
| 1649 | path: yield* Path.Path, | 1654 | path: yield* Path.Path, |
| 1655 | crypto: yield* Crypto.Crypto, | ||
| 1650 | idAllocator, | 1656 | idAllocator, |
| 1651 | queryRunner: { | 1657 | queryRunner: { |
| 1652 | allocateSessionId: Effect.succeed("source-native-session"), | 1658 | allocateSessionId: Effect.succeed("source-native-session"), |
| ... | @@ -1819,6 +1825,7 @@ describe("ClaudeAdapterV2 native session identity", () => { | ... | @@ -1819,6 +1825,7 @@ describe("ClaudeAdapterV2 native session identity", () => { |
| 1819 | attachmentsDir, | 1825 | attachmentsDir, |
| 1820 | fileSystem, | 1826 | fileSystem, |
| 1821 | path: yield* Path.Path, | 1827 | path: yield* Path.Path, |
| 1828 | crypto: yield* Crypto.Crypto, | ||
| 1822 | idAllocator, | 1829 | idAllocator, |
| 1823 | queryRunner: { | 1830 | queryRunner: { |
| 1824 | allocateSessionId: Effect.succeed("native-session-identity"), | 1831 | allocateSessionId: Effect.succeed("native-session-identity"), |
| ... | @@ -2108,6 +2115,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -2108,6 +2115,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2108 | attachmentsDir, | 2115 | attachmentsDir, |
| 2109 | fileSystem, | 2116 | fileSystem, |
| 2110 | path: yield* Path.Path, | 2117 | path: yield* Path.Path, |
| 2118 | crypto: yield* Crypto.Crypto, | ||
| 2111 | idAllocator, | 2119 | idAllocator, |
| 2112 | continuationRequests: { | 2120 | continuationRequests: { |
| 2113 | offer: (request) => | 2121 | offer: (request) => |
| ... | @@ -3892,6 +3900,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -3892,6 +3900,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 3892 | attachmentsDir, | 3900 | attachmentsDir, |
| 3893 | fileSystem, | 3901 | fileSystem, |
| 3894 | path: yield* Path.Path, | 3902 | path: yield* Path.Path, |
| 3903 | crypto: yield* Crypto.Crypto, | ||
| 3895 | idAllocator, | 3904 | idAllocator, |
| 3896 | continuationRequests: { | 3905 | continuationRequests: { |
| 3897 | offer: (request) => | 3906 | offer: (request) => |
| ... | @@ -4137,6 +4146,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4137,6 +4146,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4137 | attachmentsDir, | 4146 | attachmentsDir, |
| 4138 | fileSystem, | 4147 | fileSystem, |
| 4139 | path: yield* Path.Path, | 4148 | path: yield* Path.Path, |
| 4149 | crypto: yield* Crypto.Crypto, | ||
| 4140 | idAllocator, | 4150 | idAllocator, |
| 4141 | continuationRequests: { | 4151 | continuationRequests: { |
| 4142 | offer: () => Effect.void, | 4152 | offer: () => Effect.void, |
| ... | @@ -4389,11 +4399,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4389,11 +4399,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4389 | const continuationAttempt = RunAttemptId.make("attempt-claude-wake-callback-3"); | 4399 | const continuationAttempt = RunAttemptId.make("attempt-claude-wake-callback-3"); |
| 4390 | const planToolUseId = "toolu_01WakePlanExitPlanMode"; | 4400 | const planToolUseId = "toolu_01WakePlanExitPlanMode"; |
| 4391 | const planMarkdown = "# Wake plan\n\n1. Report the background result."; | 4401 | const planMarkdown = "# Wake plan\n\n1. Report the background result."; |
| 4392 | const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => | 4402 | const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { |
| 4393 | claudeSdkFrame({ | 4403 | return claudeSdkFrame({ |
| 4394 | ...frame, | 4404 | ...frame, |
| 4395 | user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), | 4405 | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4396 | }); | 4406 | }); |
| 4407 | }); | ||
| 4397 | const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`); | 4408 | const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`); |
| 4398 | const planToolUse = claudeSdkFrame({ | 4409 | const planToolUse = claudeSdkFrame({ |
| 4399 | type: "assistant", | 4410 | type: "assistant", |
| ... | @@ -4446,10 +4457,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4446,10 +4457,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4446 | runtimePolicy, | 4457 | runtimePolicy, |
| 4447 | }), | 4458 | }), |
| 4448 | ); | 4459 | ); |
| 4449 | yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, firstAttempt)); | 4460 | yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt)); |
| 4450 | yield* Queue.offer( | 4461 | yield* Queue.offer( |
| 4451 | harness.sdkMessages, | 4462 | harness.sdkMessages, |
| 4452 | stamp( | 4463 | yield* stamp( |
| 4453 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000740", result: "STARTED" }), | 4464 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000740", result: "STARTED" }), |
| 4454 | firstAttempt, | 4465 | firstAttempt, |
| 4455 | ), | 4466 | ), |
| ... | @@ -4508,7 +4519,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4508,7 +4519,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4508 | // The prompt's own turn follows and echoes its uuid. | 4519 | // The prompt's own turn follows and echoes its uuid. |
| 4509 | yield* Queue.offer( | 4520 | yield* Queue.offer( |
| 4510 | harness.sdkMessages, | 4521 | harness.sdkMessages, |
| 4511 | stamp( | 4522 | yield* stamp( |
| 4512 | makeAssistantTextFrame({ | 4523 | makeAssistantTextFrame({ |
| 4513 | uuid: "00000000-0000-4000-8000-000000000743", | 4524 | uuid: "00000000-0000-4000-8000-000000000743", |
| 4514 | text: "USER_REPLY", | 4525 | text: "USER_REPLY", |
| ... | @@ -4518,7 +4529,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4518,7 +4529,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4518 | ); | 4529 | ); |
| 4519 | yield* Queue.offer( | 4530 | yield* Queue.offer( |
| 4520 | harness.sdkMessages, | 4531 | harness.sdkMessages, |
| 4521 | stamp( | 4532 | yield* stamp( |
| 4522 | makeResultFrame({ | 4533 | makeResultFrame({ |
| 4523 | uuid: "00000000-0000-4000-8000-000000000744", | 4534 | uuid: "00000000-0000-4000-8000-000000000744", |
| 4524 | result: "USER_REPLY", | 4535 | result: "USER_REPLY", |
| ... | @@ -4688,11 +4699,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4688,11 +4699,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4688 | const firstAttempt = RunAttemptId.make("attempt-claude-wake-approval-1"); | 4699 | const firstAttempt = RunAttemptId.make("attempt-claude-wake-approval-1"); |
| 4689 | const userAttempt = RunAttemptId.make("attempt-claude-wake-approval-2"); | 4700 | const userAttempt = RunAttemptId.make("attempt-claude-wake-approval-2"); |
| 4690 | const bashToolUseId = "toolu_01WakeApprovalBash"; | 4701 | const bashToolUseId = "toolu_01WakeApprovalBash"; |
| 4691 | const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => | 4702 | const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { |
| 4692 | claudeSdkFrame({ | 4703 | return claudeSdkFrame({ |
| 4693 | ...frame, | 4704 | ...frame, |
| 4694 | user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), | 4705 | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4695 | }); | 4706 | }); |
| 4707 | }); | ||
| 4696 | 4708 | ||
| 4697 | yield* harness.runtime.startTurn( | 4709 | yield* harness.runtime.startTurn( |
| 4698 | makeClaudeTestTurnInput({ | 4710 | makeClaudeTestTurnInput({ |
| ... | @@ -4707,14 +4719,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4707,14 +4719,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4707 | ); | 4719 | ); |
| 4708 | yield* Queue.offer( | 4720 | yield* Queue.offer( |
| 4709 | harness.sdkMessages, | 4721 | harness.sdkMessages, |
| 4710 | stamp( | 4722 | yield* stamp( |
| 4711 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000780", text: "One." }), | 4723 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000780", text: "One." }), |
| 4712 | firstAttempt, | 4724 | firstAttempt, |
| 4713 | ), | 4725 | ), |
| 4714 | ); | 4726 | ); |
| 4715 | yield* Queue.offer( | 4727 | yield* Queue.offer( |
| 4716 | harness.sdkMessages, | 4728 | harness.sdkMessages, |
| 4717 | stamp( | 4729 | yield* stamp( |
| 4718 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000781", result: "One." }), | 4730 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000781", result: "One." }), |
| 4719 | firstAttempt, | 4731 | firstAttempt, |
| 4720 | ), | 4732 | ), |
| ... | @@ -4811,11 +4823,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4811,11 +4823,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4811 | const now = yield* DateTime.now; | 4823 | const now = yield* DateTime.now; |
| 4812 | const firstAttempt = RunAttemptId.make("attempt-claude-echo-debris-1"); | 4824 | const firstAttempt = RunAttemptId.make("attempt-claude-echo-debris-1"); |
| 4813 | const secondAttempt = RunAttemptId.make("attempt-claude-echo-debris-2"); | 4825 | const secondAttempt = RunAttemptId.make("attempt-claude-echo-debris-2"); |
| 4814 | const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => | 4826 | const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { |
| 4815 | claudeSdkFrame({ | 4827 | return claudeSdkFrame({ |
| 4816 | ...frame, | 4828 | ...frame, |
| 4817 | user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), | 4829 | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4818 | }); | 4830 | }); |
| 4831 | }); | ||
| 4819 | 4832 | ||
| 4820 | yield* harness.runtime.startTurn( | 4833 | yield* harness.runtime.startTurn( |
| 4821 | makeClaudeTestTurnInput({ | 4834 | makeClaudeTestTurnInput({ |
| ... | @@ -4830,14 +4843,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4830,14 +4843,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4830 | // The first turn echoes on its first frame: this process echoes early. | 4843 | // The first turn echoes on its first frame: this process echoes early. |
| 4831 | yield* Queue.offer( | 4844 | yield* Queue.offer( |
| 4832 | harness.sdkMessages, | 4845 | harness.sdkMessages, |
| 4833 | stamp( | 4846 | yield* stamp( |
| 4834 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000760", text: "One." }), | 4847 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000760", text: "One." }), |
| 4835 | firstAttempt, | 4848 | firstAttempt, |
| 4836 | ), | 4849 | ), |
| 4837 | ); | 4850 | ); |
| 4838 | yield* Queue.offer( | 4851 | yield* Queue.offer( |
| 4839 | harness.sdkMessages, | 4852 | harness.sdkMessages, |
| 4840 | stamp( | 4853 | yield* stamp( |
| 4841 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000761", result: "One." }), | 4854 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000761", result: "One." }), |
| 4842 | firstAttempt, | 4855 | firstAttempt, |
| 4843 | ), | 4856 | ), |
| ... | @@ -4864,14 +4877,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4864,14 +4877,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4864 | yield* Queue.offer(harness.sdkMessages, staleTaskNotificationResult); | 4877 | yield* Queue.offer(harness.sdkMessages, staleTaskNotificationResult); |
| 4865 | yield* Queue.offer( | 4878 | yield* Queue.offer( |
| 4866 | harness.sdkMessages, | 4879 | harness.sdkMessages, |
| 4867 | stamp( | 4880 | yield* stamp( |
| 4868 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000762", text: "Two." }), | 4881 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000762", text: "Two." }), |
| 4869 | secondAttempt, | 4882 | secondAttempt, |
| 4870 | ), | 4883 | ), |
| 4871 | ); | 4884 | ); |
| 4872 | yield* Queue.offer( | 4885 | yield* Queue.offer( |
| 4873 | harness.sdkMessages, | 4886 | harness.sdkMessages, |
| 4874 | stamp( | 4887 | yield* stamp( |
| 4875 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000763", result: "Two." }), | 4888 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000763", result: "Two." }), |
| 4876 | secondAttempt, | 4889 | secondAttempt, |
| 4877 | ), | 4890 | ), |
| ... | @@ -4909,11 +4922,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4909,11 +4922,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4909 | const now = yield* DateTime.now; | 4922 | const now = yield* DateTime.now; |
| 4910 | const firstAttempt = RunAttemptId.make("attempt-claude-echo-1"); | 4923 | const firstAttempt = RunAttemptId.make("attempt-claude-echo-1"); |
| 4911 | const secondAttempt = RunAttemptId.make("attempt-claude-echo-2"); | 4924 | const secondAttempt = RunAttemptId.make("attempt-claude-echo-2"); |
| 4912 | const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => | 4925 | const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { |
| 4913 | claudeSdkFrame({ | 4926 | return claudeSdkFrame({ |
| 4914 | ...frame, | 4927 | ...frame, |
| 4915 | user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), | 4928 | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4916 | }); | 4929 | }); |
| 4930 | }); | ||
| 4917 | const assistantTexts = () => | 4931 | const assistantTexts = () => |
| 4918 | harness.events.flatMap((event) => | 4932 | harness.events.flatMap((event) => |
| 4919 | event.type === "message.updated" && event.message.role === "assistant" | 4933 | event.type === "message.updated" && event.message.role === "assistant" |
| ... | @@ -4935,18 +4949,18 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -4935,18 +4949,18 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4935 | ); | 4949 | ); |
| 4936 | assert.equal( | 4950 | assert.equal( |
| 4937 | harness.offeredMessages[0]?.uuid, | 4951 | harness.offeredMessages[0]?.uuid, |
| 4938 | ClaudeAdapterV2.claudePromptUuid(firstAttempt), | 4952 | yield* ClaudeAdapterV2.claudePromptUuid(firstAttempt), |
| 4939 | ); | 4953 | ); |
| 4940 | yield* Queue.offer( | 4954 | yield* Queue.offer( |
| 4941 | harness.sdkMessages, | 4955 | harness.sdkMessages, |
| 4942 | stamp( | 4956 | yield* stamp( |
| 4943 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000701", text: "One." }), | 4957 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000701", text: "One." }), |
| 4944 | firstAttempt, | 4958 | firstAttempt, |
| 4945 | ), | 4959 | ), |
| 4946 | ); | 4960 | ); |
| 4947 | yield* Queue.offer( | 4961 | yield* Queue.offer( |
| 4948 | harness.sdkMessages, | 4962 | harness.sdkMessages, |
| 4949 | stamp( | 4963 | yield* stamp( |
| 4950 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000702", result: "One." }), | 4964 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000702", result: "One." }), |
| 4951 | firstAttempt, | 4965 | firstAttempt, |
| 4952 | ), | 4966 | ), |
| ... | @@ -5995,11 +6009,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -5995,11 +6009,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 5995 | const userAttempt = RunAttemptId.make("attempt-claude-wake-subagent-2"); | 6009 | const userAttempt = RunAttemptId.make("attempt-claude-wake-subagent-2"); |
| 5996 | const continuationAttempt = RunAttemptId.make("attempt-claude-wake-subagent-3"); | 6010 | const continuationAttempt = RunAttemptId.make("attempt-claude-wake-subagent-3"); |
| 5997 | const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`); | 6011 | const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`); |
| 5998 | const stamp = (frame: SDKMessage, attemptId: RunAttemptId) => | 6012 | const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) { |
| 5999 | claudeSdkFrame({ | 6013 | return claudeSdkFrame({ |
| 6000 | ...frame, | 6014 | ...frame, |
| 6001 | user_message_uuid: ClaudeAdapterV2.claudePromptUuid(attemptId), | 6015 | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 6002 | }); | 6016 | }); |
| 6017 | }); | ||
| 6003 | 6018 | ||
| 6004 | yield* harness.runtime.startTurn( | 6019 | yield* harness.runtime.startTurn( |
| 6005 | makeClaudeTestTurnInput({ | 6020 | makeClaudeTestTurnInput({ |
| ... | @@ -6011,10 +6026,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -6011,10 +6026,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 6011 | attachments: [], | 6026 | attachments: [], |
| 6012 | }), | 6027 | }), |
| 6013 | ); | 6028 | ); |
| 6014 | yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, firstAttempt)); | 6029 | yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt)); |
| 6015 | yield* Queue.offer( | 6030 | yield* Queue.offer( |
| 6016 | harness.sdkMessages, | 6031 | harness.sdkMessages, |
| 6017 | stamp( | 6032 | yield* stamp( |
| 6018 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000770", result: "STARTED" }), | 6033 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000770", result: "STARTED" }), |
| 6019 | firstAttempt, | 6034 | firstAttempt, |
| 6020 | ), | 6035 | ), |
| ... | @@ -6083,14 +6098,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -6083,14 +6098,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 6083 | session_id: WAKE_NATIVE_SESSION, | 6098 | session_id: WAKE_NATIVE_SESSION, |
| 6084 | }), | 6099 | }), |
| 6085 | wakeResult, | 6100 | wakeResult, |
| 6086 | stamp( | 6101 | yield* stamp( |
| 6087 | makeAssistantTextFrame({ | 6102 | makeAssistantTextFrame({ |
| 6088 | uuid: "00000000-0000-4000-8000-000000000776", | 6103 | uuid: "00000000-0000-4000-8000-000000000776", |
| 6089 | text: "USER_REPLY", | 6104 | text: "USER_REPLY", |
| 6090 | }), | 6105 | }), |
| 6091 | userAttempt, | 6106 | userAttempt, |
| 6092 | ), | 6107 | ), |
| 6093 | stamp( | 6108 | yield* stamp( |
| 6094 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000777", result: "USER_REPLY" }), | 6109 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000777", result: "USER_REPLY" }), |
| 6095 | userAttempt, | 6110 | userAttempt, |
| 6096 | ), | 6111 | ), |
| ... | @@ -7018,6 +7033,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -7018,6 +7033,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 7018 | attachmentsDir, | 7033 | attachmentsDir, |
| 7019 | fileSystem, | 7034 | fileSystem, |
| 7020 | path: yield* Path.Path, | 7035 | path: yield* Path.Path, |
| 7036 | crypto: yield* Crypto.Crypto, | ||
| 7021 | idAllocator, | 7037 | idAllocator, |
| 7022 | continuationRequests: { | 7038 | continuationRequests: { |
| 7023 | offer: (request) => | 7039 | offer: (request) => |
| ... | @@ -7201,6 +7217,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -7201,6 +7217,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 7201 | attachmentsDir, | 7217 | attachmentsDir, |
| 7202 | fileSystem, | 7218 | fileSystem, |
| 7203 | path: yield* Path.Path, | 7219 | path: yield* Path.Path, |
| 7220 | crypto: yield* Crypto.Crypto, | ||
| 7204 | idAllocator, | 7221 | idAllocator, |
| 7205 | continuationRequests: { | 7222 | continuationRequests: { |
| 7206 | offer: (request) => | 7223 | offer: (request) => |
| ... | @@ -7436,6 +7453,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -7436,6 +7453,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 7436 | attachmentsDir, | 7453 | attachmentsDir, |
| 7437 | fileSystem, | 7454 | fileSystem, |
| 7438 | path: yield* Path.Path, | 7455 | path: yield* Path.Path, |
| 7456 | crypto: yield* Crypto.Crypto, | ||
| 7439 | idAllocator, | 7457 | idAllocator, |
| 7440 | continuationRequests: { | 7458 | continuationRequests: { |
| 7441 | offer: (request) => | 7459 | offer: (request) => |
| ... | @@ -7628,6 +7646,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -7628,6 +7646,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 7628 | attachmentsDir, | 7646 | attachmentsDir, |
| 7629 | fileSystem, | 7647 | fileSystem, |
| 7630 | path: yield* Path.Path, | 7648 | path: yield* Path.Path, |
| 7649 | crypto: yield* Crypto.Crypto, | ||
| 7631 | idAllocator, | 7650 | idAllocator, |
| 7632 | continuationRequests: { offer: () => Effect.void }, | 7651 | continuationRequests: { offer: () => Effect.void }, |
| 7633 | queryRunner: { | 7652 | queryRunner: { |
| ... | @@ -7794,6 +7813,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -7794,6 +7813,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 7794 | attachmentsDir, | 7813 | attachmentsDir, |
| 7795 | fileSystem, | 7814 | fileSystem, |
| 7796 | path: yield* Path.Path, | 7815 | path: yield* Path.Path, |
| 7816 | crypto: yield* Crypto.Crypto, | ||
| 7797 | idAllocator, | 7817 | idAllocator, |
| 7798 | continuationRequests: { | 7818 | continuationRequests: { |
| 7799 | offer: () => Effect.void, | 7819 | offer: () => Effect.void, |
| ... | @@ -7923,6 +7943,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -7923,6 +7943,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 7923 | attachmentsDir, | 7943 | attachmentsDir, |
| 7924 | fileSystem, | 7944 | fileSystem, |
| 7925 | path: yield* Path.Path, | 7945 | path: yield* Path.Path, |
| 7946 | crypto: yield* Crypto.Crypto, | ||
| 7926 | idAllocator, | 7947 | idAllocator, |
| 7927 | continuationRequests: { | 7948 | continuationRequests: { |
| 7928 | offer: (request) => | 7949 | offer: (request) => |
| ... | @@ -8117,6 +8138,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -8117,6 +8138,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 8117 | attachmentsDir, | 8138 | attachmentsDir, |
| 8118 | fileSystem, | 8139 | fileSystem, |
| 8119 | path: yield* Path.Path, | 8140 | path: yield* Path.Path, |
| 8141 | crypto: yield* Crypto.Crypto, | ||
| 8120 | idAllocator, | 8142 | idAllocator, |
| 8121 | continuationRequests: { | 8143 | continuationRequests: { |
| 8122 | offer: () => Effect.void, | 8144 | offer: () => Effect.void, |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts+5-1| ... | @@ -1580,7 +1580,11 @@ async function recordClaudeStreamingQuery(input: { | ... | @@ -1580,7 +1580,11 @@ async function recordClaudeStreamingQuery(input: { |
| 1580 | // Like the adapter, give each prompt a uuid Claude echoes on its turn. | 1580 | // Like the adapter, give each prompt a uuid Claude echoes on its turn. |
| 1581 | const message = ClaudeAdapterV2.makeClaudeUserMessage({ | 1581 | const message = ClaudeAdapterV2.makeClaudeUserMessage({ |
| 1582 | text: prompt, | 1582 | text: prompt, |
| 1583 | uuid: ClaudeAdapterV2.claudePromptUuid(`${input.sessionId}:prompt:${index + 1}`), | 1583 | uuid: await Effect.runPromise( |
| 1584 | ClaudeAdapterV2.claudePromptUuid(`${input.sessionId}:prompt:${index + 1}`).pipe( | ||
| 1585 | Effect.provide(NodeServices.layer), | ||
| 1586 | ), | ||
| 1587 | ), | ||
| 1584 | }); | 1588 | }); |
| 1585 | input.entries.push({ | 1589 | input.entries.push({ |
| 1586 | type: "expect_outbound", | 1590 | type: "expect_outbound", |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+39-24| ... | @@ -1,5 +1,3 @@ | ... | @@ -1,5 +1,3 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | |||
| 3 | import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; | 1 | import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; |
| 4 | import { | 2 | import { |
| 5 | dynamicToolTitle, | 3 | dynamicToolTitle, |
| ... | @@ -73,6 +71,7 @@ import { | ... | @@ -73,6 +71,7 @@ import { |
| 73 | import * as Cause from "effect/Cause"; | 71 | import * as Cause from "effect/Cause"; |
| 74 | import * as Context from "effect/Context"; | 72 | import * as Context from "effect/Context"; |
| 75 | import * as Crypto from "effect/Crypto"; | 73 | import * as Crypto from "effect/Crypto"; |
| 74 | import * as Hex from "effect/encoding/Hex"; | ||
| 76 | import * as DateTime from "effect/DateTime"; | 75 | import * as DateTime from "effect/DateTime"; |
| 77 | import * as Deferred from "effect/Deferred"; | 76 | import * as Deferred from "effect/Deferred"; |
| 78 | import * as Effect from "effect/Effect"; | 77 | import * as Effect from "effect/Effect"; |
| ... | @@ -1420,11 +1419,17 @@ const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input: | ... | @@ -1420,11 +1419,17 @@ const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input: |
| 1420 | 1419 | ||
| 1421 | // Stable per run attempt, so a replayed prompt offer matches its recording. | 1420 | // Stable per run attempt, so a replayed prompt offer matches its recording. |
| 1422 | // Claude echoes it back as user_message_uuid on the turn that answers it. | 1421 | // Claude echoes it back as user_message_uuid on the turn that answers it. |
| 1423 | export function claudePromptUuid(attemptId: string): NonNullable<SDKUserMessage["uuid"]> { | 1422 | export const claudePromptUuid = Effect.fn("claudePromptUuid")(function* (attemptId: string) { |
| 1424 | const hex = NodeCrypto.createHash("sha256").update(`t3-claude-prompt:${attemptId}`).digest("hex"); | 1423 | const crypto = yield* Crypto.Crypto; |
| 1424 | const digest = yield* crypto | ||
| 1425 | .digest("SHA-256", new TextEncoder().encode(`t3-claude-prompt:${attemptId}`)) | ||
| 1426 | .pipe(Effect.orDie); | ||
| 1427 | const hex = Hex.encode(digest); | ||
| 1425 | const variant = ((Number.parseInt(hex[16]!, 16) & 0x3) | 0x8).toString(16); | 1428 | const variant = ((Number.parseInt(hex[16]!, 16) & 0x3) | 0x8).toString(16); |
| 1426 | return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`; | 1429 | const uuid: NonNullable<SDKUserMessage["uuid"]> = |
| 1427 | } | 1430 | `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`; |
| 1431 | return uuid; | ||
| 1432 | }); | ||
| 1428 | 1433 | ||
| 1429 | type ClaudeAssistantContentBlock = SDKAssistantMessage["message"]["content"][number]; | 1434 | type ClaudeAssistantContentBlock = SDKAssistantMessage["message"]["content"][number]; |
| 1430 | type ClaudeToolUseContentBlock = Extract< | 1435 | type ClaudeToolUseContentBlock = Extract< |
| ... | @@ -2998,6 +3003,7 @@ export interface ClaudeAdapterV2Options { | ... | @@ -2998,6 +3003,7 @@ export interface ClaudeAdapterV2Options { |
| 2998 | readonly attachmentsDir: string; | 3003 | readonly attachmentsDir: string; |
| 2999 | readonly fileSystem: FileSystem.FileSystem; | 3004 | readonly fileSystem: FileSystem.FileSystem; |
| 3000 | readonly path: Path.Path; | 3005 | readonly path: Path.Path; |
| 3006 | readonly crypto: Crypto.Crypto; | ||
| 3001 | readonly idAllocator: IdAllocator.IdAllocatorV2Shape; | 3007 | readonly idAllocator: IdAllocator.IdAllocatorV2Shape; |
| 3002 | readonly queryRunner: ClaudeAgentSdkQueryRunnerShape; | 3008 | readonly queryRunner: ClaudeAgentSdkQueryRunnerShape; |
| 3003 | readonly scopedLimitNames?: Ref.Ref<ClaudeScopedLimitNames>; | 3009 | readonly scopedLimitNames?: Ref.Ref<ClaudeScopedLimitNames>; |
| ... | @@ -3013,7 +3019,7 @@ export interface ClaudeAdapterV2Options { | ... | @@ -3013,7 +3019,7 @@ export interface ClaudeAdapterV2Options { |
| 3013 | export function makeClaudeAdapterV2( | 3019 | export function makeClaudeAdapterV2( |
| 3014 | adapterOptions: ClaudeAdapterV2Options, | 3020 | adapterOptions: ClaudeAdapterV2Options, |
| 3015 | ): ProviderAdapter.ProviderAdapterV2Shape { | 3021 | ): ProviderAdapter.ProviderAdapterV2Shape { |
| 3016 | const { attachmentsDir, fileSystem, path, idAllocator, queryRunner } = adapterOptions; | 3022 | const { attachmentsDir, fileSystem, path, crypto, idAllocator, queryRunner } = adapterOptions; |
| 3017 | const continuationRequests = adapterOptions.continuationRequests ?? { | 3023 | const continuationRequests = adapterOptions.continuationRequests ?? { |
| 3018 | offer: () => Effect.void, | 3024 | offer: () => Effect.void, |
| 3019 | }; | 3025 | }; |
| ... | @@ -7242,6 +7248,11 @@ export function makeClaudeAdapterV2( | ... | @@ -7242,6 +7248,11 @@ export function makeClaudeAdapterV2( |
| 7242 | const startedAt = yield* DateTime.now; | 7248 | const startedAt = yield* DateTime.now; |
| 7243 | const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread); | 7249 | const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread); |
| 7244 | const nativeTurnId = `turn:${turnInput.attemptId}`; | 7250 | const nativeTurnId = `turn:${turnInput.attemptId}`; |
| 7251 | const promptUuid = isClaudeProviderContinuationTurn(turnInput) | ||
| 7252 | ? null | ||
| 7253 | : yield* claudePromptUuid(turnInput.attemptId).pipe( | ||
| 7254 | Effect.provideService(Crypto.Crypto, crypto), | ||
| 7255 | ); | ||
| 7245 | const providerTurnId = idAllocator.derive.providerTurn({ | 7256 | const providerTurnId = idAllocator.derive.providerTurn({ |
| 7246 | driver: CLAUDE_PROVIDER, | 7257 | driver: CLAUDE_PROVIDER, |
| 7247 | nativeTurnId, | 7258 | nativeTurnId, |
| ... | @@ -7298,9 +7309,7 @@ export function makeClaudeAdapterV2( | ... | @@ -7298,9 +7309,7 @@ export function makeClaudeAdapterV2( |
| 7298 | subagentsByToolUseId: new Map(), | 7309 | subagentsByToolUseId: new Map(), |
| 7299 | subagentNodesByTaskId: new Map(), | 7310 | subagentNodesByTaskId: new Map(), |
| 7300 | pendingSubagentLaunchesByToolUseId: new Map(), | 7311 | pendingSubagentLaunchesByToolUseId: new Map(), |
| 7301 | promptUuid: isClaudeProviderContinuationTurn(turnInput) | 7312 | promptUuid, |
| 7302 | ? null | ||
| 7303 | : claudePromptUuid(turnInput.attemptId), | ||
| 7304 | promptEcho: isClaudeProviderContinuationTurn(turnInput) ? "confirmed" : "pending", | 7313 | promptEcho: isClaudeProviderContinuationTurn(turnInput) ? "confirmed" : "pending", |
| 7305 | gatedFramesBeforeEcho: 0, | 7314 | gatedFramesBeforeEcho: 0, |
| 7306 | heldRootFrames: [], | 7315 | heldRootFrames: [], |
| ... | @@ -7309,20 +7318,20 @@ export function makeClaudeAdapterV2( | ... | @@ -7309,20 +7318,20 @@ export function makeClaudeAdapterV2( |
| 7309 | // produced instead of prompting it again: drain the buffered wake | 7318 | // produced instead of prompting it again: drain the buffered wake |
| 7310 | // messages into this turn and let any still-streaming messages | 7319 | // messages into this turn and let any still-streaming messages |
| 7311 | // follow live. The continuation prompt text never reaches the CLI. | 7320 | // follow live. The continuation prompt text never reaches the CLI. |
| 7312 | const isContinuationTurn = context.promptUuid === null; | 7321 | const userMessage = |
| 7313 | const userMessage = isContinuationTurn | 7322 | promptUuid === null |
| 7314 | ? null | 7323 | ? null |
| 7315 | : yield* makeClaudeUserMessageWithAttachments({ | 7324 | : yield* makeClaudeUserMessageWithAttachments({ |
| 7316 | text: applyClaudePromptEffortPrefix( | 7325 | text: applyClaudePromptEffortPrefix( |
| 7317 | turnInput.message.text, | 7326 | turnInput.message.text, |
| 7318 | compileClaudeModelSelection(turnInput.modelSelection).promptEffort, | 7327 | compileClaudeModelSelection(turnInput.modelSelection).promptEffort, |
| 7319 | ), | 7328 | ), |
| 7320 | attachments: turnInput.message.attachments, | 7329 | attachments: turnInput.message.attachments, |
| 7321 | attachmentsDir, | 7330 | attachmentsDir, |
| 7322 | fileSystem, | 7331 | fileSystem, |
| 7323 | skillNames: yield* userInvocableSkillNames(turnInput.runtimePolicy.cwd), | 7332 | skillNames: yield* userInvocableSkillNames(turnInput.runtimePolicy.cwd), |
| 7324 | uuid: claudePromptUuid(turnInput.attemptId), | 7333 | uuid: promptUuid, |
| 7325 | }); | 7334 | }); |
| 7326 | const querySession = yield* openQuery(turnInput, nativeThreadId); | 7335 | const querySession = yield* openQuery(turnInput, nativeThreadId); |
| 7327 | yield* Ref.set(activeTurn, context); | 7336 | yield* Ref.set(activeTurn, context); |
| 7328 | yield* emitProviderEvent({ | 7337 | yield* emitProviderEvent({ |
| ... | @@ -7906,6 +7915,7 @@ export function makeClaudeAdapterV2( | ... | @@ -7906,6 +7915,7 @@ export function makeClaudeAdapterV2( |
| 7906 | 7915 | ||
| 7907 | export type ClaudeAdapterV2DriverEnv = | 7916 | export type ClaudeAdapterV2DriverEnv = |
| 7908 | | ClaudeAgentSdkQueryRunner | 7917 | | ClaudeAgentSdkQueryRunner |
| 7918 | | Crypto.Crypto | ||
| 7909 | | FileSystem.FileSystem | 7919 | | FileSystem.FileSystem |
| 7910 | | IdAllocator.IdAllocatorV2 | 7920 | | IdAllocator.IdAllocatorV2 |
| 7911 | | Path.Path | 7921 | | Path.Path |
| ... | @@ -7926,6 +7936,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( | ... | @@ -7926,6 +7936,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( |
| 7926 | const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment); | 7936 | const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment); |
| 7927 | const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment); | 7937 | const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment); |
| 7928 | const path = yield* Path.Path; | 7938 | const path = yield* Path.Path; |
| 7939 | const crypto = yield* Crypto.Crypto; | ||
| 7929 | const binaryPath = yield* resolveClaudeSdkExecutablePath( | 7940 | const binaryPath = yield* resolveClaudeSdkExecutablePath( |
| 7930 | expandHomePath(config.binaryPath), | 7941 | expandHomePath(config.binaryPath), |
| 7931 | claudeEnvironment, | 7942 | claudeEnvironment, |
| ... | @@ -7937,6 +7948,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( | ... | @@ -7937,6 +7948,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( |
| 7937 | attachmentsDir: serverConfig.attachmentsDir, | 7948 | attachmentsDir: serverConfig.attachmentsDir, |
| 7938 | fileSystem, | 7949 | fileSystem, |
| 7939 | path, | 7950 | path, |
| 7951 | crypto, | ||
| 7940 | idAllocator, | 7952 | idAllocator, |
| 7941 | queryRunner, | 7953 | queryRunner, |
| 7942 | continuationRequests, | 7954 | continuationRequests, |
| ... | @@ -7970,6 +7982,7 @@ export const ClaudeAdapterV2Driver: ProviderAdapterDriver< | ... | @@ -7970,6 +7982,7 @@ export const ClaudeAdapterV2Driver: ProviderAdapterDriver< |
| 7970 | const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* () { | 7982 | const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* () { |
| 7971 | const fileSystem = yield* FileSystem.FileSystem; | 7983 | const fileSystem = yield* FileSystem.FileSystem; |
| 7972 | const path = yield* Path.Path; | 7984 | const path = yield* Path.Path; |
| 7985 | const crypto = yield* Crypto.Crypto; | ||
| 7973 | const hostEnvironment = yield* HostProcessEnvironment; | 7986 | const hostEnvironment = yield* HostProcessEnvironment; |
| 7974 | const idAllocator = yield* IdAllocator.IdAllocatorV2; | 7987 | const idAllocator = yield* IdAllocator.IdAllocatorV2; |
| 7975 | const queryRunner = yield* ClaudeAgentSdkQueryRunner; | 7988 | const queryRunner = yield* ClaudeAgentSdkQueryRunner; |
| ... | @@ -7983,6 +7996,7 @@ const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* | ... | @@ -7983,6 +7996,7 @@ const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* |
| 7983 | attachmentsDir: serverConfig.attachmentsDir, | 7996 | attachmentsDir: serverConfig.attachmentsDir, |
| 7984 | fileSystem, | 7997 | fileSystem, |
| 7985 | path, | 7998 | path, |
| 7999 | crypto, | ||
| 7986 | idAllocator, | 8000 | idAllocator, |
| 7987 | queryRunner, | 8001 | queryRunner, |
| 7988 | continuationRequests, | 8002 | continuationRequests, |
| ... | @@ -7993,6 +8007,7 @@ const layer: Layer.Layer< | ... | @@ -7993,6 +8007,7 @@ const layer: Layer.Layer< |
| 7993 | ProviderAdapter.ProviderAdapterV2, | 8007 | ProviderAdapter.ProviderAdapterV2, |
| 7994 | never, | 8008 | never, |
| 7995 | | ClaudeAgentSdkQueryRunner | 8009 | | ClaudeAgentSdkQueryRunner |
| 8010 | | Crypto.Crypto | ||
| 7996 | | FileSystem.FileSystem | 8011 | | FileSystem.FileSystem |
| 7997 | | IdAllocator.IdAllocatorV2 | 8012 | | IdAllocator.IdAllocatorV2 |
| 7998 | | Path.Path | 8013 | | Path.Path |
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+175-160| ... | @@ -35,6 +35,7 @@ import * as CodexClient from "effect-codex-app-server/client"; | ... | @@ -35,6 +35,7 @@ import * as CodexClient from "effect-codex-app-server/client"; |
| 35 | import * as CodexReplay from "effect-codex-app-server/replay"; | 35 | import * as CodexReplay from "effect-codex-app-server/replay"; |
| 36 | import * as DateTime from "effect/DateTime"; | 36 | import * as DateTime from "effect/DateTime"; |
| 37 | import * as Deferred from "effect/Deferred"; | 37 | import * as Deferred from "effect/Deferred"; |
| 38 | import * as Crypto from "effect/Crypto"; | ||
| 38 | import * as Effect from "effect/Effect"; | 39 | import * as Effect from "effect/Effect"; |
| 39 | import * as Predicate from "effect/Predicate"; | 40 | import * as Predicate from "effect/Predicate"; |
| 40 | import * as FileSystem from "effect/FileSystem"; | 41 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -801,183 +802,195 @@ describe("CodexAdapterV2 process spawning", () => { | ... | @@ -801,183 +802,195 @@ describe("CodexAdapterV2 process spawning", () => { |
| 801 | }); | 802 | }); |
| 802 | 803 | ||
| 803 | describe("CodexAdapterV2 dynamic tool projection", () => { | 804 | describe("CodexAdapterV2 dynamic tool projection", () => { |
| 804 | it("uses the CUA call title while leaving other MCP titles as tool arguments", () => { | 805 | it.effect("uses the CUA call title while leaving other MCP titles as tool arguments", () => |
| 805 | const call = { | 806 | Effect.gen(function* () { |
| 806 | type: "mcpToolCall" as const, | 807 | const call = { |
| 807 | id: "inspect", | 808 | type: "mcpToolCall" as const, |
| 808 | server: "cua_repl", | 809 | id: "inspect", |
| 809 | tool: "js", | 810 | server: "cua_repl", |
| 810 | status: "completed" as const, | 811 | tool: "js", |
| 811 | arguments: { | 812 | status: "completed" as const, |
| 812 | code: "await game.getAXStateAndScreenshot();", | 813 | arguments: { |
| 813 | title: "Inspect Saga music screen", | 814 | code: "await game.getAXStateAndScreenshot();", |
| 814 | }, | 815 | title: "Inspect Saga music screen", |
| 815 | result: { content: [] }, | 816 | }, |
| 816 | }; | 817 | result: { content: [] }, |
| 817 | assert.equal( | 818 | }; |
| 818 | CodexAdapterV2.projectCodexDynamicToolItem(call).title, | 819 | assert.equal( |
| 819 | "Inspect Saga music screen", | 820 | (yield* CodexAdapterV2.projectCodexDynamicToolItem(call)).title, |
| 820 | ); | 821 | "Inspect Saga music screen", |
| 821 | assert.equal( | 822 | ); |
| 822 | CodexAdapterV2.projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title, | 823 | assert.equal( |
| 823 | "js", | 824 | (yield* CodexAdapterV2.projectCodexDynamicToolItem({ ...call, arguments: { title: " " } })) |
| 824 | ); | 825 | .title, |
| 825 | assert.equal( | 826 | "js", |
| 826 | CodexAdapterV2.projectCodexDynamicToolItem({ ...call, server: "github" }).title, | 827 | ); |
| 827 | "js", | 828 | assert.equal( |
| 828 | ); | 829 | (yield* CodexAdapterV2.projectCodexDynamicToolItem({ ...call, server: "github" })).title, |
| 829 | }); | 830 | "js", |
| 831 | ); | ||
| 832 | }).pipe(Effect.provide(NodeServices.layer)), | ||
| 833 | ); | ||
| 830 | 834 | ||
| 831 | it.each(["inProgress", "completed", "failed"] as const)( | 835 | it.effect.each(["inProgress", "completed", "failed"] as const)( |
| 832 | "presents ordinary MCP calls when %s", | 836 | "presents ordinary MCP calls when %s", |
| 833 | (status) => { | 837 | (status) => |
| 834 | const projection = CodexAdapterV2.projectCodexDynamicToolItem({ | 838 | Effect.gen(function* () { |
| 839 | const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ | ||
| 840 | type: "mcpToolCall", | ||
| 841 | id: "weather-call", | ||
| 842 | server: "weather", | ||
| 843 | tool: "get_weather", | ||
| 844 | status, | ||
| 845 | arguments: { city: "Berlin" }, | ||
| 846 | }); | ||
| 847 | assert.equal(projection.title, "get weather"); | ||
| 848 | assert.deepEqual(projection.toolSource, { | ||
| 849 | key: "mcp:weather", | ||
| 850 | name: "weather", | ||
| 851 | kind: "integration", | ||
| 852 | }); | ||
| 853 | assert.deepEqual(projection.input, { city: "Berlin" }); | ||
| 854 | }).pipe(Effect.provide(NodeServices.layer)), | ||
| 855 | ); | ||
| 856 | |||
| 857 | it.effect("uses Codex connector names without reading a display title from arguments", () => | ||
| 858 | Effect.gen(function* () { | ||
| 859 | const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ | ||
| 835 | type: "mcpToolCall", | 860 | type: "mcpToolCall", |
| 836 | id: "weather-call", | 861 | id: "connector-call", |
| 837 | server: "weather", | 862 | server: "_apps", |
| 838 | tool: "get_weather", | 863 | tool: "connector_get_weather", |
| 839 | status, | 864 | status: "completed", |
| 840 | arguments: { city: "Berlin" }, | 865 | arguments: { title: "Argument, not display metadata" }, |
| 866 | appContext: { | ||
| 867 | connectorId: "weather-app", | ||
| 868 | appName: "Weather", | ||
| 869 | actionName: "Check weather", | ||
| 870 | }, | ||
| 871 | result: { | ||
| 872 | content: [], | ||
| 873 | _meta: { source: { logoUrl: "https://example.com/weather.png" } }, | ||
| 874 | }, | ||
| 841 | }); | 875 | }); |
| 842 | assert.equal(projection.title, "get weather"); | 876 | assert.equal(projection.title, "Check weather"); |
| 843 | assert.deepEqual(projection.toolSource, { | 877 | assert.equal(projection.toolSource?.name, "Weather"); |
| 844 | key: "mcp:weather", | 878 | assert.deepEqual(projection.toolIcon, { |
| 845 | name: "weather", | 879 | _tag: "themed-logo", |
| 846 | kind: "integration", | 880 | logoUrl: "https://example.com/weather.png", |
| 847 | }); | 881 | }); |
| 848 | assert.deepEqual(projection.input, { city: "Berlin" }); | 882 | assert.deepEqual(projection.toolSource?.icon, projection.toolIcon); |
| 849 | }, | 883 | }).pipe(Effect.provide(NodeServices.layer)), |
| 850 | ); | 884 | ); |
| 851 | 885 | ||
| 852 | it("uses Codex connector names without reading a display title from arguments", () => { | 886 | it.effect("preserves native browser and app icons alongside MCP tool output", () => |
| 853 | const projection = CodexAdapterV2.projectCodexDynamicToolItem({ | 887 | Effect.gen(function* () { |
| 854 | type: "mcpToolCall", | 888 | const browser = yield* CodexAdapterV2.projectCodexDynamicToolItem({ |
| 855 | id: "connector-call", | 889 | type: "mcpToolCall", |
| 856 | server: "_apps", | 890 | id: "browser", |
| 857 | tool: "connector_get_weather", | 891 | server: "browser", |
| 858 | status: "completed", | 892 | tool: "open", |
| 859 | arguments: { title: "Argument, not display metadata" }, | 893 | status: "completed", |
| 860 | appContext: { | 894 | arguments: {}, |
| 861 | connectorId: "weather-app", | 895 | result: { |
| 862 | appName: "Weather", | 896 | content: [], |
| 863 | actionName: "Check weather", | 897 | _meta: { |
| 864 | }, | 898 | "codex/toolSurface": { |
| 865 | result: { | 899 | kind: "browserUse", |
| 866 | content: [], | 900 | browserFamily: "Chrome", |
| 867 | _meta: { source: { logoUrl: "https://example.com/weather.png" } }, | 901 | screenshot: { |
| 868 | }, | 902 | pageUrl: "https://example.com/docs", |
| 869 | }); | 903 | faviconUrl: "https://example.com/icon.png", |
| 870 | assert.equal(projection.title, "Check weather"); | 904 | }, |
| 871 | assert.equal(projection.toolSource?.name, "Weather"); | ||
| 872 | assert.deepEqual(projection.toolIcon, { | ||
| 873 | _tag: "themed-logo", | ||
| 874 | logoUrl: "https://example.com/weather.png", | ||
| 875 | }); | ||
| 876 | assert.deepEqual(projection.toolSource?.icon, projection.toolIcon); | ||
| 877 | }); | ||
| 878 | |||
| 879 | it("preserves native browser and app icons alongside MCP tool output", () => { | ||
| 880 | const browser = CodexAdapterV2.projectCodexDynamicToolItem({ | ||
| 881 | type: "mcpToolCall", | ||
| 882 | id: "browser", | ||
| 883 | server: "browser", | ||
| 884 | tool: "open", | ||
| 885 | status: "completed", | ||
| 886 | arguments: {}, | ||
| 887 | result: { | ||
| 888 | content: [], | ||
| 889 | _meta: { | ||
| 890 | "codex/toolSurface": { | ||
| 891 | kind: "browserUse", | ||
| 892 | browserFamily: "Chrome", | ||
| 893 | screenshot: { | ||
| 894 | pageUrl: "https://example.com/docs", | ||
| 895 | faviconUrl: "https://example.com/icon.png", | ||
| 896 | }, | 905 | }, |
| 897 | }, | 906 | }, |
| 898 | }, | 907 | }, |
| 899 | }, | 908 | }); |
| 900 | }); | 909 | assert.equal(browser.toolSurface, "browser"); |
| 901 | assert.equal(browser.toolSurface, "browser"); | 910 | assert.deepEqual(browser.toolIcon, { |
| 902 | assert.deepEqual(browser.toolIcon, { | 911 | _tag: "website", |
| 903 | _tag: "website", | 912 | pageUrl: "https://example.com/docs", |
| 904 | pageUrl: "https://example.com/docs", | 913 | faviconUrl: "https://example.com/icon.png", |
| 905 | faviconUrl: "https://example.com/icon.png", | 914 | }); |
| 906 | }); | 915 | assert.equal(browser.toolSource?.name, "Chrome"); |
| 907 | assert.equal(browser.toolSource?.name, "Chrome"); | 916 | const app = yield* CodexAdapterV2.projectCodexDynamicToolItem({ |
| 908 | const app = CodexAdapterV2.projectCodexDynamicToolItem({ | 917 | type: "mcpToolCall", |
| 909 | type: "mcpToolCall", | 918 | id: "app", |
| 910 | id: "app", | 919 | server: "computer", |
| 911 | server: "computer", | 920 | tool: "click", |
| 912 | tool: "click", | 921 | status: "completed", |
| 913 | status: "completed", | 922 | arguments: {}, |
| 914 | arguments: {}, | 923 | result: { |
| 915 | result: { | 924 | content: [], |
| 916 | content: [], | 925 | _meta: { |
| 917 | _meta: { | 926 | "codex/toolSurface": { |
| 918 | "codex/toolSurface": { | 927 | kind: "computerUse", |
| 919 | kind: "computerUse", | 928 | app: { kind: "appId", appId: "com.apple.finder" }, |
| 920 | app: { kind: "appId", appId: "com.apple.finder" }, | 929 | }, |
| 921 | }, | 930 | }, |
| 922 | }, | 931 | }, |
| 923 | }, | 932 | }); |
| 924 | }); | 933 | assert.deepEqual(app.toolIcon, { |
| 925 | assert.deepEqual(app.toolIcon, { | 934 | _tag: "native-app", |
| 926 | _tag: "native-app", | 935 | app: { _tag: "app-id", appId: "com.apple.finder" }, |
| 927 | app: { _tag: "app-id", appId: "com.apple.finder" }, | 936 | }); |
| 928 | }); | 937 | assert.equal(app.toolSource?.name, "Finder"); |
| 929 | assert.equal(app.toolSource?.name, "Finder"); | 938 | }).pipe(Effect.provide(NodeServices.layer)), |
| 930 | }); | 939 | ); |
| 931 | 940 | ||
| 932 | it("preserves MCP arguments and prefers structured output", () => { | 941 | it.effect("preserves MCP arguments and prefers structured output", () => |
| 933 | const projection = CodexAdapterV2.projectCodexDynamicToolItem({ | 942 | Effect.gen(function* () { |
| 934 | type: "mcpToolCall", | 943 | const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ |
| 935 | id: "call-create-threads", | 944 | type: "mcpToolCall", |
| 936 | server: "t3-code", | 945 | id: "call-create-threads", |
| 937 | tool: "create_threads", | 946 | server: "t3-code", |
| 938 | status: "completed", | 947 | tool: "create_threads", |
| 939 | arguments: { | 948 | status: "completed", |
| 940 | threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], | 949 | arguments: { |
| 941 | }, | 950 | threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], |
| 942 | result: { | ||
| 943 | content: [{ type: "text", text: '{"threads":[{"threadId":"thread:mcp:fixture:0"}]}' }], | ||
| 944 | structuredContent: { | ||
| 945 | threads: [{ threadId: "thread:mcp:fixture:0" }], | ||
| 946 | }, | 951 | }, |
| 947 | }, | 952 | result: { |
| 948 | }); | 953 | content: [{ type: "text", text: '{"threads":[{"threadId":"thread:mcp:fixture:0"}]}' }], |
| 954 | structuredContent: { | ||
| 955 | threads: [{ threadId: "thread:mcp:fixture:0" }], | ||
| 956 | }, | ||
| 957 | }, | ||
| 958 | }); | ||
| 949 | 959 | ||
| 950 | assert.deepEqual(projection, { | 960 | assert.deepEqual(projection, { |
| 951 | toolName: "t3-code.create_threads", | 961 | toolName: "t3-code.create_threads", |
| 952 | input: { | 962 | input: { |
| 953 | threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], | 963 | threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], |
| 954 | }, | 964 | }, |
| 955 | output: { | 965 | output: { |
| 956 | threads: [{ threadId: "thread:mcp:fixture:0" }], | 966 | threads: [{ threadId: "thread:mcp:fixture:0" }], |
| 957 | }, | 967 | }, |
| 958 | status: "completed", | 968 | status: "completed", |
| 959 | }); | 969 | }); |
| 960 | }); | 970 | }).pipe(Effect.provide(NodeServices.layer)), |
| 971 | ); | ||
| 961 | 972 | ||
| 962 | it("preserves namespaced dynamic tool output", () => { | 973 | it.effect("preserves namespaced dynamic tool output", () => |
| 963 | const projection = CodexAdapterV2.projectCodexDynamicToolItem({ | 974 | Effect.gen(function* () { |
| 964 | type: "dynamicToolCall", | 975 | const projection = yield* CodexAdapterV2.projectCodexDynamicToolItem({ |
| 965 | id: "call-dynamic", | 976 | type: "dynamicToolCall", |
| 966 | namespace: "workspace", | 977 | id: "call-dynamic", |
| 967 | tool: "inspect", | 978 | namespace: "workspace", |
| 968 | status: "failed", | 979 | tool: "inspect", |
| 969 | arguments: { path: "package.json" }, | 980 | status: "failed", |
| 970 | contentItems: [{ type: "inputText", text: "inspection failed" }], | 981 | arguments: { path: "package.json" }, |
| 971 | success: false, | 982 | contentItems: [{ type: "inputText", text: "inspection failed" }], |
| 972 | }); | 983 | success: false, |
| 984 | }); | ||
| 973 | 985 | ||
| 974 | assert.deepEqual(projection, { | 986 | assert.deepEqual(projection, { |
| 975 | toolName: "workspace.inspect", | 987 | toolName: "workspace.inspect", |
| 976 | input: { path: "package.json" }, | 988 | input: { path: "package.json" }, |
| 977 | output: [{ type: "inputText", text: "inspection failed" }], | 989 | output: [{ type: "inputText", text: "inspection failed" }], |
| 978 | status: "failed", | 990 | status: "failed", |
| 979 | }); | 991 | }); |
| 980 | }); | 992 | }).pipe(Effect.provide(NodeServices.layer)), |
| 993 | ); | ||
| 981 | }); | 994 | }); |
| 982 | 995 | ||
| 983 | describe("CodexAdapterV2 native protocol logging", () => { | 996 | describe("CodexAdapterV2 native protocol logging", () => { |
| ... | @@ -1703,6 +1716,7 @@ describe("CodexAdapterV2 session initialize", () => { | ... | @@ -1703,6 +1716,7 @@ describe("CodexAdapterV2 session initialize", () => { |
| 1703 | ), | 1716 | ), |
| 1704 | ), | 1717 | ), |
| 1705 | }, | 1718 | }, |
| 1719 | crypto: yield* Crypto.Crypto, | ||
| 1706 | fileSystem: yield* FileSystem.FileSystem, | 1720 | fileSystem: yield* FileSystem.FileSystem, |
| 1707 | idAllocator: yield* IdAllocator.IdAllocatorV2, | 1721 | idAllocator: yield* IdAllocator.IdAllocatorV2, |
| 1708 | serverConfig: yield* makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie), | 1722 | serverConfig: yield* makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie), |
| ... | @@ -1899,6 +1913,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { | ... | @@ -1899,6 +1913,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { |
| 1899 | settings: DEFAULT_CODEX_SETTINGS, | 1913 | settings: DEFAULT_CODEX_SETTINGS, |
| 1900 | environment: {}, | 1914 | environment: {}, |
| 1901 | clientFactory, | 1915 | clientFactory, |
| 1916 | crypto: yield* Crypto.Crypto, | ||
| 1902 | fileSystem, | 1917 | fileSystem, |
| 1903 | idAllocator, | 1918 | idAllocator, |
| 1904 | serverConfig, | 1919 | serverConfig, |
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+35-23| ... | @@ -66,6 +66,7 @@ import * as CodexClient from "effect-codex-app-server/client"; | ... | @@ -66,6 +66,7 @@ import * as CodexClient from "effect-codex-app-server/client"; |
| 66 | import * as CodexErrors from "effect-codex-app-server/errors"; | 66 | import * as CodexErrors from "effect-codex-app-server/errors"; |
| 67 | import * as CodexSchema from "effect-codex-app-server/schema"; | 67 | import * as CodexSchema from "effect-codex-app-server/schema"; |
| 68 | import * as Context from "effect/Context"; | 68 | import * as Context from "effect/Context"; |
| 69 | import * as Crypto from "effect/Crypto"; | ||
| 69 | import * as DateTime from "effect/DateTime"; | 70 | import * as DateTime from "effect/DateTime"; |
| 70 | import * as Deferred from "effect/Deferred"; | 71 | import * as Deferred from "effect/Deferred"; |
| 71 | import * as Effect from "effect/Effect"; | 72 | import * as Effect from "effect/Effect"; |
| ... | @@ -479,26 +480,29 @@ function codexDynamicToolOutput( | ... | @@ -479,26 +480,29 @@ function codexDynamicToolOutput( |
| 479 | return item.success === false ? { success: false } : undefined; | 480 | return item.success === false ? { success: false } : undefined; |
| 480 | } | 481 | } |
| 481 | 482 | ||
| 482 | export function projectCodexDynamicToolItem( | 483 | export const projectCodexDynamicToolItem = Effect.fn("CodexAdapterV2.projectDynamicToolItem")( |
| 483 | item: CodexDynamicToolItem, | 484 | function* ( |
| 484 | ): CodexDynamicToolProjection { | 485 | item: CodexDynamicToolItem, |
| 485 | const output = | 486 | ): Effect.fn.Return<CodexDynamicToolProjection, never, Crypto.Crypto> { |
| 486 | item.type === "mcpToolCall" ? codexMcpToolOutput(item) : codexDynamicToolOutput(item); | 487 | const output = |
| 487 | const toolName = | 488 | item.type === "mcpToolCall" ? codexMcpToolOutput(item) : codexDynamicToolOutput(item); |
| 488 | item.type === "mcpToolCall" | 489 | const toolName = |
| 489 | ? `${item.server}.${item.tool}` | 490 | item.type === "mcpToolCall" |
| 490 | : [trimText(item.namespace), item.tool].filter(Boolean).join("."); | 491 | ? `${item.server}.${item.tool}` |
| 491 | const presentation = item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}; | 492 | : [trimText(item.namespace), item.tool].filter(Boolean).join("."); |
| 492 | const title = dynamicToolTitle(toolName, item.arguments) ?? presentation.title; | 493 | const presentation: McpToolPresentation = |
| 493 | const projection: CodexDynamicToolProjection = { | 494 | item.type === "mcpToolCall" ? yield* mcpToolPresentation(item) : {}; |
| 494 | ...presentation, | 495 | const title = dynamicToolTitle(toolName, item.arguments) ?? presentation.title; |
| 495 | toolName, | 496 | const projection: CodexDynamicToolProjection = { |
| 496 | ...(title ? { title } : {}), | 497 | ...presentation, |
| 497 | input: item.arguments, | 498 | toolName, |
| 498 | status: codexItemStatus(item.status).turnItem, | 499 | ...(title ? { title } : {}), |
| 499 | }; | 500 | input: item.arguments, |
| 500 | return output === undefined ? projection : { ...projection, output }; | 501 | status: codexItemStatus(item.status).turnItem, |
| 501 | } | 502 | }; |
| 503 | return output === undefined ? projection : { ...projection, output }; | ||
| 504 | }, | ||
| 505 | ); | ||
| 502 | 506 | ||
| 503 | function codexNativeItemRef(nativeItemId: string) { | 507 | function codexNativeItemRef(nativeItemId: string) { |
| 504 | return { | 508 | return { |
| ... | @@ -1513,6 +1517,7 @@ export const layerAppServerClientFactory: Layer.Layer< | ... | @@ -1513,6 +1517,7 @@ export const layerAppServerClientFactory: Layer.Layer< |
| 1513 | 1517 | ||
| 1514 | export type CodexAdapterV2DriverEnv = | 1518 | export type CodexAdapterV2DriverEnv = |
| 1515 | | CodexAppServerClientFactory | 1519 | | CodexAppServerClientFactory |
| 1520 | | Crypto.Crypto | ||
| 1516 | | FileSystem.FileSystem | 1521 | | FileSystem.FileSystem |
| 1517 | | IdAllocatorV2 | 1522 | | IdAllocatorV2 |
| 1518 | | Path.Path | 1523 | | Path.Path |
| ... | @@ -1525,6 +1530,7 @@ export const createCodexAdapterV2 = ( | ... | @@ -1525,6 +1530,7 @@ export const createCodexAdapterV2 = ( |
| 1525 | Effect.gen(function* () { | 1530 | Effect.gen(function* () { |
| 1526 | const clientFactory = yield* CodexAppServerClientFactory; | 1531 | const clientFactory = yield* CodexAppServerClientFactory; |
| 1527 | const continuationRequests = yield* ProviderContinuationRequests; | 1532 | const continuationRequests = yield* ProviderContinuationRequests; |
| 1533 | const crypto = yield* Crypto.Crypto; | ||
| 1528 | const fileSystem = yield* FileSystem.FileSystem; | 1534 | const fileSystem = yield* FileSystem.FileSystem; |
| 1529 | const hostEnvironment = yield* HostProcessEnvironment; | 1535 | const hostEnvironment = yield* HostProcessEnvironment; |
| 1530 | const idAllocator = yield* IdAllocatorV2; | 1536 | const idAllocator = yield* IdAllocatorV2; |
| ... | @@ -1555,6 +1561,7 @@ export const createCodexAdapterV2 = ( | ... | @@ -1555,6 +1561,7 @@ export const createCodexAdapterV2 = ( |
| 1555 | settings, | 1561 | settings, |
| 1556 | environment: mergeProviderInstanceEnvironment(environment, hostEnvironment), | 1562 | environment: mergeProviderInstanceEnvironment(environment, hostEnvironment), |
| 1557 | clientFactory, | 1563 | clientFactory, |
| 1564 | crypto, | ||
| 1558 | fileSystem, | 1565 | fileSystem, |
| 1559 | idAllocator, | 1566 | idAllocator, |
| 1560 | serverConfig, | 1567 | serverConfig, |
| ... | @@ -1573,12 +1580,13 @@ export const CodexAdapterV2Driver: ProviderAdapterDriver<CodexSettings, CodexAda | ... | @@ -1573,12 +1580,13 @@ export const CodexAdapterV2Driver: ProviderAdapterDriver<CodexSettings, CodexAda |
| 1573 | const layer: Layer.Layer< | 1580 | const layer: Layer.Layer< |
| 1574 | ProviderAdapterV2, | 1581 | ProviderAdapterV2, |
| 1575 | never, | 1582 | never, |
| 1576 | CodexAppServerClientFactory | FileSystem.FileSystem | IdAllocatorV2 | ServerConfig | 1583 | CodexAppServerClientFactory | Crypto.Crypto | FileSystem.FileSystem | IdAllocatorV2 | ServerConfig |
| 1577 | > = Layer.effect( | 1584 | > = Layer.effect( |
| 1578 | ProviderAdapterV2, | 1585 | ProviderAdapterV2, |
| 1579 | Effect.gen(function* () { | 1586 | Effect.gen(function* () { |
| 1580 | const clientFactory = yield* CodexAppServerClientFactory; | 1587 | const clientFactory = yield* CodexAppServerClientFactory; |
| 1581 | const continuationRequests = yield* ProviderContinuationRequests; | 1588 | const continuationRequests = yield* ProviderContinuationRequests; |
| 1589 | const crypto = yield* Crypto.Crypto; | ||
| 1582 | const fileSystem = yield* FileSystem.FileSystem; | 1590 | const fileSystem = yield* FileSystem.FileSystem; |
| 1583 | const hostEnvironment = yield* HostProcessEnvironment; | 1591 | const hostEnvironment = yield* HostProcessEnvironment; |
| 1584 | const idAllocator = yield* IdAllocatorV2; | 1592 | const idAllocator = yield* IdAllocatorV2; |
| ... | @@ -1589,6 +1597,7 @@ const layer: Layer.Layer< | ... | @@ -1589,6 +1597,7 @@ const layer: Layer.Layer< |
| 1589 | settings: DEFAULT_CODEX_SETTINGS, | 1597 | settings: DEFAULT_CODEX_SETTINGS, |
| 1590 | environment: hostEnvironment, | 1598 | environment: hostEnvironment, |
| 1591 | clientFactory, | 1599 | clientFactory, |
| 1600 | crypto, | ||
| 1592 | fileSystem, | 1601 | fileSystem, |
| 1593 | idAllocator, | 1602 | idAllocator, |
| 1594 | serverConfig, | 1603 | serverConfig, |
| ... | @@ -1609,6 +1618,7 @@ export interface CodexAdapterV2Options { | ... | @@ -1609,6 +1618,7 @@ export interface CodexAdapterV2Options { |
| 1609 | * Codex with a current access token. | 1618 | * Codex with a current access token. |
| 1610 | */ | 1619 | */ |
| 1611 | readonly resolveRuntime?: Effect.Effect<CodexEffectiveRuntime, ProviderSetupError, Scope.Scope>; | 1620 | readonly resolveRuntime?: Effect.Effect<CodexEffectiveRuntime, ProviderSetupError, Scope.Scope>; |
| 1621 | readonly crypto: Crypto.Crypto; | ||
| 1612 | readonly fileSystem: FileSystem.FileSystem; | 1622 | readonly fileSystem: FileSystem.FileSystem; |
| 1613 | readonly idAllocator: IdAllocatorV2Shape; | 1623 | readonly idAllocator: IdAllocatorV2Shape; |
| 1614 | readonly serverConfig: ServerConfig["Service"]; | 1624 | readonly serverConfig: ServerConfig["Service"]; |
| ... | @@ -1623,7 +1633,7 @@ export interface CodexAdapterV2Options { | ... | @@ -1623,7 +1633,7 @@ export interface CodexAdapterV2Options { |
| 1623 | } | 1633 | } |
| 1624 | 1634 | ||
| 1625 | export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): ProviderAdapterV2Shape { | 1635 | export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): ProviderAdapterV2Shape { |
| 1626 | const { clientFactory, fileSystem, idAllocator, serverConfig } = adapterOptions; | 1636 | const { clientFactory, crypto, fileSystem, idAllocator, serverConfig } = adapterOptions; |
| 1627 | const continuationRequests = adapterOptions.continuationRequests; | 1637 | const continuationRequests = adapterOptions.continuationRequests; |
| 1628 | 1638 | ||
| 1629 | return ProviderAdapterV2.of({ | 1639 | return ProviderAdapterV2.of({ |
| ... | @@ -3480,7 +3490,9 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi | ... | @@ -3480,7 +3490,9 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi |
| 3480 | nativeItemId: item.id, | 3490 | nativeItemId: item.id, |
| 3481 | }); | 3491 | }); |
| 3482 | const { ordinal, startedAt } = yield* resolveItemPosition(context, item.id); | 3492 | const { ordinal, startedAt } = yield* resolveItemPosition(context, item.id); |
| 3483 | const projection = projectCodexDynamicToolItem(item); | 3493 | const projection = yield* projectCodexDynamicToolItem(item).pipe( |
| 3494 | Effect.provideService(Crypto.Crypto, crypto), | ||
| 3495 | ); | ||
| 3484 | const node: OrchestrationV2ExecutionNode = { | 3496 | const node: OrchestrationV2ExecutionNode = { |
| 3485 | id: nodeId, | 3497 | id: nodeId, |
| 3486 | threadId: context.projectionThreadId, | 3498 | threadId: context.projectionThreadId, |
apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts+20-15| ... | @@ -3,6 +3,7 @@ | ... | @@ -3,6 +3,7 @@ |
| 3 | * driven through the real adapter and `@opencode/client` against a replayed | 3 | * driven through the real adapter and `@opencode/client` against a replayed |
| 4 | * HTTP server. Frames reuse the shapes recorded against 2.0.18. | 4 | * HTTP server. Frames reuse the shapes recorded against 2.0.18. |
| 5 | */ | 5 | */ |
| 6 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 6 | import { assert, it } from "@effect/vitest"; | 7 | import { assert, it } from "@effect/vitest"; |
| 7 | import { | 8 | import { |
| 8 | CheckpointId, | 9 | CheckpointId, |
| ... | @@ -2766,21 +2767,25 @@ describe("OpenCode2 adapter", () => { | ... | @@ -2766,21 +2767,25 @@ describe("OpenCode2 adapter", () => { |
| 2766 | }).pipe(Effect.scoped), | 2767 | }).pipe(Effect.scoped), |
| 2767 | ); | 2768 | ); |
| 2768 | 2769 | ||
| 2769 | it("names each thread's MCP server within OpenCode's limits, one name per thread", () => { | 2770 | it.effect("names each thread's MCP server within OpenCode's limits, one name per thread", () => |
| 2770 | const project = "thread:project:ce04e4e2-6c29-4ff0-a1d7-b089dd63e258"; | 2771 | Effect.gen(function* () { |
| 2771 | const ids = [ | 2772 | const project = "thread:project:ce04e4e2-6c29-4ff0-a1d7-b089dd63e258"; |
| 2772 | `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c4`, | 2773 | const ids = [ |
| 2773 | `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c5`, | 2774 | `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c4`, |
| 2774 | "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around1", | 2775 | `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c5`, |
| 2775 | "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around2", | 2776 | "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around1", |
| 2776 | ]; | 2777 | "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around2", |
| 2777 | const names = ids.map(t3McpServerName); | 2778 | ]; |
| 2778 | for (const name of names) assert.match(name, /^t3-code-[A-Za-z0-9_-]{1,56}$/); | 2779 | const names = yield* Effect.forEach(ids, t3McpServerName); |
| 2779 | assert.equal(new Set(names).size, ids.length); | 2780 | for (const name of names) assert.match(name, /^t3-code-[A-Za-z0-9_-]{1,56}$/); |
| 2780 | assert.deepEqual(ids.map(t3McpServerName), names); | 2781 | assert.equal(new Set(names).size, ids.length); |
| 2781 | // A name that already fits stays readable. | 2782 | assert.deepEqual(yield* Effect.forEach(ids, t3McpServerName), names); |
| 2782 | assert.equal(t3McpServerName(threadId), "t3-code-thread_opencode2-adapter"); | 2783 | // A digested name is the one the synchronous node:crypto version produced. |
| 2783 | }); | 2784 | assert.equal(names[0], "t3-code-63abb5df2b188bdd"); |
| 2785 | // A name that already fits stays readable. | ||
| 2786 | assert.equal(yield* t3McpServerName(threadId), "t3-code-thread_opencode2-adapter"); | ||
| 2787 | }).pipe(Effect.provide(NodeCrypto.layer)), | ||
| 2788 | ); | ||
| 2784 | 2789 | ||
| 2785 | it.effect("reads user and assistant text from the session's message list", () => | 2790 | it.effect("reads user and assistant text from the session's message list", () => |
| 2786 | Effect.gen(function* () { | 2791 | Effect.gen(function* () { |
apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts+14-2| ... | @@ -284,7 +284,13 @@ function layerRegistry( | ... | @@ -284,7 +284,13 @@ function layerRegistry( |
| 284 | Effect.map((adapter) => ProviderAdapterRegistry.layerFromAdapters([adapter])), | 284 | Effect.map((adapter) => ProviderAdapterRegistry.layerFromAdapters([adapter])), |
| 285 | ), | 285 | ), |
| 286 | ).pipe( | 286 | ).pipe( |
| 287 | Layer.provide(Layer.mergeAll(layerReplayServerConfig(transcript.scenario), IdAllocator.layer)), | 287 | Layer.provide( |
| 288 | Layer.mergeAll( | ||
| 289 | layerReplayServerConfig(transcript.scenario), | ||
| 290 | IdAllocator.layer, | ||
| 291 | NodeServices.layer, | ||
| 292 | ), | ||
| 293 | ), | ||
| 288 | ); | 294 | ); |
| 289 | } | 295 | } |
| 290 | 296 | ||
| ... | @@ -322,7 +328,13 @@ export const openCode2ReplayRuntime = ( | ... | @@ -322,7 +328,13 @@ export const openCode2ReplayRuntime = ( |
| 322 | }, | 328 | }, |
| 323 | }); | 329 | }); |
| 324 | }).pipe( | 330 | }).pipe( |
| 325 | Effect.provide(Layer.mergeAll(layerReplayServerConfig("opencode2_adapter"), IdAllocator.layer)), | 331 | Effect.provide( |
| 332 | Layer.mergeAll( | ||
| 333 | layerReplayServerConfig("opencode2_adapter"), | ||
| 334 | IdAllocator.layer, | ||
| 335 | NodeServices.layer, | ||
| 336 | ), | ||
| 337 | ), | ||
| 326 | ); | 338 | ); |
| 327 | 339 | ||
| 328 | export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< | 340 | export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< |
apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts+19-12| ... | @@ -21,7 +21,6 @@ | ... | @@ -21,7 +21,6 @@ |
| 21 | * | 21 | * |
| 22 | * @module orchestration-v2/Adapters/OpenCode2AdapterV2 | 22 | * @module orchestration-v2/Adapters/OpenCode2AdapterV2 |
| 23 | */ | 23 | */ |
| 24 | import * as NodeCrypto from "node:crypto"; | ||
| 25 | 24 | ||
| 26 | import { | 25 | import { |
| 27 | AbsolutePath, | 26 | AbsolutePath, |
| ... | @@ -59,7 +58,9 @@ import { | ... | @@ -59,7 +58,9 @@ import { |
| 59 | import type * as Cause from "effect/Cause"; | 58 | import type * as Cause from "effect/Cause"; |
| 60 | import * as DateTime from "effect/DateTime"; | 59 | import * as DateTime from "effect/DateTime"; |
| 61 | import * as Deferred from "effect/Deferred"; | 60 | import * as Deferred from "effect/Deferred"; |
| 61 | import * as Crypto from "effect/Crypto"; | ||
| 62 | import * as Effect from "effect/Effect"; | 62 | import * as Effect from "effect/Effect"; |
| 63 | import * as Hex from "effect/encoding/Hex"; | ||
| 63 | import * as Exit from "effect/Exit"; | 64 | import * as Exit from "effect/Exit"; |
| 64 | import * as Option from "effect/Option"; | 65 | import * as Option from "effect/Option"; |
| 65 | import * as Queue from "effect/Queue"; | 66 | import * as Queue from "effect/Queue"; |
| ... | @@ -453,12 +454,15 @@ const rule = (action: string, effect: Rule["effect"]): Rule => ({ action, resour | ... | @@ -453,12 +454,15 @@ const rule = (action: string, effect: Rule["effect"]): Rule => ({ action, resour |
| 453 | * tool namespace limit), and its router rejects adding one over 100, so a | 454 | * tool namespace limit), and its router rejects adding one over 100, so a |
| 454 | * thread id that does not fit is replaced by a digest of it. | 455 | * thread id that does not fit is replaced by a digest of it. |
| 455 | */ | 456 | */ |
| 456 | export const t3McpServerName = (threadId: string) => { | 457 | export const t3McpServerName = Effect.fn("t3McpServerName")(function* (threadId: string) { |
| 457 | const name = `t3-code-${threadId.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}`; | 458 | const name = `t3-code-${threadId.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}`; |
| 458 | if (name.length <= 64) return name; | 459 | if (name.length <= 64) return name; |
| 459 | const digest = NodeCrypto.createHash("sha256").update(threadId).digest("hex"); | 460 | const crypto = yield* Crypto.Crypto; |
| 460 | return `t3-code-${digest.slice(0, 16)}`; | 461 | const digest = yield* crypto |
| 461 | }; | 462 | .digest("SHA-256", new TextEncoder().encode(threadId)) |
| 463 | .pipe(Effect.orDie); | ||
| 464 | return `t3-code-${Hex.encode(digest).slice(0, 16)}`; | ||
| 465 | }); | ||
| 462 | 466 | ||
| 463 | /** | 467 | /** |
| 464 | * The rules that keep T3's MCP servers to their own thread, after the mode's: | 468 | * The rules that keep T3's MCP servers to their own thread, after the mode's: |
| ... | @@ -466,19 +470,19 @@ export const t3McpServerName = (threadId: string) => { | ... | @@ -466,19 +470,19 @@ export const t3McpServerName = (threadId: string) => { |
| 466 | * this thread's own is allowed again, in every mode. A subagent's session | 470 | * this thread's own is allowed again, in every mode. A subagent's session |
| 467 | * inherits the thread's. | 471 | * inherits the thread's. |
| 468 | */ | 472 | */ |
| 469 | const mcpRules = (threadId: string | null): ReadonlyArray<Rule> => | 473 | const mcpRules = (mcpServerName: string | null): ReadonlyArray<Rule> => |
| 470 | threadId === null | 474 | mcpServerName === null |
| 471 | ? [] | 475 | ? [] |
| 472 | : [ | 476 | : [ |
| 473 | { action: "t3-code-*", resource: "*", effect: "deny" }, | 477 | { action: "t3-code-*", resource: "*", effect: "deny" }, |
| 474 | { action: `${t3McpServerName(threadId)}_*`, resource: "*", effect: "allow" }, | 478 | { action: `${mcpServerName}_*`, resource: "*", effect: "allow" }, |
| 475 | ]; | 479 | ]; |
| 476 | 480 | ||
| 477 | const sessionRules = ( | 481 | const sessionRules = ( |
| 478 | policy: RulesPolicy, | 482 | policy: RulesPolicy, |
| 479 | paths: ReadonlyArray<Rule>, | 483 | paths: ReadonlyArray<Rule>, |
| 480 | grants: ReadonlyArray<Rule>, | 484 | grants: ReadonlyArray<Rule>, |
| 481 | threadId: string | null, | 485 | mcpServerName: string | null, |
| 482 | ): ReadonlyArray<Rule> => [ | 486 | ): ReadonlyArray<Rule> => [ |
| 483 | ...(policy.runtimeMode === "full-access" | 487 | ...(policy.runtimeMode === "full-access" |
| 484 | ? [rule("*", "allow")] | 488 | ? [rule("*", "allow")] |
| ... | @@ -492,7 +496,7 @@ const sessionRules = ( | ... | @@ -492,7 +496,7 @@ const sessionRules = ( |
| 492 | // are never denied: the free tier refuses sessions whose rules deny them. | 496 | // are never denied: the free tier refuses sessions whose rules deny them. |
| 493 | ...(policy.interactionMode === "plan" ? [rule("edit", "deny")] : []), | 497 | ...(policy.interactionMode === "plan" ? [rule("edit", "deny")] : []), |
| 494 | ...paths, | 498 | ...paths, |
| 495 | ...mcpRules(threadId), | 499 | ...mcpRules(mcpServerName), |
| 496 | ]; | 500 | ]; |
| 497 | 501 | ||
| 498 | const sameRules = (left: ReadonlyArray<Rule> | undefined, right: ReadonlyArray<Rule>) => | 502 | const sameRules = (left: ReadonlyArray<Rule> | undefined, right: ReadonlyArray<Rule>) => |
| ... | @@ -828,7 +832,10 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -828,7 +832,10 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 828 | const idAllocator = yield* IdAllocator.IdAllocatorV2; | 832 | const idAllocator = yield* IdAllocator.IdAllocatorV2; |
| 829 | const serverConfig = yield* ServerConfig.ServerConfig; | 833 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 830 | const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; | 834 | const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; |
| 835 | const crypto = yield* Crypto.Crypto; | ||
| 831 | const driver = OPENCODE_PROVIDER; | 836 | const driver = OPENCODE_PROVIDER; |
| 837 | const mcpServerNameFor = (threadId: string) => | ||
| 838 | t3McpServerName(threadId).pipe(Effect.provideService(Crypto.Crypto, crypto)); | ||
| 832 | 839 | ||
| 833 | /** | 840 | /** |
| 834 | * Lends the instance's server to a session until its scope closes. A spawned | 841 | * Lends the instance's server to a session until its scope closes. A spawned |
| ... | @@ -2977,7 +2984,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -2977,7 +2984,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 2977 | policy, | 2984 | policy, |
| 2978 | paths, | 2985 | paths, |
| 2979 | policy.runtimeMode === "full-access" ? [] : thread.grants, | 2986 | policy.runtimeMode === "full-access" ? [] : thread.grants, |
| 2980 | appThreadId, | 2987 | appThreadId === null ? null : yield* mcpServerNameFor(appThreadId), |
| 2981 | ); | 2988 | ); |
| 2982 | }); | 2989 | }); |
| 2983 | 2990 | ||
| ... | @@ -3234,7 +3241,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -3234,7 +3241,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 3234 | ) { | 3241 | ) { |
| 3235 | const mcpSession = McpProviderSession.readMcpProviderSession(turnInput.threadId); | 3242 | const mcpSession = McpProviderSession.readMcpProviderSession(turnInput.threadId); |
| 3236 | const directory = turnInput.runtimePolicy.cwd ?? serverConfig.cwd; | 3243 | const directory = turnInput.runtimePolicy.cwd ?? serverConfig.cwd; |
| 3237 | const name = t3McpServerName(turnInput.threadId); | 3244 | const name = yield* mcpServerNameFor(turnInput.threadId); |
| 3238 | // An external server may not reach T3's MCP endpoint, as with 1.x. | 3245 | // An external server may not reach T3's MCP endpoint, as with 1.x. |
| 3239 | const wanted = | 3246 | const wanted = |
| 3240 | mcpSession === undefined || connection.external | 3247 | mcpSession === undefined || connection.external |
apps/server/src/orchestration-v2/CheckpointCaptureService.test.ts+2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import { | 3 | import { |
| 3 | CheckpointId, | 4 | CheckpointId, |
| ... | @@ -274,6 +275,7 @@ it.layer(layerProjectionStoreTest)("CheckpointCaptureServiceV2", (it) => { | ... | @@ -274,6 +275,7 @@ it.layer(layerProjectionStoreTest)("CheckpointCaptureServiceV2", (it) => { |
| 274 | Layer.provide( | 275 | Layer.provide( |
| 275 | Layer.mergeAll( | 276 | Layer.mergeAll( |
| 276 | IdAllocator.layer, | 277 | IdAllocator.layer, |
| 278 | NodeCrypto.layer, | ||
| 277 | Layer.mock(CheckpointStore.CheckpointStore)({ | 279 | Layer.mock(CheckpointStore.CheckpointStore)({ |
| 278 | isGitRepository: () => Effect.succeed(true), | 280 | isGitRepository: () => Effect.succeed(true), |
| 279 | captureCheckpoint: () => Effect.void, | 281 | captureCheckpoint: () => Effect.void, |
apps/server/src/orchestration-v2/CheckpointScopeOwnership.test.ts+10-9| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as DateTime from "effect/DateTime"; | 3 | import * as DateTime from "effect/DateTime"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| ... | @@ -24,6 +25,7 @@ const layerProjection = Layer.mergeAll( | ... | @@ -24,6 +25,7 @@ const layerProjection = Layer.mergeAll( |
| 24 | ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)), | 25 | ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)), |
| 25 | SqlitePersistence.layerMemory, | 26 | SqlitePersistence.layerMemory, |
| 26 | IdAllocator.layer, | 27 | IdAllocator.layer, |
| 28 | NodeCrypto.layer, | ||
| 27 | ); | 29 | ); |
| 28 | it.effect("resolves the thread baseline after a second root run replaces scope ownership", () => | 30 | it.effect("resolves the thread baseline after a second root run replaces scope ownership", () => |
| 29 | Effect.gen(function* () { | 31 | Effect.gen(function* () { |
| ... | @@ -38,6 +40,11 @@ it.effect("resolves the thread baseline after a second root run replaces scope o | ... | @@ -38,6 +40,11 @@ it.effect("resolves the thread baseline after a second root run replaces scope o |
| 38 | const providerThreadId = ProviderThreadId.make("provider-thread:audit-root-scope"); | 40 | const providerThreadId = ProviderThreadId.make("provider-thread:audit-root-scope"); |
| 39 | const providerInstanceId = ProviderInstanceId.make("codex"); | 41 | const providerInstanceId = ProviderInstanceId.make("codex"); |
| 40 | const scopeId = yield* ids.allocate.checkpointScope({ threadId, name: "root" }); | 42 | const scopeId = yield* ids.allocate.checkpointScope({ threadId, name: "root" }); |
| 43 | const checkpointRefs = [ | ||
| 44 | yield* checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 0 }), | ||
| 45 | yield* checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 1 }), | ||
| 46 | yield* checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 2 }), | ||
| 47 | ]; | ||
| 41 | const baselineId = CheckpointId.make("checkpoint:audit-root-scope:0"); | 48 | const baselineId = CheckpointId.make("checkpoint:audit-root-scope:0"); |
| 42 | const firstCheckpointId = CheckpointId.make("checkpoint:audit-root-scope:1"); | 49 | const firstCheckpointId = CheckpointId.make("checkpoint:audit-root-scope:1"); |
| 43 | const secondCheckpointId = CheckpointId.make("checkpoint:audit-root-scope:2"); | 50 | 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 | ... | @@ -154,7 +161,7 @@ it.effect("resolves the thread baseline after a second root run replaces scope o |
| 154 | input.ordinal === 0 ? null : input.ordinal === 1 ? baselineId : firstCheckpointId, | 161 | input.ordinal === 0 ? null : input.ordinal === 1 ? baselineId : firstCheckpointId, |
| 155 | ordinalWithinScope: input.ordinal, | 162 | ordinalWithinScope: input.ordinal, |
| 156 | appRunOrdinal: input.appRunOrdinal, | 163 | appRunOrdinal: input.appRunOrdinal, |
| 157 | ref: checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: input.ordinal }), | 164 | ref: checkpointRefs[input.ordinal]!, |
| 158 | status: "ready" as const, | 165 | status: "ready" as const, |
| 159 | files: [], | 166 | files: [], |
| 160 | capturedAt: now, | 167 | capturedAt: now, |
| ... | @@ -214,14 +221,8 @@ it.effect("resolves the thread baseline after a second root run replaces scope o | ... | @@ -214,14 +221,8 @@ it.effect("resolves the thread baseline after a second root run replaces scope o |
| 214 | Layer.mock(CheckpointStore.CheckpointStore)({ | 221 | Layer.mock(CheckpointStore.CheckpointStore)({ |
| 215 | diffCheckpoints: (input) => { | 222 | diffCheckpoints: (input) => { |
| 216 | assert.equal(input.cwd, "/prepared-repo"); | 223 | assert.equal(input.cwd, "/prepared-repo"); |
| 217 | assert.equal( | 224 | assert.equal(input.fromCheckpointRef, checkpointRefs[0]); |
| 218 | input.fromCheckpointRef, | 225 | assert.equal(input.toCheckpointRef, checkpointRefs[2]); |
| 219 | checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 0 }), | ||
| 220 | ); | ||
| 221 | assert.equal( | ||
| 222 | input.toCheckpointRef, | ||
| 223 | checkpointRefForScopeOrdinal({ scopeId, ordinalWithinScope: 2 }), | ||
| 224 | ); | ||
| 225 | return Effect.succeed("two-run diff"); | 226 | return Effect.succeed("two-run diff"); |
| 226 | }, | 227 | }, |
| 227 | }), | 228 | }), |
apps/server/src/orchestration-v2/CheckpointService.test.ts+3-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { assert, it, vi } from "@effect/vitest"; | 2 | import { assert, it, vi } from "@effect/vitest"; |
| 2 | import { | 3 | import { |
| 3 | CheckpointScopeId, | 4 | CheckpointScopeId, |
| ... | @@ -58,6 +59,7 @@ it.effect.each([false, true, "interrupt"] as const)( | ... | @@ -58,6 +59,7 @@ it.effect.each([false, true, "interrupt"] as const)( |
| 58 | }), | 59 | }), |
| 59 | ), | 60 | ), |
| 60 | ), | 61 | ), |
| 62 | Layer.provideMerge(NodeCrypto.layer), | ||
| 61 | ); | 63 | ); |
| 62 | 64 | ||
| 63 | return Effect.gen(function* () { | 65 | return Effect.gen(function* () { |
| ... | @@ -88,7 +90,7 @@ it.effect.each([false, true, "interrupt"] as const)( | ... | @@ -88,7 +90,7 @@ it.effect.each([false, true, "interrupt"] as const)( |
| 88 | assert.equal(baseline.ordinalWithinScope, 2); | 90 | assert.equal(baseline.ordinalWithinScope, 2); |
| 89 | assert.equal( | 91 | assert.equal( |
| 90 | baseline.ref, | 92 | baseline.ref, |
| 91 | CheckpointService.checkpointRefForScopeOrdinal({ | 93 | yield* CheckpointService.checkpointRefForScopeOrdinal({ |
| 92 | scopeId: scope.id, | 94 | scopeId: scope.id, |
| 93 | ordinalWithinScope: 2, | 95 | ordinalWithinScope: 2, |
| 94 | }), | 96 | }), |
apps/server/src/orchestration-v2/CheckpointService.ts+22-15| ... | @@ -9,11 +9,12 @@ import { | ... | @@ -9,11 +9,12 @@ import { |
| 9 | RunId, | 9 | RunId, |
| 10 | ThreadId, | 10 | ThreadId, |
| 11 | } from "@t3tools/contracts"; | 11 | } from "@t3tools/contracts"; |
| 12 | import * as NodeCrypto from "node:crypto"; | ||
| 13 | import * as Context from "effect/Context"; | 12 | import * as Context from "effect/Context"; |
| 13 | import * as Crypto from "effect/Crypto"; | ||
| 14 | import * as DateTime from "effect/DateTime"; | 14 | import * as DateTime from "effect/DateTime"; |
| 15 | import * as Effect from "effect/Effect"; | 15 | import * as Effect from "effect/Effect"; |
| 16 | import * as Base64Url from "effect/encoding/Base64Url"; | 16 | import * as Base64Url from "effect/encoding/Base64Url"; |
| 17 | import * as Hex from "effect/encoding/Hex"; | ||
| 17 | import * as KeyedLock from "@t3tools/shared/KeyedLock"; | 18 | import * as KeyedLock from "@t3tools/shared/KeyedLock"; |
| 18 | import * as Layer from "effect/Layer"; | 19 | import * as Layer from "effect/Layer"; |
| 19 | import * as Schema from "effect/Schema"; | 20 | import * as Schema from "effect/Schema"; |
| ... | @@ -157,15 +158,18 @@ export class CheckpointServiceV2 extends Context.Service< | ... | @@ -157,15 +158,18 @@ export class CheckpointServiceV2 extends Context.Service< |
| 157 | CheckpointServiceV2Shape | 158 | CheckpointServiceV2Shape |
| 158 | >()("t3/orchestration-v2/CheckpointService/CheckpointServiceV2") {} | 159 | >()("t3/orchestration-v2/CheckpointService/CheckpointServiceV2") {} |
| 159 | 160 | ||
| 160 | export function checkpointRefForScopeOrdinal(input: { | 161 | export const checkpointRefForScopeOrdinal = Effect.fn("checkpointRefForScopeOrdinal")( |
| 161 | readonly scopeId: CheckpointScopeId; | 162 | function* (input: { readonly scopeId: CheckpointScopeId; readonly ordinalWithinScope: number }) { |
| 162 | readonly ordinalWithinScope: number; | 163 | const crypto = yield* Crypto.Crypto; |
| 163 | }): CheckpointRef { | 164 | const digest = yield* crypto |
| 164 | const scopeKey = NodeCrypto.createHash("sha256").update(input.scopeId).digest("hex").slice(0, 32); | 165 | .digest("SHA-256", new TextEncoder().encode(input.scopeId)) |
| 165 | return CheckpointRef.make( | 166 | .pipe(Effect.orDie); |
| 166 | `${CHECKPOINT_REFS_PREFIX}/${Base64Url.encode(scopeKey)}/ordinal/${input.ordinalWithinScope}`, | 167 | const scopeKey = Hex.encode(digest).slice(0, 32); |
| 167 | ); | 168 | return CheckpointRef.make( |
| 168 | } | 169 | `${CHECKPOINT_REFS_PREFIX}/${Base64Url.encode(scopeKey)}/ordinal/${input.ordinalWithinScope}`, |
| 170 | ); | ||
| 171 | }, | ||
| 172 | ); | ||
| 169 | 173 | ||
| 170 | function checkpointIdForScopeOrdinal( | 174 | function checkpointIdForScopeOrdinal( |
| 171 | idAllocator: IdAllocator.IdAllocatorV2Shape, | 175 | idAllocator: IdAllocator.IdAllocatorV2Shape, |
| ... | @@ -242,11 +246,14 @@ function makeCheckpoint(input: { | ... | @@ -242,11 +246,14 @@ function makeCheckpoint(input: { |
| 242 | export const layer: Layer.Layer< | 246 | export const layer: Layer.Layer< |
| 243 | CheckpointServiceV2, | 247 | CheckpointServiceV2, |
| 244 | never, | 248 | never, |
| 245 | CheckpointStore.CheckpointStore | IdAllocator.IdAllocatorV2 | 249 | CheckpointStore.CheckpointStore | Crypto.Crypto | IdAllocator.IdAllocatorV2 |
| 246 | > = Layer.effect( | 250 | > = Layer.effect( |
| 247 | CheckpointServiceV2, | 251 | CheckpointServiceV2, |
| 248 | Effect.gen(function* () { | 252 | Effect.gen(function* () { |
| 249 | const checkpointStore = yield* CheckpointStore.CheckpointStore; | 253 | const checkpointStore = yield* CheckpointStore.CheckpointStore; |
| 254 | const crypto = yield* Crypto.Crypto; | ||
| 255 | const checkpointRefFor = (input: Parameters<typeof checkpointRefForScopeOrdinal>[0]) => | ||
| 256 | checkpointRefForScopeOrdinal(input).pipe(Effect.provideService(Crypto.Crypto, crypto)); | ||
| 250 | const idAllocator = yield* IdAllocator.IdAllocatorV2; | 257 | const idAllocator = yield* IdAllocator.IdAllocatorV2; |
| 251 | const workspaceLocks = yield* KeyedLock.make<string>(); | 258 | const workspaceLocks = yield* KeyedLock.make<string>(); |
| 252 | const withWorkspaceLock = <A, E, R>(cwd: string, effect: Effect.Effect<A, E, R>) => | 259 | const withWorkspaceLock = <A, E, R>(cwd: string, effect: Effect.Effect<A, E, R>) => |
| ... | @@ -265,7 +272,7 @@ export const layer: Layer.Layer< | ... | @@ -265,7 +272,7 @@ export const layer: Layer.Layer< |
| 265 | return; | 272 | return; |
| 266 | } | 273 | } |
| 267 | 274 | ||
| 268 | const checkpointRef = checkpointRefForScopeOrdinal({ | 275 | const checkpointRef = yield* checkpointRefFor({ |
| 269 | scopeId: input.scope.id, | 276 | scopeId: input.scope.id, |
| 270 | ordinalWithinScope: input.ordinalWithinScope, | 277 | ordinalWithinScope: input.ordinalWithinScope, |
| 271 | }); | 278 | }); |
| ... | @@ -298,7 +305,7 @@ export const layer: Layer.Layer< | ... | @@ -298,7 +305,7 @@ export const layer: Layer.Layer< |
| 298 | withWorkspaceLock( | 305 | withWorkspaceLock( |
| 299 | input.scope.cwd, | 306 | input.scope.cwd, |
| 300 | Effect.gen(function* () { | 307 | Effect.gen(function* () { |
| 301 | const checkpointRef = checkpointRefForScopeOrdinal({ | 308 | const checkpointRef = yield* checkpointRefFor({ |
| 302 | scopeId: input.scope.id, | 309 | scopeId: input.scope.id, |
| 303 | ordinalWithinScope: input.ordinalWithinScope, | 310 | ordinalWithinScope: input.ordinalWithinScope, |
| 304 | }); | 311 | }); |
| ... | @@ -362,11 +369,11 @@ export const layer: Layer.Layer< | ... | @@ -362,11 +369,11 @@ export const layer: Layer.Layer< |
| 362 | ordinalWithinScope: input.ordinalWithinScope - 1, | 369 | ordinalWithinScope: input.ordinalWithinScope - 1, |
| 363 | }) | 370 | }) |
| 364 | : null; | 371 | : null; |
| 365 | const checkpointRef = checkpointRefForScopeOrdinal({ | 372 | const checkpointRef = yield* checkpointRefFor({ |
| 366 | scopeId: input.scope.id, | 373 | scopeId: input.scope.id, |
| 367 | ordinalWithinScope: input.ordinalWithinScope, | 374 | ordinalWithinScope: input.ordinalWithinScope, |
| 368 | }); | 375 | }); |
| 369 | const previousCheckpointRef = checkpointRefForScopeOrdinal({ | 376 | const previousCheckpointRef = yield* checkpointRefFor({ |
| 370 | scopeId: input.scope.id, | 377 | scopeId: input.scope.id, |
| 371 | ordinalWithinScope: Math.max(0, input.ordinalWithinScope - 1), | 378 | ordinalWithinScope: Math.max(0, input.ordinalWithinScope - 1), |
| 372 | }); | 379 | }); |
apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts+2| ... | @@ -15,6 +15,7 @@ import { | ... | @@ -15,6 +15,7 @@ import { |
| 15 | type OrchestrationV2DomainEvent, | 15 | type OrchestrationV2DomainEvent, |
| 16 | } from "@t3tools/contracts"; | 16 | } from "@t3tools/contracts"; |
| 17 | import * as DateTime from "effect/DateTime"; | 17 | import * as DateTime from "effect/DateTime"; |
| 18 | import * as Crypto from "effect/Crypto"; | ||
| 18 | import * as Effect from "effect/Effect"; | 19 | import * as Effect from "effect/Effect"; |
| 19 | import * as FileSystem from "effect/FileSystem"; | 20 | import * as FileSystem from "effect/FileSystem"; |
| 20 | import * as Fiber from "effect/Fiber"; | 21 | import * as Fiber from "effect/Fiber"; |
| ... | @@ -137,6 +138,7 @@ it.effect.each(["child completion", "scheduled message", "user steering"] as con | ... | @@ -137,6 +138,7 @@ it.effect.each(["child completion", "scheduled message", "user steering"] as con |
| 137 | attachmentsDir: cwd, | 138 | attachmentsDir: cwd, |
| 138 | fileSystem: yield* FileSystem.FileSystem, | 139 | fileSystem: yield* FileSystem.FileSystem, |
| 139 | path: yield* Path.Path, | 140 | path: yield* Path.Path, |
| 141 | crypto: yield* Crypto.Crypto, | ||
| 140 | idAllocator: yield* IdAllocator.IdAllocatorV2, | 142 | idAllocator: yield* IdAllocator.IdAllocatorV2, |
| 141 | queryRunner: { | 143 | queryRunner: { |
| 142 | allocateSessionId: Effect.succeed(sessionId), | 144 | allocateSessionId: Effect.succeed(sessionId), |
apps/server/src/orchestration-v2/ProviderTurnControlService.ts+17-10| ... | @@ -7,12 +7,12 @@ import { | ... | @@ -7,12 +7,12 @@ import { |
| 7 | RunAttemptId, | 7 | RunAttemptId, |
| 8 | ThreadId, | 8 | ThreadId, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import * as Clock from "effect/Clock"; | ||
| 10 | import * as Context from "effect/Context"; | 11 | import * as Context from "effect/Context"; |
| 11 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| 12 | import * as Layer from "effect/Layer"; | 13 | import * as Layer from "effect/Layer"; |
| 13 | import * as Option from "effect/Option"; | 14 | import * as Option from "effect/Option"; |
| 14 | import * as Schema from "effect/Schema"; | 15 | import * as Schema from "effect/Schema"; |
| 15 | import * as NodeTimersPromises from "node:timers/promises"; | ||
| 16 | 16 | ||
| 17 | import * as ProjectionStore from "./ProjectionStore.ts"; | 17 | import * as ProjectionStore from "./ProjectionStore.ts"; |
| 18 | import * as ProviderSessionManager from "./ProviderSessionManager.ts"; | 18 | import * as ProviderSessionManager from "./ProviderSessionManager.ts"; |
| ... | @@ -188,17 +188,24 @@ export const layer: Layer.Layer< | ... | @@ -188,17 +188,24 @@ export const layer: Layer.Layer< |
| 188 | }); | 188 | }); |
| 189 | // Give native terminal ingestion time to finish before the Stop | 189 | // Give native terminal ingestion time to finish before the Stop |
| 190 | // follow-up repairs a run whose provider no longer reports on it. | 190 | // follow-up repairs a run whose provider no longer reports on it. |
| 191 | const deadline = performance.now() + 2_000; | 191 | // The wait is real time: ingestion runs on other fibers and never |
| 192 | while (loaded.providerTurn.status === "running" && performance.now() < deadline) { | 192 | // advances a test clock, so a test clock would hold Stop forever. |
| 193 | const current = yield* projections.getProviderControlContext(input.threadId, input); | 193 | yield* Effect.gen(function* () { |
| 194 | if ( | 194 | const deadline = (yield* Clock.currentTimeMillis) + 2_000; |
| 195 | current.providerTurn?.status !== "running" && | 195 | while ( |
| 196 | current.attempt?.status !== "running" | 196 | loaded.providerTurn.status === "running" && |
| 197 | (yield* Clock.currentTimeMillis) < deadline | ||
| 197 | ) { | 198 | ) { |
| 198 | break; | 199 | const current = yield* projections.getProviderControlContext(input.threadId, input); |
| 200 | if ( | ||
| 201 | current.providerTurn?.status !== "running" && | ||
| 202 | current.attempt?.status !== "running" | ||
| 203 | ) { | ||
| 204 | return; | ||
| 205 | } | ||
| 206 | yield* Effect.sleep("10 millis"); | ||
| 199 | } | 207 | } |
| 200 | yield* Effect.promise(() => NodeTimersPromises.setTimeout(10)); | 208 | }).pipe(Effect.provideService(Clock.Clock, Clock.Clock.defaultValue())); |
| 201 | } | ||
| 202 | }).pipe( | 209 | }).pipe( |
| 203 | Effect.mapError((cause) => | 210 | Effect.mapError((cause) => |
| 204 | isProviderTurnControlError(cause) | 211 | isProviderTurnControlError(cause) |
apps/server/src/preview/PreviewBrowser.test.ts+6-2| ... | @@ -2,9 +2,11 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; | ... | @@ -2,9 +2,11 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { expect, it } from "@effect/vitest"; | 2 | import { expect, it } from "@effect/vitest"; |
| 3 | import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 3 | import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 4 | import * as Deferred from "effect/Deferred"; | 4 | import * as Deferred from "effect/Deferred"; |
| 5 | import * as Crypto from "effect/Crypto"; | ||
| 5 | import type * as Duration from "effect/Duration"; | 6 | import type * as Duration from "effect/Duration"; |
| 6 | import * as DateTime from "effect/DateTime"; | 7 | import * as DateTime from "effect/DateTime"; |
| 7 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 9 | import * as Hex from "effect/encoding/Hex"; | ||
| 8 | import * as Fiber from "effect/Fiber"; | 10 | import * as Fiber from "effect/Fiber"; |
| 9 | import * as FileSystem from "effect/FileSystem"; | 11 | import * as FileSystem from "effect/FileSystem"; |
| 10 | import * as Option from "effect/Option"; | 12 | import * as Option from "effect/Option"; |
| ... | @@ -12,7 +14,6 @@ import * as Path from "effect/Path"; | ... | @@ -12,7 +14,6 @@ import * as Path from "effect/Path"; |
| 12 | import * as Stream from "effect/Stream"; | 14 | import * as Stream from "effect/Stream"; |
| 13 | import * as TestClock from "effect/testing/TestClock"; | 15 | import * as TestClock from "effect/testing/TestClock"; |
| 14 | import { HttpClient, HttpClientResponse } from "effect/http"; | 16 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 15 | import * as NodeCrypto from "node:crypto"; | ||
| 16 | import * as NodeZlib from "node:zlib"; | 17 | import * as NodeZlib from "node:zlib"; |
| 17 | 18 | ||
| 18 | import * as PreviewBrowser from "./PreviewBrowser.ts"; | 19 | import * as PreviewBrowser from "./PreviewBrowser.ts"; |
| ... | @@ -87,6 +88,9 @@ const makeHarness = Effect.fn("test.makePreviewBrowser")(function* ( | ... | @@ -87,6 +88,9 @@ const makeHarness = Effect.fn("test.makePreviewBrowser")(function* ( |
| 87 | const path = yield* Path.Path; | 88 | const path = yield* Path.Path; |
| 88 | const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-preview-browser-test-" }); | 89 | const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-preview-browser-test-" }); |
| 89 | const archive = options.archive ?? browserArchive; | 90 | const archive = options.archive ?? browserArchive; |
| 91 | const crypto = yield* Crypto.Crypto; | ||
| 92 | const sha256 = | ||
| 93 | options.sha256 ?? Hex.encode(yield* crypto.digest("SHA-256", archive).pipe(Effect.orDie)); | ||
| 90 | const requests: Array<string> = []; | 94 | const requests: Array<string> = []; |
| 91 | const browser = yield* PreviewBrowser.makePreviewBrowser({ | 95 | const browser = yield* PreviewBrowser.makePreviewBrowser({ |
| 92 | baseDir, | 96 | baseDir, |
| ... | @@ -97,7 +101,7 @@ const makeHarness = Effect.fn("test.makePreviewBrowser")(function* ( | ... | @@ -97,7 +101,7 @@ const makeHarness = Effect.fn("test.makePreviewBrowser")(function* ( |
| 97 | platform: "fixture", | 101 | platform: "fixture", |
| 98 | url: "https://storage.googleapis.com/chrome-headless-shell-fixture.zip", | 102 | url: "https://storage.googleapis.com/chrome-headless-shell-fixture.zip", |
| 99 | bytes: archive.byteLength, | 103 | bytes: archive.byteLength, |
| 100 | sha256: options.sha256 ?? NodeCrypto.createHash("sha256").update(archive).digest("hex"), | 104 | sha256, |
| 101 | }, | 105 | }, |
| 102 | ...(options.wait === undefined ? {} : { wait: options.wait }), | 106 | ...(options.wait === undefined ? {} : { wait: options.wait }), |
| 103 | }).pipe( | 107 | }).pipe( |
apps/server/src/provider/AntigravityInstallation.test.ts+20-13| ... | @@ -6,8 +6,10 @@ import { | ... | @@ -6,8 +6,10 @@ import { |
| 6 | HostProcessIsExecutable, | 6 | HostProcessIsExecutable, |
| 7 | HostProcessPlatform, | 7 | HostProcessPlatform, |
| 8 | } from "@t3tools/shared/hostProcess"; | 8 | } from "@t3tools/shared/hostProcess"; |
| 9 | import * as Crypto from "effect/Crypto"; | ||
| 9 | import * as Deferred from "effect/Deferred"; | 10 | import * as Deferred from "effect/Deferred"; |
| 10 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as Hex from "effect/encoding/Hex"; | ||
| 11 | import * as Exit from "effect/Exit"; | 13 | import * as Exit from "effect/Exit"; |
| 12 | import * as Fiber from "effect/Fiber"; | 14 | import * as Fiber from "effect/Fiber"; |
| 13 | import * as FileSystem from "effect/FileSystem"; | 15 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -21,7 +23,6 @@ import * as Sink from "effect/Sink"; | ... | @@ -21,7 +23,6 @@ import * as Sink from "effect/Sink"; |
| 21 | import * as Stream from "effect/Stream"; | 23 | import * as Stream from "effect/Stream"; |
| 22 | import { HttpClient, HttpClientResponse } from "effect/http"; | 24 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 23 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; | 25 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 24 | import * as NodeCrypto from "node:crypto"; | ||
| 25 | 26 | ||
| 26 | import * as AntigravityInstallation from "./AntigravityInstallation.ts"; | 27 | import * as AntigravityInstallation from "./AntigravityInstallation.ts"; |
| 27 | import { ANTIGRAVITY_AUTH_BROWSER_MARKER } from "./antigravityAuthSupport.ts"; | 28 | import { ANTIGRAVITY_AUTH_BROWSER_MARKER } from "./antigravityAuthSupport.ts"; |
| ... | @@ -70,14 +71,15 @@ const executableName = hostPlatform === "win32" ? "agy_acp_server.exe" : "agy_ac | ... | @@ -70,14 +71,15 @@ const executableName = hostPlatform === "win32" ? "agy_acp_server.exe" : "agy_ac |
| 70 | const harnessName = | 71 | const harnessName = |
| 71 | hostPlatform === "win32" ? "localharness_external.exe" : "localharness_external"; | 72 | hostPlatform === "win32" ? "localharness_external.exe" : "localharness_external"; |
| 72 | 73 | ||
| 73 | function releaseAsset( | 74 | const releaseAsset = Effect.fn("test.antigravityReleaseAsset")(function* ( |
| 74 | archive: Uint8Array = completeArchive, | 75 | archive: Uint8Array = completeArchive, |
| 75 | platform: NodeJS.Platform = hostPlatform, | 76 | platform: NodeJS.Platform = hostPlatform, |
| 76 | ) { | 77 | ) { |
| 78 | const crypto = yield* Crypto.Crypto; | ||
| 77 | return { | 79 | return { |
| 78 | version: "fixture-new", | 80 | version: "fixture-new", |
| 79 | url: "https://dl.google.com/antigravity-test.zip", | 81 | url: "https://dl.google.com/antigravity-test.zip", |
| 80 | sha256: NodeCrypto.createHash("sha256").update(archive).digest("hex"), | 82 | sha256: Hex.encode(yield* crypto.digest("SHA-256", archive).pipe(Effect.orDie)), |
| 81 | archiveBytes: archive.byteLength, | 83 | archiveBytes: archive.byteLength, |
| 82 | executable: { | 84 | executable: { |
| 83 | name: platform === "win32" ? "agy_acp_server.exe" : "agy_acp_server.par", | 85 | name: platform === "win32" ? "agy_acp_server.exe" : "agy_acp_server.par", |
| ... | @@ -88,7 +90,7 @@ function releaseAsset( | ... | @@ -88,7 +90,7 @@ function releaseAsset( |
| 88 | bytes: Buffer.byteLength(harnessContents), | 90 | bytes: Buffer.byteLength(harnessContents), |
| 89 | }, | 91 | }, |
| 90 | } satisfies AntigravityReleaseAsset; | 92 | } satisfies AntigravityReleaseAsset; |
| 91 | } | 93 | }); |
| 92 | 94 | ||
| 93 | const writeRelease = Effect.fn("test.writeAntigravityRelease")(function* ( | 95 | const writeRelease = Effect.fn("test.writeAntigravityRelease")(function* ( |
| 94 | managedDirectory: string, | 96 | managedDirectory: string, |
| ... | @@ -146,11 +148,12 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( | ... | @@ -146,11 +148,12 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( |
| 146 | options.baseDir ?? (yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-test-" })); | 148 | options.baseDir ?? (yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-test-" })); |
| 147 | const platform = options.platform ?? hostPlatform; | 149 | const platform = options.platform ?? hostPlatform; |
| 148 | const archive = options.archive ?? completeArchive; | 150 | const archive = options.archive ?? completeArchive; |
| 149 | const asset = options.asset === undefined ? releaseAsset(archive, platform) : options.asset; | 151 | const asset = |
| 152 | options.asset === undefined ? yield* releaseAsset(archive, platform) : options.asset; | ||
| 150 | const managedDirectory = path.join(baseDir, "tools", "antigravity-acp", `${platform}-x64`); | 153 | const managedDirectory = path.join(baseDir, "tools", "antigravity-acp", `${platform}-x64`); |
| 151 | if (options.previous) { | 154 | if (options.previous) { |
| 152 | yield* writeRelease(managedDirectory, { | 155 | yield* writeRelease(managedDirectory, { |
| 153 | ...releaseAsset(archive, platform), | 156 | ...(yield* releaseAsset(archive, platform)), |
| 154 | sha256: previousReleaseId, | 157 | sha256: previousReleaseId, |
| 155 | version: previousVersion, | 158 | version: previousVersion, |
| 156 | }); | 159 | }); |
| ... | @@ -301,9 +304,9 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { | ... | @@ -301,9 +304,9 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { |
| 301 | expect(yield* fs.readFileString(selected.executablePath)).toBe(serverContents); | 304 | expect(yield* fs.readFileString(selected.executablePath)).toBe(serverContents); |
| 302 | expect(yield* fs.readFileString(selected.harnessPath)).toBe(harnessContents); | 305 | expect(yield* fs.readFileString(selected.harnessPath)).toBe(harnessContents); |
| 303 | expect(yield* fs.readDirectory(path.join(installation.managedDirectory, "versions"))).toEqual( | 306 | expect(yield* fs.readDirectory(path.join(installation.managedDirectory, "versions"))).toEqual( |
| 304 | expect.arrayContaining([previousReleaseId, releaseAsset().sha256]), | 307 | expect.arrayContaining([previousReleaseId, (yield* releaseAsset()).sha256]), |
| 305 | ); | 308 | ); |
| 306 | expect(requests).toEqual([releaseAsset().url]); | 309 | expect(requests).toEqual([(yield* releaseAsset()).url]); |
| 307 | }), | 310 | }), |
| 308 | ); | 311 | ); |
| 309 | 312 | ||
| ... | @@ -469,12 +472,12 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { | ... | @@ -469,12 +472,12 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { |
| 469 | ); | 472 | ); |
| 470 | 473 | ||
| 471 | it.effect.each([ | 474 | it.effect.each([ |
| 472 | { name: "checksum mismatch", asset: { ...releaseAsset(), sha256: "2".repeat(64) } }, | 475 | { name: "checksum mismatch", completeAsset: { sha256: "2".repeat(64) } }, |
| 473 | { name: "short download", archive: completeArchive.subarray(0, -1), asset: releaseAsset() }, | 476 | { name: "short download", archive: completeArchive.subarray(0, -1), completeAsset: {} }, |
| 474 | { | 477 | { |
| 475 | name: "oversized download", | 478 | name: "oversized download", |
| 476 | archive: Buffer.concat([completeArchive, Buffer.from("extra")]), | 479 | archive: Buffer.concat([completeArchive, Buffer.from("extra")]), |
| 477 | asset: releaseAsset(), | 480 | completeAsset: {}, |
| 478 | }, | 481 | }, |
| 479 | { name: "wrong Content-Length", contentLength: completeArchive.byteLength + 1 }, | 482 | { name: "wrong Content-Length", contentLength: completeArchive.byteLength + 1 }, |
| 480 | { name: "missing harness", archive: Buffer.from(zipFixtures.missingHarness, "base64") }, | 483 | { name: "missing harness", archive: Buffer.from(zipFixtures.missingHarness, "base64") }, |
| ... | @@ -482,10 +485,14 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { | ... | @@ -482,10 +485,14 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { |
| 482 | { name: "path traversal", archive: Buffer.from(zipFixtures.traversal, "base64") }, | 485 | { name: "path traversal", archive: Buffer.from(zipFixtures.traversal, "base64") }, |
| 483 | { name: "symbolic link", archive: Buffer.from(zipFixtures.symlink, "base64") }, | 486 | { name: "symbolic link", archive: Buffer.from(zipFixtures.symlink, "base64") }, |
| 484 | { name: "oversized member", archive: Buffer.from(zipFixtures.oversizedMember, "base64") }, | 487 | { name: "oversized member", archive: Buffer.from(zipFixtures.oversizedMember, "base64") }, |
| 485 | ])("rejects $name before runtime validation", (options) => | 488 | ])("rejects $name before runtime validation", ({ completeAsset, ...options }) => |
| 486 | Effect.gen(function* () { | 489 | Effect.gen(function* () { |
| 490 | // Pin the asset to the complete archive so the download itself is what disagrees. | ||
| 491 | const asset = | ||
| 492 | completeAsset === undefined ? undefined : { ...(yield* releaseAsset()), ...completeAsset }; | ||
| 487 | const { installation, validations, stagingReleased, fs, path } = yield* makeHarness({ | 493 | const { installation, validations, stagingReleased, fs, path } = yield* makeHarness({ |
| 488 | ...options, | 494 | ...options, |
| 495 | ...(asset === undefined ? {} : { asset }), | ||
| 489 | previous: true, | 496 | previous: true, |
| 490 | }); | 497 | }); |
| 491 | yield* installation.start; | 498 | yield* installation.start; |
| ... | @@ -893,7 +900,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { | ... | @@ -893,7 +900,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { |
| 893 | const fs = yield* FileSystem.FileSystem; | 900 | const fs = yield* FileSystem.FileSystem; |
| 894 | const path = yield* Path.Path; | 901 | const path = yield* Path.Path; |
| 895 | const archive = Buffer.from(zipFixtures.windows, "base64"); | 902 | const archive = Buffer.from(zipFixtures.windows, "base64"); |
| 896 | const asset = releaseAsset(archive, "win32"); | 903 | const asset = yield* releaseAsset(archive, "win32"); |
| 897 | let denyPointerRename = true; | 904 | let denyPointerRename = true; |
| 898 | const renameTargets: string[] = []; | 905 | const renameTargets: string[] = []; |
| 899 | const { installation, requests, validations } = yield* makeHarness({ | 906 | const { installation, requests, validations } = yield* makeHarness({ |
apps/server/src/provider/CodexInstallation.test.ts+5-3| ... | @@ -6,15 +6,16 @@ import { | ... | @@ -6,15 +6,16 @@ import { |
| 6 | HostProcessEnvironment, | 6 | HostProcessEnvironment, |
| 7 | HostProcessPlatform, | 7 | HostProcessPlatform, |
| 8 | } from "@t3tools/shared/hostProcess"; | 8 | } from "@t3tools/shared/hostProcess"; |
| 9 | import * as Crypto from "effect/Crypto"; | ||
| 9 | import * as Deferred from "effect/Deferred"; | 10 | import * as Deferred from "effect/Deferred"; |
| 10 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as Hex from "effect/encoding/Hex"; | ||
| 11 | import * as FileSystem from "effect/FileSystem"; | 13 | import * as FileSystem from "effect/FileSystem"; |
| 12 | import * as Option from "effect/Option"; | 14 | import * as Option from "effect/Option"; |
| 13 | import * as Exit from "effect/Exit"; | 15 | import * as Exit from "effect/Exit"; |
| 14 | import * as Scope from "effect/Scope"; | 16 | import * as Scope from "effect/Scope"; |
| 15 | import * as Stream from "effect/Stream"; | 17 | import * as Stream from "effect/Stream"; |
| 16 | import { HttpClient, HttpClientResponse } from "effect/http"; | 18 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 17 | import * as NodeCrypto from "node:crypto"; | ||
| 18 | import * as CodexInstallation from "./CodexInstallation.ts"; | 19 | import * as CodexInstallation from "./CodexInstallation.ts"; |
| 19 | 20 | ||
| 20 | const archive = Buffer.from( | 21 | const archive = Buffer.from( |
| ... | @@ -25,7 +26,6 @@ const asset = { | ... | @@ -25,7 +26,6 @@ const asset = { |
| 25 | version: "0.156.1", | 26 | version: "0.156.1", |
| 26 | target: "aarch64-apple-darwin", | 27 | target: "aarch64-apple-darwin", |
| 27 | url: "https://github.com/openai/codex/releases/download/test/package.tar.gz", | 28 | url: "https://github.com/openai/codex/releases/download/test/package.tar.gz", |
| 28 | sha256: NodeCrypto.createHash("sha256").update(archive).digest("hex"), | ||
| 29 | archiveBytes: archive.length, | 29 | archiveBytes: archive.length, |
| 30 | }; | 30 | }; |
| 31 | const makeHarness = Effect.fn("test.makeCodexInstallation")(function* ( | 31 | const makeHarness = Effect.fn("test.makeCodexInstallation")(function* ( |
| ... | @@ -52,9 +52,11 @@ const makeHarness = Effect.fn("test.makeCodexInstallation")(function* ( | ... | @@ -52,9 +52,11 @@ const makeHarness = Effect.fn("test.makeCodexInstallation")(function* ( |
| 52 | ); | 52 | ); |
| 53 | } | 53 | } |
| 54 | let downloads = 0; | 54 | let downloads = 0; |
| 55 | const crypto = yield* Crypto.Crypto; | ||
| 56 | const sha256 = Hex.encode(yield* crypto.digest("SHA-256", archive).pipe(Effect.orDie)); | ||
| 55 | const installation = yield* CodexInstallation.makeCodexInstallation({ | 57 | const installation = yield* CodexInstallation.makeCodexInstallation({ |
| 56 | baseDir, | 58 | baseDir, |
| 57 | releaseAsset: asset, | 59 | releaseAsset: { ...asset, sha256 }, |
| 58 | validate: () => Effect.void, | 60 | validate: () => Effect.void, |
| 59 | ...input.options, | 61 | ...input.options, |
| 60 | }).pipe( | 62 | }).pipe( |
apps/server/src/provider/CodexToolPresentation.ts+114-105| ... | @@ -1,9 +1,11 @@ | ... | @@ -1,9 +1,11 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import type { | 1 | import type { |
| 3 | ToolActivityIcon, | 2 | ToolActivityIcon, |
| 4 | ToolActivityNativeAppReference, | 3 | ToolActivityNativeAppReference, |
| 5 | ToolActivitySource, | 4 | ToolActivitySource, |
| 6 | } from "@t3tools/contracts"; | 5 | } from "@t3tools/contracts"; |
| 6 | import * as Crypto from "effect/Crypto"; | ||
| 7 | import * as Effect from "effect/Effect"; | ||
| 8 | import * as Hex from "effect/encoding/Hex"; | ||
| 7 | import type * as EffectCodexSchema from "effect-codex-app-server/schema"; | 9 | import type * as EffectCodexSchema from "effect-codex-app-server/schema"; |
| 8 | 10 | ||
| 9 | import { | 11 | import { |
| ... | @@ -42,12 +44,17 @@ function normalizedSourceKeyPart(value: string): string { | ... | @@ -42,12 +44,17 @@ function normalizedSourceKeyPart(value: string): string { |
| 42 | return value.trim().toLowerCase(); | 44 | return value.trim().toLowerCase(); |
| 43 | } | 45 | } |
| 44 | 46 | ||
| 45 | function nativeAppSourceKey(appId: string): string { | 47 | const nativeAppSourceKey = Effect.fn("CodexToolPresentation.nativeAppSourceKey")(function* ( |
| 48 | appId: string, | ||
| 49 | ) { | ||
| 46 | const key = `native-app:${appId.toLowerCase()}`; | 50 | const key = `native-app:${appId.toLowerCase()}`; |
| 47 | if (key.length <= 512) return key; | 51 | if (key.length <= 512) return key; |
| 48 | const digest = NodeCrypto.createHash("sha256").update(key).digest("hex"); | 52 | const crypto = yield* Crypto.Crypto; |
| 53 | const digest = Hex.encode( | ||
| 54 | yield* crypto.digest("SHA-256", new TextEncoder().encode(key)).pipe(Effect.orDie), | ||
| 55 | ); | ||
| 49 | return `${key.slice(0, 512 - digest.length - 1)}:${digest}`; | 56 | return `${key.slice(0, 512 - digest.length - 1)}:${digest}`; |
| 50 | } | 57 | }); |
| 51 | 58 | ||
| 52 | function browserDisplayName(value: unknown): string | undefined { | 59 | function browserDisplayName(value: unknown): string | undefined { |
| 53 | const normalized = normalizedDisplayName(value)?.toLowerCase(); | 60 | const normalized = normalizedDisplayName(value)?.toLowerCase(); |
| ... | @@ -123,105 +130,107 @@ export interface McpToolPresentation { | ... | @@ -123,105 +130,107 @@ export interface McpToolPresentation { |
| 123 | readonly toolSource?: ToolActivitySource; | 130 | readonly toolSource?: ToolActivitySource; |
| 124 | } | 131 | } |
| 125 | 132 | ||
| 126 | export function mcpToolPresentation( | 133 | export const mcpToolPresentation = Effect.fn("CodexToolPresentation.mcpToolPresentation")( |
| 127 | item: Extract<CodexLifecycleItem, { readonly type: "mcpToolCall" }>, | 134 | function* ( |
| 128 | ): McpToolPresentation { | 135 | item: Extract<CodexLifecycleItem, { readonly type: "mcpToolCall" }>, |
| 129 | const result = asUnknownRecord(item.result); | 136 | ): Effect.fn.Return<McpToolPresentation, never, Crypto.Crypto> { |
| 130 | const metadata = asUnknownRecord(result?._meta); | 137 | const result = asUnknownRecord(item.result); |
| 131 | const surface = asUnknownRecord(metadata?.["codex/toolSurface"]); | 138 | const metadata = asUnknownRecord(result?._meta); |
| 132 | const sourceMetadata = asUnknownRecord(metadata?.source); | 139 | const surface = asUnknownRecord(metadata?.["codex/toolSurface"]); |
| 133 | const appContext = asUnknownRecord(item.appContext); | 140 | const sourceMetadata = asUnknownRecord(metadata?.source); |
| 134 | const sourceLogo = themedLogoIcon(surface, sourceMetadata, appContext); | 141 | const appContext = asUnknownRecord(item.appContext); |
| 135 | if (surface?.kind === "browserUse") { | 142 | const sourceLogo = themedLogoIcon(surface, sourceMetadata, appContext); |
| 136 | const screenshot = asUnknownRecord(surface.screenshot); | 143 | if (surface?.kind === "browserUse") { |
| 137 | const browserUse = asUnknownRecord(metadata?.browser_use); | 144 | const screenshot = asUnknownRecord(surface.screenshot); |
| 138 | const openTabs = Array.isArray(surface.openTabs) ? surface.openTabs : []; | 145 | const browserUse = asUnknownRecord(metadata?.browser_use); |
| 139 | const latestOpenTab = openTabs | 146 | const openTabs = Array.isArray(surface.openTabs) ? surface.openTabs : []; |
| 140 | .toReversed() | 147 | const latestOpenTab = openTabs |
| 141 | .map(asUnknownRecord) | 148 | .toReversed() |
| 142 | .find((tab) => normalizedHttpUrl(tab?.url) !== undefined); | 149 | .map(asUnknownRecord) |
| 143 | const selectedPage = [ | 150 | .find((tab) => normalizedHttpUrl(tab?.url) !== undefined); |
| 144 | { record: screenshot, url: screenshot?.pageUrl }, | 151 | const selectedPage = [ |
| 145 | { record: browserUse, url: browserUse?.url }, | 152 | { record: screenshot, url: screenshot?.pageUrl }, |
| 146 | { record: latestOpenTab, url: latestOpenTab?.url }, | 153 | { record: browserUse, url: browserUse?.url }, |
| 147 | ] | 154 | { record: latestOpenTab, url: latestOpenTab?.url }, |
| 148 | .map((candidate) => ({ ...candidate, pageUrl: normalizedHttpUrl(candidate.url) })) | 155 | ] |
| 149 | .find((candidate) => candidate.pageUrl !== undefined); | 156 | .map((candidate) => ({ ...candidate, pageUrl: normalizedHttpUrl(candidate.url) })) |
| 150 | const pageUrl = selectedPage?.pageUrl; | 157 | .find((candidate) => candidate.pageUrl !== undefined); |
| 151 | const faviconUrl = normalizedImageUrl( | 158 | const pageUrl = selectedPage?.pageUrl; |
| 152 | selectedPage?.record?.faviconUrl ?? selectedPage?.record?.favIconUrl, | 159 | const faviconUrl = normalizedImageUrl( |
| 153 | ); | 160 | selectedPage?.record?.faviconUrl ?? selectedPage?.record?.favIconUrl, |
| 154 | const faviconUrlDark = normalizedImageUrl( | 161 | ); |
| 155 | selectedPage?.record?.faviconUrlDark ?? selectedPage?.record?.favIconUrlDark, | 162 | const faviconUrlDark = normalizedImageUrl( |
| 156 | ); | 163 | selectedPage?.record?.faviconUrlDark ?? selectedPage?.record?.favIconUrlDark, |
| 157 | const name = | 164 | ); |
| 158 | browserDisplayName(appContext?.appName) ?? | 165 | const name = |
| 159 | browserDisplayName(surface.browserFamily) ?? | 166 | browserDisplayName(appContext?.appName) ?? |
| 160 | browserDisplayName(surface.backend) ?? | 167 | browserDisplayName(surface.browserFamily) ?? |
| 161 | "Browser"; | 168 | browserDisplayName(surface.backend) ?? |
| 162 | const nativeBrowserIcon = browserNativeAppReference(name); | 169 | "Browser"; |
| 163 | const sourceIcon = | 170 | const nativeBrowserIcon = browserNativeAppReference(name); |
| 164 | sourceLogo ?? | 171 | const sourceIcon = |
| 165 | (nativeBrowserIcon ? ({ _tag: "native-app", app: nativeBrowserIcon } as const) : undefined); | 172 | sourceLogo ?? |
| 166 | const sourceKeyPart = normalizedSourceKeyPart(name) || "browser"; | 173 | (nativeBrowserIcon ? ({ _tag: "native-app", app: nativeBrowserIcon } as const) : undefined); |
| 167 | return { | 174 | const sourceKeyPart = normalizedSourceKeyPart(name) || "browser"; |
| 168 | toolSurface: "browser", | 175 | return { |
| 169 | ...(pageUrl | 176 | toolSurface: "browser", |
| 170 | ? { | 177 | ...(pageUrl |
| 171 | toolIcon: { | 178 | ? { |
| 172 | _tag: "website", | 179 | toolIcon: { |
| 173 | pageUrl, | 180 | _tag: "website", |
| 174 | ...(faviconUrl ? { faviconUrl } : {}), | 181 | pageUrl, |
| 175 | ...(faviconUrlDark ? { faviconUrlDark } : {}), | 182 | ...(faviconUrl ? { faviconUrl } : {}), |
| 176 | } as const, | 183 | ...(faviconUrlDark ? { faviconUrlDark } : {}), |
| 177 | } | 184 | } as const, |
| 178 | : {}), | 185 | } |
| 179 | toolSource: { | 186 | : {}), |
| 180 | key: `browser-use:${sourceKeyPart}`, | 187 | toolSource: { |
| 181 | name, | 188 | key: `browser-use:${sourceKeyPart}`, |
| 182 | kind: name === "Browser" ? "browser" : "integration", | 189 | name, |
| 183 | ...(sourceIcon ? { icon: sourceIcon } : {}), | 190 | kind: name === "Browser" ? "browser" : "integration", |
| 184 | }, | 191 | ...(sourceIcon ? { icon: sourceIcon } : {}), |
| 185 | }; | 192 | }, |
| 186 | } | 193 | }; |
| 187 | if (surface?.kind === "computerUse") { | 194 | } |
| 188 | const app = nativeAppReference(surface.app); | 195 | if (surface?.kind === "computerUse") { |
| 189 | const args = asUnknownRecord(item.arguments); | 196 | const app = nativeAppReference(surface.app); |
| 190 | const argumentAppName = | 197 | const args = asUnknownRecord(item.arguments); |
| 191 | normalizedDisplayName(args?.appName) ?? | 198 | const argumentAppName = |
| 192 | normalizedDisplayName(args?.application) ?? | 199 | normalizedDisplayName(args?.appName) ?? |
| 193 | normalizedDisplayName(typeof args?.app === "string" ? args.app : undefined); | 200 | normalizedDisplayName(args?.application) ?? |
| 194 | const name = | 201 | normalizedDisplayName(typeof args?.app === "string" ? args.app : undefined); |
| 195 | normalizedDisplayName(appContext?.appName) ?? | 202 | const name = |
| 196 | argumentAppName ?? | 203 | normalizedDisplayName(appContext?.appName) ?? |
| 197 | (app?._tag === "display-name" ? app.displayName : undefined) ?? | 204 | argumentAppName ?? |
| 198 | (app?._tag === "app-id" ? appDisplayNameFromId(app.appId) : undefined) ?? | 205 | (app?._tag === "display-name" ? app.displayName : undefined) ?? |
| 199 | "Computer Use"; | 206 | (app?._tag === "app-id" ? appDisplayNameFromId(app.appId) : undefined) ?? |
| 200 | const sourceIcon = sourceLogo ?? (app ? ({ _tag: "native-app", app } as const) : undefined); | 207 | "Computer Use"; |
| 201 | const sourceKey = app | 208 | const sourceIcon = sourceLogo ?? (app ? ({ _tag: "native-app", app } as const) : undefined); |
| 202 | ? app._tag === "app-id" | 209 | const sourceKey = app |
| 203 | ? nativeAppSourceKey(app.appId) | 210 | ? app._tag === "app-id" |
| 204 | : `native-app-name:${normalizedSourceKeyPart(app.displayName)}` | 211 | ? yield* nativeAppSourceKey(app.appId) |
| 205 | : "computer-use"; | 212 | : `native-app-name:${normalizedSourceKeyPart(app.displayName)}` |
| 206 | return { | 213 | : "computer-use"; |
| 207 | toolSurface: "computer", | 214 | return { |
| 208 | ...(app ? { toolIcon: { _tag: "native-app", app } as const } : {}), | 215 | toolSurface: "computer", |
| 209 | toolSource: { | 216 | ...(app ? { toolIcon: { _tag: "native-app", app } as const } : {}), |
| 210 | key: sourceKey, | 217 | toolSource: { |
| 211 | name, | 218 | key: sourceKey, |
| 212 | kind: "computer", | 219 | name, |
| 213 | ...(sourceIcon ? { icon: sourceIcon } : {}), | 220 | kind: "computer", |
| 214 | }, | 221 | ...(sourceIcon ? { icon: sourceIcon } : {}), |
| 215 | }; | 222 | }, |
| 216 | } | 223 | }; |
| 224 | } | ||
| 217 | 225 | ||
| 218 | return integrationToolPresentation({ | 226 | return integrationToolPresentation({ |
| 219 | serverName: appContext?.connectorId ?? item.server, | 227 | serverName: appContext?.connectorId ?? item.server, |
| 220 | toolName: item.tool, | 228 | toolName: item.tool, |
| 221 | title: appContext?.actionName, | 229 | title: appContext?.actionName, |
| 222 | serverDisplayName: appContext?.appName, | 230 | serverDisplayName: appContext?.appName, |
| 223 | source: sourceMetadata, | 231 | source: sourceMetadata, |
| 224 | iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined, | 232 | iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined, |
| 225 | iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined, | 233 | iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined, |
| 226 | }); | 234 | }); |
| 227 | } | 235 | }, |
| 236 | ); |
apps/server/src/provider/OpenCodeProvider.test.ts+2-2| ... | @@ -1,5 +1,4 @@ | ... | @@ -1,5 +1,4 @@ |
| 1 | import * as NodeAssert from "node:assert/strict"; | 1 | import * as NodeAssert from "node:assert/strict"; |
| 2 | import * as NodeCrypto from "node:crypto"; | ||
| 3 | 2 | ||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 5 | import { it } from "@effect/vitest"; | 4 | import { it } from "@effect/vitest"; |
| ... | @@ -81,7 +80,8 @@ it.effect("reads Go limits with the instance's XDG credentials and preserves res | ... | @@ -81,7 +80,8 @@ it.effect("reads Go limits with the instance's XDG credentials and preserves res |
| 81 | NodeAssert.equal(limits.unavailable, undefined); | 80 | NodeAssert.equal(limits.unavailable, undefined); |
| 82 | NodeAssert.equal( | 81 | NodeAssert.equal( |
| 83 | limits.credentialFingerprint, | 82 | limits.credentialFingerprint, |
| 84 | NodeCrypto.createHash("sha256").update("opencode-go\0instance-key").digest("hex"), | 83 | // SHA-256 of "opencode-go\0instance-key". |
| 84 | "aba48e85c981a8edc1c9fb4575121accc7235fa55d0f8689f66f153de5566a37", | ||
| 85 | ); | 85 | ); |
| 86 | NodeAssert.deepEqual( | 86 | NodeAssert.deepEqual( |
| 87 | limits.windows.map(({ kind, usedPercent, resetsAt: reset }) => ({ | 87 | limits.windows.map(({ kind, usedPercent, resetsAt: reset }) => ({ |
apps/server/src/provider/acp/AcpRegistryAuthenticationState.ts+11-5| ... | @@ -1,10 +1,11 @@ | ... | @@ -1,10 +1,11 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import type { | 1 | import type { |
| 3 | AcpRegistrySettings, | 2 | AcpRegistrySettings, |
| 4 | ProviderInstanceEnvironment, | 3 | ProviderInstanceEnvironment, |
| 5 | ProviderInstanceId, | 4 | ProviderInstanceId, |
| 6 | } from "@t3tools/contracts"; | 5 | } from "@t3tools/contracts"; |
| 6 | import * as Crypto from "effect/Crypto"; | ||
| 7 | import * as Effect from "effect/Effect"; | 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Hex from "effect/encoding/Hex"; | ||
| 8 | import * as FileSystem from "effect/FileSystem"; | 9 | import * as FileSystem from "effect/FileSystem"; |
| 9 | import * as Path from "effect/Path"; | 10 | import * as Path from "effect/Path"; |
| 10 | import * as Ref from "effect/Ref"; | 11 | import * as Ref from "effect/Ref"; |
| ... | @@ -16,8 +17,13 @@ import { writeFileStringAtomically } from "../../atomicWrite.ts"; | ... | @@ -16,8 +17,13 @@ import { writeFileStringAtomically } from "../../atomicWrite.ts"; |
| 16 | const decodeState = Schema.decodeUnknownEffect( | 17 | const decodeState = Schema.decodeUnknownEffect( |
| 17 | Schema.fromJsonString(Schema.Struct({ binding: Schema.String, authenticated: Schema.Boolean })), | 18 | Schema.fromJsonString(Schema.Struct({ binding: Schema.String, authenticated: Schema.Boolean })), |
| 18 | ); | 19 | ); |
| 19 | const hash = (value: unknown) => | 20 | const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); |
| 20 | NodeCrypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); | 21 | const hash = Effect.fn("AcpRegistryAuthenticationState.hash")(function* (value: unknown) { |
| 22 | const crypto = yield* Crypto.Crypto; | ||
| 23 | const json = yield* encodeJson(value).pipe(Effect.orDie); | ||
| 24 | const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(json)).pipe(Effect.orDie); | ||
| 25 | return Hex.encode(digest); | ||
| 26 | }); | ||
| 21 | 27 | ||
| 22 | /** Remember explicit sign-in success, never discovery success or the agent's credentials. */ | 28 | /** Remember explicit sign-in success, never discovery success or the agent's credentials. */ |
| 23 | export const makeAcpRegistryAuthenticationState = Effect.fn("makeAcpRegistryAuthenticationState")( | 29 | export const makeAcpRegistryAuthenticationState = Effect.fn("makeAcpRegistryAuthenticationState")( |
| ... | @@ -30,10 +36,10 @@ export const makeAcpRegistryAuthenticationState = Effect.fn("makeAcpRegistryAuth | ... | @@ -30,10 +36,10 @@ export const makeAcpRegistryAuthenticationState = Effect.fn("makeAcpRegistryAuth |
| 30 | }) { | 36 | }) { |
| 31 | const fs = yield* FileSystem.FileSystem; | 37 | const fs = yield* FileSystem.FileSystem; |
| 32 | const path = yield* Path.Path; | 38 | const path = yield* Path.Path; |
| 33 | const filePath = path.join(input.cacheDir, `acp-auth-${hash(input.instanceId)}.json`); | 39 | const filePath = path.join(input.cacheDir, `acp-auth-${yield* hash(input.instanceId)}.json`); |
| 34 | // Cosmetic settings and model discovery can rebuild the driver without | 40 | // Cosmetic settings and model discovery can rebuild the driver without |
| 35 | // changing the account. Credential overrides and profile paths cannot. | 41 | // changing the account. Credential overrides and profile paths cannot. |
| 36 | const binding = hash({ | 42 | const binding = yield* hash({ |
| 37 | ...(input.settings.source === "local" | 43 | ...(input.settings.source === "local" |
| 38 | ? { source: "local", commandArgs: input.settings.commandArgs } | 44 | ? { source: "local", commandArgs: input.settings.commandArgs } |
| 39 | : {}), | 45 | : {}), |
apps/server/src/provider/acp/AcpRegistrySupport.test.ts+4-3| ... | @@ -16,7 +16,6 @@ import * as Layer from "effect/Layer"; | ... | @@ -16,7 +16,6 @@ import * as Layer from "effect/Layer"; |
| 16 | import * as Schema from "effect/Schema"; | 16 | import * as Schema from "effect/Schema"; |
| 17 | import { HttpClient, HttpClientResponse } from "effect/http"; | 17 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 18 | import * as TestClock from "effect/testing/TestClock"; | 18 | import * as TestClock from "effect/testing/TestClock"; |
| 19 | import * as NodeCrypto from "node:crypto"; | ||
| 20 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; | 19 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 21 | 20 | ||
| 22 | import * as AcpRegistrySupport from "./AcpRegistrySupport.ts"; | 21 | import * as AcpRegistrySupport from "./AcpRegistrySupport.ts"; |
| ... | @@ -587,7 +586,8 @@ describe("AcpRegistrySupport", () => { | ... | @@ -587,7 +586,8 @@ describe("AcpRegistrySupport", () => { |
| 587 | archive: archiveUrl, | 586 | archive: archiveUrl, |
| 588 | cmd: "./bin/example-agent", | 587 | cmd: "./bin/example-agent", |
| 589 | args: ["acp"], | 588 | args: ["acp"], |
| 590 | sha256: NodeCrypto.createHash("sha256").update(binaryBytes).digest("hex"), | 589 | // SHA-256 of binaryBytes. |
| 590 | sha256: "97b29a636d7ddf7bf3567ae4d48c0f2a9b03943fd808b6efbf74f1dd3db131b7", | ||
| 591 | }, | 591 | }, |
| 592 | }, | 592 | }, |
| 593 | }); | 593 | }); |
| ... | @@ -646,7 +646,8 @@ describe("AcpRegistrySupport", () => { | ... | @@ -646,7 +646,8 @@ describe("AcpRegistrySupport", () => { |
| 646 | archive: archiveUrl, | 646 | archive: archiveUrl, |
| 647 | cmd: "./bin/example-agent", | 647 | cmd: "./bin/example-agent", |
| 648 | args: ["acp"], | 648 | args: ["acp"], |
| 649 | sha256: NodeCrypto.createHash("sha256").update(binaryBytes).digest("hex"), | 649 | // SHA-256 of binaryBytes. |
| 650 | sha256: "e1eb7c905b91c400af2ec858628aec22866bfda1383b93ceb73ae01d9769fb31", | ||
| 650 | }, | 651 | }, |
| 651 | }, | 652 | }, |
| 652 | }); | 653 | }); |
apps/server/src/provider/acp/AcpRegistrySupport.ts+33-27| ... | @@ -25,7 +25,9 @@ import { | ... | @@ -25,7 +25,9 @@ import { |
| 25 | import * as Clock from "effect/Clock"; | 25 | import * as Clock from "effect/Clock"; |
| 26 | import * as Context from "effect/Context"; | 26 | import * as Context from "effect/Context"; |
| 27 | import * as Duration from "effect/Duration"; | 27 | import * as Duration from "effect/Duration"; |
| 28 | import * as Crypto from "effect/Crypto"; | ||
| 28 | import * as Effect from "effect/Effect"; | 29 | import * as Effect from "effect/Effect"; |
| 30 | import * as Hex from "effect/encoding/Hex"; | ||
| 29 | import * as FileSystem from "effect/FileSystem"; | 31 | import * as FileSystem from "effect/FileSystem"; |
| 30 | import * as Layer from "effect/Layer"; | 32 | import * as Layer from "effect/Layer"; |
| 31 | import * as Option from "effect/Option"; | 33 | import * as Option from "effect/Option"; |
| ... | @@ -35,9 +37,9 @@ import * as Ref from "effect/Ref"; | ... | @@ -35,9 +37,9 @@ import * as Ref from "effect/Ref"; |
| 35 | import * as Schema from "effect/Schema"; | 37 | import * as Schema from "effect/Schema"; |
| 36 | import * as Semaphore from "effect/Semaphore"; | 38 | import * as Semaphore from "effect/Semaphore"; |
| 37 | import * as Stream from "effect/Stream"; | 39 | import * as Stream from "effect/Stream"; |
| 40 | import { sha256 } from "@noble/hashes/sha2"; | ||
| 38 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; | 41 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; |
| 39 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; | 42 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 40 | import * as NodeCrypto from "node:crypto"; | ||
| 41 | 43 | ||
| 42 | import { collectUint8StreamText } from "../../stream/collectUint8StreamText.ts"; | 44 | import { collectUint8StreamText } from "../../stream/collectUint8StreamText.ts"; |
| 43 | import * as ServerSettings from "../../serverSettings.ts"; | 45 | import * as ServerSettings from "../../serverSettings.ts"; |
| ... | @@ -597,11 +599,15 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -597,11 +599,15 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 597 | AcpRegistryCatalog["Service"], | 599 | AcpRegistryCatalog["Service"], |
| 598 | never, | 600 | never, |
| 599 | | ChildProcessSpawner.ChildProcessSpawner | 601 | | ChildProcessSpawner.ChildProcessSpawner |
| 602 | | Crypto.Crypto | ||
| 600 | | FileSystem.FileSystem | 603 | | FileSystem.FileSystem |
| 601 | | HttpClient.HttpClient | 604 | | HttpClient.HttpClient |
| 602 | | Path.Path | 605 | | Path.Path |
| 603 | | ServerSettings.ServerSettingsService | 606 | | ServerSettings.ServerSettingsService |
| 604 | > { | 607 | > { |
| 608 | const crypto = yield* Crypto.Crypto; | ||
| 609 | const sha256Hex = (data: Uint8Array) => | ||
| 610 | crypto.digest("SHA-256", data).pipe(Effect.map(Hex.encode), Effect.orDie); | ||
| 605 | const fileSystem = yield* FileSystem.FileSystem; | 611 | const fileSystem = yield* FileSystem.FileSystem; |
| 606 | const path = yield* Path.Path; | 612 | const path = yield* Path.Path; |
| 607 | const httpClient = yield* HttpClient.HttpClient; | 613 | const httpClient = yield* HttpClient.HttpClient; |
| ... | @@ -731,17 +737,20 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -731,17 +737,20 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 731 | .readFileString(registryCachePath) | 737 | .readFileString(registryCachePath) |
| 732 | .pipe(Effect.flatMap(decodeRegistryText), Effect.option); | 738 | .pipe(Effect.flatMap(decodeRegistryText), Effect.option); |
| 733 | 739 | ||
| 734 | const writeRegistryCache = (text: string) => { | 740 | const writeRegistryCache = (text: string) => |
| 735 | const temporaryPath = `${registryCachePath}.${process.pid}-${NodeCrypto.randomUUID()}.tmp`; | 741 | crypto.randomUUIDv4.pipe( |
| 736 | return fileSystem | 742 | Effect.flatMap((id) => { |
| 737 | .makeDirectory(registryDirectory, { recursive: true }) | 743 | const temporaryPath = `${registryCachePath}.${process.pid}-${id}.tmp`; |
| 738 | .pipe( | 744 | return fileSystem |
| 739 | Effect.andThen(fileSystem.writeFileString(temporaryPath, text)), | 745 | .makeDirectory(registryDirectory, { recursive: true }) |
| 740 | Effect.andThen(fileSystem.rename(temporaryPath, registryCachePath)), | 746 | .pipe( |
| 741 | Effect.ensuring(fileSystem.remove(temporaryPath, { force: true }).pipe(Effect.ignore)), | 747 | Effect.andThen(fileSystem.writeFileString(temporaryPath, text)), |
| 742 | Effect.ignore, | 748 | Effect.andThen(fileSystem.rename(temporaryPath, registryCachePath)), |
| 743 | ); | 749 | Effect.ensuring(fileSystem.remove(temporaryPath, { force: true }).pipe(Effect.ignore)), |
| 744 | }; | 750 | ); |
| 751 | }), | ||
| 752 | Effect.ignore, | ||
| 753 | ); | ||
| 745 | 754 | ||
| 746 | const fetchRegistry = Effect.fn("AcpRegistryCatalog.fetchRegistry")(function* () { | 755 | const fetchRegistry = Effect.fn("AcpRegistryCatalog.fetchRegistry")(function* () { |
| 747 | yield* assertHttpsUrl(registryUrl, "ACP Registry index URL must use HTTPS."); | 756 | yield* assertHttpsUrl(registryUrl, "ACP Registry index URL must use HTTPS."); |
| ... | @@ -928,17 +937,12 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -928,17 +937,12 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 928 | ); | 937 | ); |
| 929 | }); | 938 | }); |
| 930 | 939 | ||
| 931 | const packageReceiptPath = ( | 940 | const packageReceiptPath = (agentId: string, distribution: "npx" | "uvx", managerPath: string) => |
| 932 | agentId: string, | 941 | sha256Hex(new TextEncoder().encode(`${distribution}\0${managerPath}`)).pipe( |
| 933 | distribution: "npx" | "uvx", | 942 | Effect.map((digest) => |
| 934 | managerPath: string, | 943 | path.join(packageReceiptsDirectory, `${agentId}-${digest.slice(0, 16)}.json`), |
| 935 | ) => { | 944 | ), |
| 936 | const managerId = NodeCrypto.createHash("sha256") | 945 | ); |
| 937 | .update(`${distribution}\0${managerPath}`) | ||
| 938 | .digest("hex") | ||
| 939 | .slice(0, 16); | ||
| 940 | return path.join(packageReceiptsDirectory, `${agentId}-${managerId}.json`); | ||
| 941 | }; | ||
| 942 | 946 | ||
| 943 | const readPackageReceipt = Effect.fn("AcpRegistryCatalog.readPackageReceipt")(function* ( | 947 | const readPackageReceipt = Effect.fn("AcpRegistryCatalog.readPackageReceipt")(function* ( |
| 944 | agent: AcpRegistryAgent, | 948 | agent: AcpRegistryAgent, |
| ... | @@ -947,7 +951,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -947,7 +951,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 947 | managerPath: string, | 951 | managerPath: string, |
| 948 | ) { | 952 | ) { |
| 949 | const receipt = yield* fileSystem | 953 | const receipt = yield* fileSystem |
| 950 | .readFileString(packageReceiptPath(agent.id, distribution, managerPath)) | 954 | .readFileString(yield* packageReceiptPath(agent.id, distribution, managerPath)) |
| 951 | .pipe( | 955 | .pipe( |
| 952 | Effect.map(decodePackageInstallReceipt), | 956 | Effect.map(decodePackageInstallReceipt), |
| 953 | Effect.orElseSucceed(() => Option.none()), | 957 | Effect.orElseSucceed(() => Option.none()), |
| ... | @@ -991,7 +995,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -991,7 +995,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 991 | 995 | ||
| 992 | const writePackageReceipt = Effect.fn("AcpRegistryCatalog.writePackageReceipt")( | 996 | const writePackageReceipt = Effect.fn("AcpRegistryCatalog.writePackageReceipt")( |
| 993 | function* (receipt: AcpRegistryPackageInstallReceipt) { | 997 | function* (receipt: AcpRegistryPackageInstallReceipt) { |
| 994 | const receiptPath = packageReceiptPath( | 998 | const receiptPath = yield* packageReceiptPath( |
| 995 | receipt.agentId, | 999 | receipt.agentId, |
| 996 | receipt.distribution, | 1000 | receipt.distribution, |
| 997 | receipt.managerPath, | 1001 | receipt.managerPath, |
| ... | @@ -1409,7 +1413,9 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -1409,7 +1413,9 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 1409 | ), | 1413 | ), |
| 1410 | }), | 1414 | }), |
| 1411 | ); | 1415 | ); |
| 1412 | const hash = NodeCrypto.createHash("sha256"); | 1416 | // Hash while streaming: archives can be up to MAX_ARCHIVE_BYTES, and |
| 1417 | // Effect's Crypto only digests a whole buffer. | ||
| 1418 | const hash = sha256.create(); | ||
| 1413 | let downloadedBytes = 0; | 1419 | let downloadedBytes = 0; |
| 1414 | yield* response.stream.pipe( | 1420 | yield* response.stream.pipe( |
| 1415 | Stream.tap((chunk) => { | 1421 | Stream.tap((chunk) => { |
| ... | @@ -1447,7 +1453,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -1447,7 +1453,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 1447 | }), | 1453 | }), |
| 1448 | ); | 1454 | ); |
| 1449 | if (target.sha256 !== undefined) { | 1455 | if (target.sha256 !== undefined) { |
| 1450 | const actual = hash.digest("hex"); | 1456 | const actual = Hex.encode(hash.digest()); |
| 1451 | if (actual !== target.sha256.toLowerCase()) { | 1457 | if (actual !== target.sha256.toLowerCase()) { |
| 1452 | return yield* new AcpRegistryError({ | 1458 | return yield* new AcpRegistryError({ |
| 1453 | reason: "checksum_mismatch", | 1459 | reason: "checksum_mismatch", |
apps/server/src/provider/opencode2/OpenCode2Client.live.test.ts+3-2| ... | @@ -10,6 +10,7 @@ | ... | @@ -10,6 +10,7 @@ |
| 10 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 10 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 11 | import { AbsolutePath, Location, Model, Provider } from "@opencode/client/effect"; | 11 | import { AbsolutePath, Location, Model, Provider } from "@opencode/client/effect"; |
| 12 | import { assert, it } from "@effect/vitest"; | 12 | import { assert, it } from "@effect/vitest"; |
| 13 | import * as Crypto from "effect/Crypto"; | ||
| 13 | import * as Effect from "effect/Effect"; | 14 | import * as Effect from "effect/Effect"; |
| 14 | import * as Base64Url from "effect/encoding/Base64Url"; | 15 | import * as Base64Url from "effect/encoding/Base64Url"; |
| 15 | import * as Fiber from "effect/Fiber"; | 16 | import * as Fiber from "effect/Fiber"; |
| ... | @@ -21,7 +22,6 @@ import * as Path from "effect/Path"; | ... | @@ -21,7 +22,6 @@ import * as Path from "effect/Path"; |
| 21 | import * as Stream from "effect/Stream"; | 22 | import * as Stream from "effect/Stream"; |
| 22 | import { FetchHttpClient } from "effect/http"; | 23 | import { FetchHttpClient } from "effect/http"; |
| 23 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; | 24 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 24 | import * as NodeCrypto from "node:crypto"; | ||
| 25 | import { describe } from "vite-plus/test"; | 25 | import { describe } from "vite-plus/test"; |
| 26 | 26 | ||
| 27 | import * as OpenCode2Client from "./OpenCode2Client.ts"; | 27 | import * as OpenCode2Client from "./OpenCode2Client.ts"; |
| ... | @@ -36,7 +36,8 @@ const startServer = Effect.fn("OpenCode2ClientLive.startServer")(function* (bina | ... | @@ -36,7 +36,8 @@ const startServer = Effect.fn("OpenCode2ClientLive.startServer")(function* (bina |
| 36 | const directory = path.join(root, "work"); | 36 | const directory = path.join(root, "work"); |
| 37 | yield* fs.makeDirectory(directory); | 37 | yield* fs.makeDirectory(directory); |
| 38 | // Non-ASCII on purpose: OpenCode decodes Basic credentials as UTF-8. | 38 | // Non-ASCII on purpose: OpenCode decodes Basic credentials as UTF-8. |
| 39 | const password = `${Base64Url.encode(NodeCrypto.randomBytes(32))}-pässwörd€`; | 39 | const crypto = yield* Crypto.Crypto; |
| 40 | const password = `${Base64Url.encode(yield* crypto.randomBytes(32))}-pässwörd€`; | ||
| 40 | const child = yield* spawner.spawn( | 41 | const child = yield* spawner.spawn( |
| 41 | ChildProcess.make(binary, ["serve", "--hostname=127.0.0.1", "--port=0"], { | 42 | ChildProcess.make(binary, ["serve", "--hostname=127.0.0.1", "--port=0"], { |
| 42 | cwd: directory, | 43 | cwd: directory, |
apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import * as NodePlatformCrypto from "@effect/platform-node/NodeCrypto"; | 4 | import * as NodePlatformCrypto from "@effect/platform-node/NodeCrypto"; |
apps/server/src/scheduledTasks/webhookRoute.test.ts+1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 2 | import * as NodeCrypto from "node:crypto"; | 3 | import * as NodeCrypto from "node:crypto"; |
| 3 | import * as Clock from "effect/Clock"; | 4 | import * as Clock from "effect/Clock"; |
| 4 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
apps/server/src/scheduledTasks/webhookVerification.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { assert, describe, it } from "@effect/vitest"; | 4 | import { assert, describe, it } from "@effect/vitest"; |
apps/server/src/scheduledTasks/webhookVerification.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import type { ScheduledTaskWebhookSignature } from "@t3tools/contracts"; | 4 | import type { ScheduledTaskWebhookSignature } from "@t3tools/contracts"; |
apps/server/src/secrets/SecretRequests.ts+1| ... | @@ -16,6 +16,7 @@ import { | ... | @@ -16,6 +16,7 @@ import { |
| 16 | type SecretRequestAnswerInput, | 16 | type SecretRequestAnswerInput, |
| 17 | type ThreadId, | 17 | type ThreadId, |
| 18 | } from "@t3tools/contracts"; | 18 | } from "@t3tools/contracts"; |
| 19 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac. | ||
| 19 | import * as NodeCrypto from "node:crypto"; | 20 | import * as NodeCrypto from "node:crypto"; |
| 20 | 21 | ||
| 21 | import * as Clock from "effect/Clock"; | 22 | import * as Clock from "effect/Clock"; |
apps/server/src/telemetry/Identify.test.ts+11-6| ... | @@ -1,7 +1,8 @@ | ... | @@ -1,7 +1,8 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import * as Crypto from "effect/Crypto"; | ||
| 4 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Hex from "effect/encoding/Hex"; | ||
| 5 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 7 | import * as Logger from "effect/Logger"; | 8 | import * as Logger from "effect/Logger"; |
| ... | @@ -17,8 +18,12 @@ interface CapturedLog { | ... | @@ -17,8 +18,12 @@ interface CapturedLog { |
| 17 | readonly annotations: Readonly<Record<string, unknown>>; | 18 | readonly annotations: Readonly<Record<string, unknown>>; |
| 18 | } | 19 | } |
| 19 | 20 | ||
| 20 | const sha256 = (value: string) => | 21 | const sha256 = Effect.fn("test.sha256")(function* (value: string) { |
| 21 | NodeCrypto.createHash("sha256").update(value, "utf8").digest("hex"); | 22 | const crypto = yield* Crypto.Crypto; |
| 23 | return Hex.encode( | ||
| 24 | yield* crypto.digest("SHA-256", new TextEncoder().encode(value)).pipe(Effect.orDie), | ||
| 25 | ); | ||
| 26 | }); | ||
| 22 | 27 | ||
| 23 | const makeCaptureLogger = (logs: CapturedLog[]) => | 28 | const makeCaptureLogger = (logs: CapturedLog[]) => |
| 24 | Logger.make(({ fiber, message }) => { | 29 | Logger.make(({ fiber, message }) => { |
| ... | @@ -48,7 +53,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { | ... | @@ -48,7 +53,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { |
| 48 | path.join(config.baseDir, "home"), | 53 | path.join(config.baseDir, "home"), |
| 49 | ); | 54 | ); |
| 50 | 55 | ||
| 51 | assert.equal(identifier, sha256(anonymousId)); | 56 | assert.equal(identifier, yield* sha256(anonymousId)); |
| 52 | }).pipe( | 57 | }).pipe( |
| 53 | Effect.provide( | 58 | Effect.provide( |
| 54 | ServerConfig.layerTest(process.cwd(), { | 59 | ServerConfig.layerTest(process.cwd(), { |
| ... | @@ -132,7 +137,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { | ... | @@ -132,7 +137,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { |
| 132 | 137 | ||
| 133 | const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); | 138 | const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); |
| 134 | 139 | ||
| 135 | assert.equal(identifier, sha256(anonymousId)); | 140 | assert.equal(identifier, yield* sha256(anonymousId)); |
| 136 | assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError")); | 141 | assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError")); |
| 137 | assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityReadError")); | 142 | assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityReadError")); |
| 138 | const allLogs = logs | 143 | const allLogs = logs |
| ... | @@ -175,7 +180,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { | ... | @@ -175,7 +180,7 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { |
| 175 | 180 | ||
| 176 | const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); | 181 | const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); |
| 177 | 182 | ||
| 178 | assert.equal(identifier, sha256(anonymousId)); | 183 | assert.equal(identifier, yield* sha256(anonymousId)); |
| 179 | const decodeLog = findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError"); | 184 | const decodeLog = findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError"); |
| 180 | assert.isDefined(decodeLog); | 185 | assert.isDefined(decodeLog); |
| 181 | assert.equal( | 186 | assert.equal( |
apps/server/src/usage/cliproxyApi.test.ts+8-3| ... | @@ -1,10 +1,11 @@ | ... | @@ -1,10 +1,11 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 2 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| 4 | import * as TestClock from "effect/testing/TestClock"; | 5 | import * as TestClock from "effect/testing/TestClock"; |
| 5 | import { HttpClient, HttpClientResponse } from "effect/http"; | 6 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 6 | 7 | ||
| 7 | import { creditRedeemRequestId, makeCliproxyApi } from "./cliproxyApi.ts"; | 8 | import { makeCliproxyApi } from "./cliproxyApi.ts"; |
| 8 | 9 | ||
| 9 | const config = { | 10 | const config = { |
| 10 | kind: "cliproxy", | 11 | kind: "cliproxy", |
| ... | @@ -106,7 +107,10 @@ function fixture( | ... | @@ -106,7 +107,10 @@ function fixture( |
| 106 | ); | 107 | ); |
| 107 | return { | 108 | return { |
| 108 | requests, | 109 | requests, |
| 109 | api: makeCliproxyApi.pipe(Effect.provideService(HttpClient.HttpClient, http)), | 110 | api: makeCliproxyApi.pipe( |
| 111 | Effect.provideService(HttpClient.HttpClient, http), | ||
| 112 | Effect.provide(NodeCrypto.layer), | ||
| 113 | ), | ||
| 110 | }; | 114 | }; |
| 111 | } | 115 | } |
| 112 | 116 | ||
| ... | @@ -222,7 +226,8 @@ describe("CLIProxyAPI built-in management API", () => { | ... | @@ -222,7 +226,8 @@ describe("CLIProxyAPI built-in management API", () => { |
| 222 | expect(redemptions[0]?.body?.data).toBe(redemptions[1]?.body?.data); | 226 | expect(redemptions[0]?.body?.data).toBe(redemptions[1]?.body?.data); |
| 223 | expect(redemptions[0]?.body?.data).toBe( | 227 | expect(redemptions[0]?.body?.data).toBe( |
| 224 | encodeJson({ | 228 | encodeJson({ |
| 225 | redeem_request_id: creditRedeemRequestId("account-b", "credit-b"), | 229 | // UUIDv5 of "account-b:credit-b"; must stay stable so retries deduplicate. |
| 230 | redeem_request_id: "519d5243-011a-5b7b-91f3-44d85f095705", | ||
| 226 | credit_id: "credit-b", | 231 | credit_id: "credit-b", |
| 227 | }), | 232 | }), |
| 228 | ); | 233 | ); |
apps/server/src/usage/cliproxyApi.ts+22-12| ... | @@ -1,5 +1,3 @@ | ... | @@ -1,5 +1,3 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | |||
| 3 | import { | 1 | import { |
| 4 | ProviderDriverKind, | 2 | ProviderDriverKind, |
| 5 | UsageLimitSourceError, | 3 | UsageLimitSourceError, |
| ... | @@ -7,8 +5,10 @@ import { | ... | @@ -7,8 +5,10 @@ import { |
| 7 | type UsageLimitSourceAccount, | 5 | type UsageLimitSourceAccount, |
| 8 | type UsageLimitSourceConfig, | 6 | type UsageLimitSourceConfig, |
| 9 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| 8 | import * as Crypto from "effect/Crypto"; | ||
| 10 | import * as DateTime from "effect/DateTime"; | 9 | import * as DateTime from "effect/DateTime"; |
| 11 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Hex from "effect/encoding/Hex"; | ||
| 12 | import * as Schema from "effect/Schema"; | 12 | import * as Schema from "effect/Schema"; |
| 13 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; | 13 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; |
| 14 | 14 | ||
| ... | @@ -99,21 +99,31 @@ const decodeConsumeResponse = Schema.decodeUnknownEffect( | ... | @@ -99,21 +99,31 @@ const decodeConsumeResponse = Schema.decodeUnknownEffect( |
| 99 | const CODEX_BASE = "https://chatgpt.com/backend-api/wham"; | 99 | const CODEX_BASE = "https://chatgpt.com/backend-api/wham"; |
| 100 | const CREDIT_URL = `${CODEX_BASE}/rate-limit-reset-credits`; | 100 | const CREDIT_URL = `${CODEX_BASE}/rate-limit-reset-credits`; |
| 101 | 101 | ||
| 102 | // 6f1c2a9e-2d4b-4c1e-9a7f-3b8d5e0c1a42 | ||
| 103 | const CREDIT_REDEEM_NAMESPACE = new Uint8Array([ | ||
| 104 | 0x6f, 0x1c, 0x2a, 0x9e, 0x2d, 0x4b, 0x4c, 0x1e, 0x9a, 0x7f, 0x3b, 0x8d, 0x5e, 0x0c, 0x1a, 0x42, | ||
| 105 | ]); | ||
| 106 | |||
| 102 | // UUIDv5 per account and credit also deduplicates retries across T3 environments. | 107 | // UUIDv5 per account and credit also deduplicates retries across T3 environments. |
| 103 | export function creditRedeemRequestId(accountId: string, creditId: string): string { | 108 | const creditRedeemRequestId = Effect.fn("CliproxyApi.creditRedeemRequestId")(function* ( |
| 104 | const bytes = NodeCrypto.createHash("sha1") | 109 | accountId: string, |
| 105 | .update(Buffer.from("6f1c2a9e2d4b4c1e9a7f3b8d5e0c1a42", "hex")) | 110 | creditId: string, |
| 106 | .update(`${accountId}:${creditId}`) | 111 | ) { |
| 107 | .digest() | 112 | const crypto = yield* Crypto.Crypto; |
| 108 | .subarray(0, 16); | 113 | const name = new TextEncoder().encode(`${accountId}:${creditId}`); |
| 114 | const input = new Uint8Array(CREDIT_REDEEM_NAMESPACE.length + name.length); | ||
| 115 | input.set(CREDIT_REDEEM_NAMESPACE); | ||
| 116 | input.set(name, CREDIT_REDEEM_NAMESPACE.length); | ||
| 117 | const bytes = (yield* crypto.digest("SHA-1", input).pipe(Effect.orDie)).slice(0, 16); | ||
| 109 | bytes[6] = (bytes[6]! & 0x0f) | 0x50; | 118 | bytes[6] = (bytes[6]! & 0x0f) | 0x50; |
| 110 | bytes[8] = (bytes[8]! & 0x3f) | 0x80; | 119 | bytes[8] = (bytes[8]! & 0x3f) | 0x80; |
| 111 | const hex = bytes.toString("hex"); | 120 | const hex = Hex.encode(bytes); |
| 112 | return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; | 121 | return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; |
| 113 | } | 122 | }); |
| 114 | 123 | ||
| 115 | export const makeCliproxyApi = Effect.gen(function* () { | 124 | export const makeCliproxyApi = Effect.gen(function* () { |
| 116 | const client = yield* HttpClient.HttpClient; | 125 | const client = yield* HttpClient.HttpClient; |
| 126 | const crypto = yield* Crypto.Crypto; | ||
| 117 | 127 | ||
| 118 | const management = Effect.fn("CliproxyApi.management")(function* ( | 128 | const management = Effect.fn("CliproxyApi.management")(function* ( |
| 119 | config: UsageLimitSourceConfig, | 129 | config: UsageLimitSourceConfig, |
| ... | @@ -323,10 +333,10 @@ export const makeCliproxyApi = Effect.gen(function* () { | ... | @@ -323,10 +333,10 @@ export const makeCliproxyApi = Effect.gen(function* () { |
| 323 | }); | 333 | }); |
| 324 | } | 334 | } |
| 325 | const body = yield* apiCall(config, account, `${CREDIT_URL}/consume`, { | 335 | const body = yield* apiCall(config, account, `${CREDIT_URL}/consume`, { |
| 326 | redeem_request_id: creditRedeemRequestId( | 336 | redeem_request_id: yield* creditRedeemRequestId( |
| 327 | account.id_token?.chatgpt_account_id ?? account.id, | 337 | account.id_token?.chatgpt_account_id ?? account.id, |
| 328 | creditId, | 338 | creditId, |
| 329 | ), | 339 | ).pipe(Effect.provideService(Crypto.Crypto, crypto)), |
| 330 | credit_id: creditId, | 340 | credit_id: creditId, |
| 331 | }); | 341 | }); |
| 332 | const response = yield* decodeConsumeResponse(body); | 342 | const response = yield* decodeConsumeResponse(body); |
apps/server/src/zipArchive.ts+1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import * as Effect from "effect/Effect"; | 1 | import * as Effect from "effect/Effect"; |
| 2 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- type-only, to adapt a Node Readable into Effect. | ||
| 2 | import type * as NodeStream from "node:stream"; | 3 | import type * as NodeStream from "node:stream"; |
| 3 | import * as Yauzl from "yauzl"; | 4 | import * as Yauzl from "yauzl"; |
| 4 | 5 |
infra/relay/src/agentActivity/ApnsClient.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { EnvironmentId, ThreadId } from "@t3tools/contracts"; | 4 | import { EnvironmentId, ThreadId } from "@t3tools/contracts"; |
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+1| ... | @@ -4,6 +4,7 @@ import type { | ... | @@ -4,6 +4,7 @@ import type { |
| 4 | } from "@t3tools/contracts/relay"; | 4 | } from "@t3tools/contracts/relay"; |
| 5 | import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto"; | 5 | import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto"; |
| 6 | import { describe, expect, it } from "@effect/vitest"; | 6 | import { describe, expect, it } from "@effect/vitest"; |
| 7 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 7 | import * as NodeCrypto from "node:crypto"; | 8 | import * as NodeCrypto from "node:crypto"; |
| 8 | import * as Deferred from "effect/Deferred"; | 9 | import * as Deferred from "effect/Deferred"; |
| 9 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
infra/relay/src/agentActivity/ApnsProviderTokens.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { describe, expect, it } from "@effect/vitest"; | 4 | import { describe, expect, it } from "@effect/vitest"; |
infra/relay/src/agentActivity/FcmClient.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or verify. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | import { describe, expect, it } from "@effect/vitest"; | 3 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | import * as Deferred from "effect/Deferred"; | 4 | import * as Deferred from "effect/Deferred"; |
infra/relay/src/agentActivity/apnsDeliveryJobs.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { | 4 | import { |
infra/relay/src/agentActivity/apnsJwt.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createPrivateKey. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { p256 } from "@noble/curves/nist"; | 4 | import { p256 } from "@noble/curves/nist"; |
infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { describe, expect, it } from "@effect/vitest"; | 4 | import { describe, expect, it } from "@effect/vitest"; |
infra/relay/src/auth/RelayTokens.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { describe, expect, it } from "@effect/vitest"; | 4 | import { describe, expect, it } from "@effect/vitest"; |
infra/relay/src/environments/EnvironmentConnector.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto"; | 3 | import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto"; |
| 3 | 4 |
infra/relay/src/environments/EnvironmentLinker.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | import type { | 3 | import type { |
| 3 | RelayEnvironmentLinkProofPayload, | 4 | RelayEnvironmentLinkProofPayload, |
infra/relay/src/environments/EnvironmentPublishSignatures.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import type { | 4 | import type { |
infra/relay/src/environments/ManagedEndpointProvider.test.ts+16-11| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 2 | ||
| 4 | import { describe, expect, it } from "@effect/vitest"; | 3 | import { describe, expect, it } from "@effect/vitest"; |
| ... | @@ -365,20 +364,26 @@ function layerProvider( | ... | @@ -365,20 +364,26 @@ function layerProvider( |
| 365 | ); | 364 | ); |
| 366 | } | 365 | } |
| 367 | 366 | ||
| 367 | // First 16 hex chars of SHA-256(`dev_julius:${userId}:${environmentId}`), pinned so a | ||
| 368 | // change to the endpoint naming scheme fails here instead of silently matching. | ||
| 369 | const MANAGED_ENDPOINT_HASHES: Record<string, string> = { | ||
| 370 | "user_ABC:env_ABC": "d101ac68108a423e", | ||
| 371 | "user_ABC:env_shared": "d7e6356aaf8863aa", | ||
| 372 | "user_DEF:env_shared": "a2fc84ac1b8c1c35", | ||
| 373 | }; | ||
| 374 | |||
| 375 | function expectedManagedEndpointHash(environmentId: string, userId: string): string { | ||
| 376 | const hash = MANAGED_ENDPOINT_HASHES[`${userId}:${environmentId}`]; | ||
| 377 | if (hash === undefined) throw new Error(`No pinned hash for ${userId}:${environmentId}`); | ||
| 378 | return hash; | ||
| 379 | } | ||
| 380 | |||
| 368 | function expectedManagedHostname(environmentId: string, userId = "user_ABC"): string { | 381 | function expectedManagedHostname(environmentId: string, userId = "user_ABC"): string { |
| 369 | const hash = NodeCrypto.createHash("sha256") | 382 | return `dev-julius-${expectedManagedEndpointHash(environmentId, userId)}.t3code.test`; |
| 370 | .update(`dev_julius:${userId}:${environmentId}`) | ||
| 371 | .digest("hex") | ||
| 372 | .slice(0, 16); | ||
| 373 | return `dev-julius-${hash}.t3code.test`; | ||
| 374 | } | 383 | } |
| 375 | 384 | ||
| 376 | function expectedManagedTunnelName(environmentId: string, userId = "user_ABC"): string { | 385 | function expectedManagedTunnelName(environmentId: string, userId = "user_ABC"): string { |
| 377 | const hash = NodeCrypto.createHash("sha256") | 386 | return `t3coderelay-managedendpoint-dev-julius-${expectedManagedEndpointHash(environmentId, userId)}`; |
| 378 | .update(`dev_julius:${userId}:${environmentId}`) | ||
| 379 | .digest("hex") | ||
| 380 | .slice(0, 16); | ||
| 381 | return `t3coderelay-managedendpoint-dev-julius-${hash}`; | ||
| 382 | } | 387 | } |
| 383 | 388 | ||
| 384 | describe("ManagedEndpointProvider", () => { | 389 | describe("ManagedEndpointProvider", () => { |
infra/relay/src/hooks/HookForwarder.test.ts+1| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; | 1 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 3 | import * as NodeCrypto from "node:crypto"; | 4 | import * as NodeCrypto from "node:crypto"; |
| 4 | import * as EffectNodeCrypto from "@effect/platform-node/NodeCrypto"; | 5 | import * as EffectNodeCrypto from "@effect/platform-node/NodeCrypto"; |
| 5 | import { describe, expect, it } from "@effect/vitest"; | 6 | import { describe, expect, it } from "@effect/vitest"; |
infra/relay/src/http/Api.test.ts+1| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | import * as EnvironmentLinker from "../environments/EnvironmentLinker.ts"; | 7 | import * as EnvironmentLinker from "../environments/EnvironmentLinker.ts"; |
| 8 | import * as RelayTokens from "../auth/RelayTokens.ts"; | 8 | import * as RelayTokens from "../auth/RelayTokens.ts"; |
| 9 | import * as Devices from "../agentActivity/Devices.ts"; | 9 | import * as Devices from "../agentActivity/Devices.ts"; |
| 10 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 10 | import * as NodeCrypto from "node:crypto"; | 11 | import * as NodeCrypto from "node:crypto"; |
| 11 | import { createClerkClient, verifyToken } from "@clerk/backend"; | 12 | import { createClerkClient, verifyToken } from "@clerk/backend"; |
| 12 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; | 13 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; |
packages/shared/src/desktopBootstrapToken.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac or timingSafeEqual. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | /** | 4 | /** |
packages/shared/src/dpop.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no KeyObject, generateKeyPairSync, or sign. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { assert, describe, it } from "@effect/vitest"; | 4 | import { assert, describe, it } from "@effect/vitest"; |
packages/shared/src/relayJwt.test.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. | ||
| 1 | import * as NodeCrypto from "node:crypto"; | 2 | import * as NodeCrypto from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { describe, expect, it } from "@effect/vitest"; | 4 | import { describe, expect, it } from "@effect/vitest"; |
packages/ssh/src/command.test.ts+25| ... | @@ -14,6 +14,7 @@ import { | ... | @@ -14,6 +14,7 @@ import { |
| 14 | baseSshArgs, | 14 | baseSshArgs, |
| 15 | getLastNonEmptyOutputLine, | 15 | getLastNonEmptyOutputLine, |
| 16 | parseSshResolveOutput, | 16 | parseSshResolveOutput, |
| 17 | remoteStateKey, | ||
| 17 | runSshCommand, | 18 | runSshCommand, |
| 18 | } from "./command.ts"; | 19 | } from "./command.ts"; |
| 19 | import { SshCommandError } from "./errors.ts"; | 20 | import { SshCommandError } from "./errors.ts"; |
| ... | @@ -98,6 +99,30 @@ describe("ssh command", () => { | ... | @@ -98,6 +99,30 @@ describe("ssh command", () => { |
| 98 | }), | 99 | }), |
| 99 | ); | 100 | ); |
| 100 | 101 | ||
| 102 | // Remote servers store state under this key, so it must not change across releases. | ||
| 103 | it.effect("derives a stable remote state key", () => | ||
| 104 | Effect.gen(function* () { | ||
| 105 | assert.equal( | ||
| 106 | yield* remoteStateKey({ | ||
| 107 | alias: "devbox", | ||
| 108 | hostname: "devbox.example.com", | ||
| 109 | username: "julius", | ||
| 110 | port: 2222, | ||
| 111 | }), | ||
| 112 | "711bc738002d72fd", | ||
| 113 | ); | ||
| 114 | assert.equal( | ||
| 115 | yield* remoteStateKey({ | ||
| 116 | alias: "fixture", | ||
| 117 | hostname: "fixture", | ||
| 118 | username: null, | ||
| 119 | port: null, | ||
| 120 | }), | ||
| 121 | "326264c4f08c8a0c", | ||
| 122 | ); | ||
| 123 | }).pipe(Effect.provide(NodeServices.layer)), | ||
| 124 | ); | ||
| 125 | |||
| 101 | it.effect("reads the last non-empty ssh output line", () => | 126 | it.effect("reads the last non-empty ssh output line", () => |
| 102 | Effect.sync(() => { | 127 | Effect.sync(() => { |
| 103 | assert.equal( | 128 | assert.equal( |
packages/ssh/src/command.ts+12-8| ... | @@ -1,9 +1,9 @@ | ... | @@ -1,9 +1,9 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | |||
| 3 | import type { DesktopSshEnvironmentTarget } from "@t3tools/contracts"; | 1 | import type { DesktopSshEnvironmentTarget } from "@t3tools/contracts"; |
| 4 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 3 | import * as Crypto from "effect/Crypto"; | ||
| 5 | import * as Duration from "effect/Duration"; | 4 | import * as Duration from "effect/Duration"; |
| 6 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Hex from "effect/encoding/Hex"; | ||
| 7 | import * as FileSystem from "effect/FileSystem"; | 7 | import * as FileSystem from "effect/FileSystem"; |
| 8 | import * as Option from "effect/Option"; | 8 | import * as Option from "effect/Option"; |
| 9 | import * as Path from "effect/Path"; | 9 | import * as Path from "effect/Path"; |
| ... | @@ -72,12 +72,16 @@ export function targetConnectionKey(target: DesktopSshEnvironmentTarget): string | ... | @@ -72,12 +72,16 @@ export function targetConnectionKey(target: DesktopSshEnvironmentTarget): string |
| 72 | return `${target.alias}\u0000${target.hostname}\u0000${target.username ?? ""}\u0000${target.port ?? ""}`; | 72 | return `${target.alias}\u0000${target.hostname}\u0000${target.username ?? ""}\u0000${target.port ?? ""}`; |
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | export function remoteStateKey(target: DesktopSshEnvironmentTarget): string { | 75 | /** Names the remote state directory for a target: the first 16 hex chars of its SHA-256 key. */ |
| 76 | return NodeCrypto.createHash("sha256") | 76 | export const remoteStateKey = Effect.fn("ssh/command.remoteStateKey")(function* ( |
| 77 | .update(targetConnectionKey(target)) | 77 | target: DesktopSshEnvironmentTarget, |
| 78 | .digest("hex") | 78 | ): Effect.fn.Return<string, never, Crypto.Crypto> { |
| 79 | .slice(0, 16); | 79 | const crypto = yield* Crypto.Crypto; |
| 80 | } | 80 | const digest = yield* crypto |
| 81 | .digest("SHA-256", encoder.encode(targetConnectionKey(target))) | ||
| 82 | .pipe(Effect.orDie); | ||
| 83 | return Hex.encode(digest).slice(0, 16); | ||
| 84 | }); | ||
| 81 | 85 | ||
| 82 | function buildSshHostSpec(target: DesktopSshEnvironmentTarget): string { | 86 | function buildSshHostSpec(target: DesktopSshEnvironmentTarget): string { |
| 83 | const destination = target.alias.trim() || target.hostname.trim(); | 87 | const destination = target.alias.trim() || target.hostname.trim(); |
packages/ssh/src/runnerProcess.test.ts+9-6| ... | @@ -11,6 +11,7 @@ import * as Stream from "effect/Stream"; | ... | @@ -11,6 +11,7 @@ import * as Stream from "effect/Stream"; |
| 11 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; | 11 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 12 | import * as NodeNet from "node:net"; | 12 | import * as NodeNet from "node:net"; |
| 13 | 13 | ||
| 14 | import { remoteStateKey } from "./command.ts"; | ||
| 14 | import { buildRemoteStopScript, buildRemoteT3RunnerScript } from "./tunnel.ts"; | 15 | import { buildRemoteStopScript, buildRemoteT3RunnerScript } from "./tunnel.ts"; |
| 15 | 16 | ||
| 16 | const Started = Schema.Struct({ | 17 | const Started = Schema.Struct({ |
| ... | @@ -186,12 +187,14 @@ server.listen(0, "127.0.0.1", () => { | ... | @@ -186,12 +187,14 @@ server.listen(0, "127.0.0.1", () => { |
| 186 | for (const [name, contents] of Object.entries(savedState)) { | 187 | for (const [name, contents] of Object.entries(savedState)) { |
| 187 | yield* fs.writeFileString(path.join(fixture, name), contents); | 188 | yield* fs.writeFileString(path.join(fixture, name), contents); |
| 188 | } | 189 | } |
| 189 | const script = buildRemoteStopScript({ | 190 | const script = buildRemoteStopScript( |
| 190 | alias: "fixture", | 191 | yield* remoteStateKey({ |
| 191 | hostname: "fixture", | 192 | alias: "fixture", |
| 192 | username: null, | 193 | hostname: "fixture", |
| 193 | port: null, | 194 | username: null, |
| 194 | }); | 195 | port: null, |
| 196 | }), | ||
| 197 | ); | ||
| 195 | // Redirect only the state directory. Never use the developer's SSH state. | 198 | // Redirect only the state directory. Never use the developer's SSH state. |
| 196 | const isolatedScript = script.replace( | 199 | const isolatedScript = script.replace( |
| 197 | /^STATE_DIR=.*$/mu, | 200 | /^STATE_DIR=.*$/mu, |
packages/ssh/src/tunnel.test.ts+8-13| ... | @@ -233,12 +233,7 @@ describe("ssh tunnel scripts", () => { | ... | @@ -233,12 +233,7 @@ describe("ssh tunnel scripts", () => { |
| 233 | }); | 233 | }); |
| 234 | 234 | ||
| 235 | it("uses the remote t3 runner for launch and pairing scripts", () => { | 235 | it("uses the remote t3 runner for launch and pairing scripts", () => { |
| 236 | const target = { | 236 | const stateKey = "711bc738002d72fd"; |
| 237 | alias: "devbox", | ||
| 238 | hostname: "devbox.example.com", | ||
| 239 | username: "julius", | ||
| 240 | port: 2222, | ||
| 241 | } as const; | ||
| 242 | const launch = SshTunnel.buildRemoteLaunchScript(ARCHIVE); | 237 | const launch = SshTunnel.buildRemoteLaunchScript(ARCHIVE); |
| 243 | const devLaunch = SshTunnel.buildRemoteLaunchScript({ | 238 | const devLaunch = SshTunnel.buildRemoteLaunchScript({ |
| 244 | ...NODE_SCRIPT, | 239 | ...NODE_SCRIPT, |
| ... | @@ -265,28 +260,28 @@ describe("ssh tunnel scripts", () => { | ... | @@ -265,28 +260,28 @@ describe("ssh tunnel scripts", () => { |
| 265 | assert.include(launch, "It wrote nothing to %s"); | 260 | assert.include(launch, "It wrote nothing to %s"); |
| 266 | assert.include(launch, "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'"); | 261 | assert.include(launch, "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'"); |
| 267 | assert.include( | 262 | assert.include( |
| 268 | SshTunnel.buildRemotePairingScript(target, ARCHIVE), | 263 | SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), |
| 269 | '"$RUNNER_FILE" auth pairing create --base-dir "$PAIRING_BASE_DIR" --json', | 264 | '"$RUNNER_FILE" auth pairing create --base-dir "$PAIRING_BASE_DIR" --json', |
| 270 | ); | 265 | ); |
| 271 | assert.include( | 266 | assert.include( |
| 272 | SshTunnel.buildRemotePairingScript(target, ARCHIVE), | 267 | SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), |
| 273 | 'PAIRING_BASE_DIR="$DEFAULT_SERVER_HOME"', | 268 | 'PAIRING_BASE_DIR="$DEFAULT_SERVER_HOME"', |
| 274 | ); | 269 | ); |
| 275 | assert.notInclude(SshTunnel.buildRemotePairingScript(target, ARCHIVE), "server-home"); | 270 | assert.notInclude(SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), "server-home"); |
| 276 | assert.include( | 271 | assert.include( |
| 277 | SshTunnel.buildRemotePairingScript(target, ARCHIVE), | 272 | SshTunnel.buildRemotePairingScript(stateKey, ARCHIVE), |
| 278 | "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'", | 273 | "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'", |
| 279 | ); | 274 | ); |
| 280 | assert.include( | 275 | assert.include( |
| 281 | SshTunnel.buildRemoteStopScript(target), | 276 | SshTunnel.buildRemoteStopScript(stateKey), |
| 282 | 'if [ "$REMOTE_MANAGED" != "external" ] && [ -n "$REMOTE_PID" ]', | 277 | 'if [ "$REMOTE_MANAGED" != "external" ] && [ -n "$REMOTE_PID" ]', |
| 283 | ); | 278 | ); |
| 284 | assert.include( | 279 | assert.include( |
| 285 | SshTunnel.buildRemoteStopScript(target), | 280 | SshTunnel.buildRemoteStopScript(stateKey), |
| 286 | 'kill "$REMOTE_PID" 2>/dev/null || true', | 281 | 'kill "$REMOTE_PID" 2>/dev/null || true', |
| 287 | ); | 282 | ); |
| 288 | assert.include( | 283 | assert.include( |
| 289 | SshTunnel.buildRemoteStopScript(target), | 284 | SshTunnel.buildRemoteStopScript(stateKey), |
| 290 | 'rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"', | 285 | 'rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"', |
| 291 | ); | 286 | ); |
| 292 | assert.include( | 287 | assert.include( |
packages/ssh/src/tunnel.ts+29-24| ... | @@ -12,6 +12,7 @@ import * as NetService from "@t3tools/shared/Net"; | ... | @@ -12,6 +12,7 @@ import * as NetService from "@t3tools/shared/Net"; |
| 12 | import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson"; | 12 | import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson"; |
| 13 | import { satisfiesSemverRange } from "@t3tools/shared/semver"; | 13 | import { satisfiesSemverRange } from "@t3tools/shared/semver"; |
| 14 | import * as Context from "effect/Context"; | 14 | import * as Context from "effect/Context"; |
| 15 | import * as Crypto from "effect/Crypto"; | ||
| 15 | import * as Effect from "effect/Effect"; | 16 | import * as Effect from "effect/Effect"; |
| 16 | import * as Exit from "effect/Exit"; | 17 | import * as Exit from "effect/Exit"; |
| 17 | import * as FileSystem from "effect/FileSystem"; | 18 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -98,6 +99,7 @@ interface SshTunnelEntry { | ... | @@ -98,6 +99,7 @@ interface SshTunnelEntry { |
| 98 | 99 | ||
| 99 | type SshEnvironmentEffectContext = | 100 | type SshEnvironmentEffectContext = |
| 100 | | ChildProcessSpawner.ChildProcessSpawner | 101 | | ChildProcessSpawner.ChildProcessSpawner |
| 102 | | Crypto.Crypto | ||
| 101 | | FileSystem.FileSystem | 103 | | FileSystem.FileSystem |
| 102 | | Path.Path | 104 | | Path.Path |
| 103 | | HttpClient.HttpClient | 105 | | HttpClient.HttpClient |
| ... | @@ -837,25 +839,22 @@ export function buildRemoteLaunchScript(input?: RemoteT3RunnerOptions): string { | ... | @@ -837,25 +839,22 @@ export function buildRemoteLaunchScript(input?: RemoteT3RunnerOptions): string { |
| 837 | }); | 839 | }); |
| 838 | } | 840 | } |
| 839 | 841 | ||
| 840 | export function buildRemotePairingScript( | 842 | export function buildRemotePairingScript(stateKey: string, input?: RemoteT3RunnerOptions): string { |
| 841 | target: DesktopSshEnvironmentTarget, | ||
| 842 | input?: RemoteT3RunnerOptions, | ||
| 843 | ): string { | ||
| 844 | return applyScriptPlaceholders(REMOTE_PAIRING_SCRIPT, { | 843 | return applyScriptPlaceholders(REMOTE_PAIRING_SCRIPT, { |
| 845 | T3_STATE_KEY: remoteStateKey(target), | 844 | T3_STATE_KEY: stateKey, |
| 846 | T3_RUNNER_SCRIPT: stripTrailingNewlines(buildRemoteT3RunnerScript(input)), | 845 | T3_RUNNER_SCRIPT: stripTrailingNewlines(buildRemoteT3RunnerScript(input)), |
| 847 | }); | 846 | }); |
| 848 | } | 847 | } |
| 849 | 848 | ||
| 850 | export function buildRemoteStopScript(target: DesktopSshEnvironmentTarget): string { | 849 | export function buildRemoteStopScript(stateKey: string): string { |
| 851 | return applyScriptPlaceholders(REMOTE_STOP_SCRIPT, { | 850 | return applyScriptPlaceholders(REMOTE_STOP_SCRIPT, { |
| 852 | T3_STATE_KEY: remoteStateKey(target), | 851 | T3_STATE_KEY: stateKey, |
| 853 | }); | 852 | }); |
| 854 | } | 853 | } |
| 855 | 854 | ||
| 856 | function buildRemoteLogTailScript(target: DesktopSshEnvironmentTarget): string { | 855 | function buildRemoteLogTailScript(stateKey: string): string { |
| 857 | return applyScriptPlaceholders(REMOTE_LOG_TAIL_SCRIPT, { | 856 | return applyScriptPlaceholders(REMOTE_LOG_TAIL_SCRIPT, { |
| 858 | T3_STATE_KEY: remoteStateKey(target), | 857 | T3_STATE_KEY: stateKey, |
| 859 | }); | 858 | }); |
| 860 | } | 859 | } |
| 861 | 860 | ||
| ... | @@ -867,15 +866,16 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo | ... | @@ -867,15 +866,16 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo |
| 867 | ): Effect.fn.Return< | 866 | ): Effect.fn.Return< |
| 868 | { readonly remotePort: number; readonly remoteServerKind: "external" | "managed" | null }, | 867 | { readonly remotePort: number; readonly remoteServerKind: "external" | "managed" | null }, |
| 869 | SshCommandError | SshInvalidTargetError | SshLaunchError, | 868 | SshCommandError | SshInvalidTargetError | SshLaunchError, |
| 870 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | 869 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path |
| 871 | > { | 870 | > { |
| 871 | const stateKey = yield* remoteStateKey(target); | ||
| 872 | yield* Effect.logInfo("ssh.remoteServer.launch.start", { | 872 | yield* Effect.logInfo("ssh.remoteServer.launch.start", { |
| 873 | ...sshTargetLogFields(target), | 873 | ...sshTargetLogFields(target), |
| 874 | ...sshRunnerLogFields(runner), | 874 | ...sshRunnerLogFields(runner), |
| 875 | stateKey: remoteStateKey(target), | 875 | stateKey, |
| 876 | }); | 876 | }); |
| 877 | const result = yield* runSshCommand(target, { | 877 | const result = yield* runSshCommand(target, { |
| 878 | remoteCommandArgs: ["sh", "-l", "-s", "--", remoteStateKey(target)], | 878 | remoteCommandArgs: ["sh", "-l", "-s", "--", stateKey], |
| 879 | stdin: buildRemoteLaunchScript(runner), | 879 | stdin: buildRemoteLaunchScript(runner), |
| 880 | timeoutMs: isNodeScriptRunner(runner) | 880 | timeoutMs: isNodeScriptRunner(runner) |
| 881 | ? REMOTE_LAUNCH_TIMEOUT_MS | 881 | ? REMOTE_LAUNCH_TIMEOUT_MS |
| ... | @@ -910,7 +910,7 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo | ... | @@ -910,7 +910,7 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo |
| 910 | ...sshTargetLogFields(target), | 910 | ...sshTargetLogFields(target), |
| 911 | remotePort: parsed.remotePort, | 911 | remotePort: parsed.remotePort, |
| 912 | remoteServerKind: parsed.serverKind ?? null, | 912 | remoteServerKind: parsed.serverKind ?? null, |
| 913 | stateKey: remoteStateKey(target), | 913 | stateKey, |
| 914 | }); | 914 | }); |
| 915 | return { | 915 | return { |
| 916 | remotePort: parsed.remotePort, | 916 | remotePort: parsed.remotePort, |
| ... | @@ -928,15 +928,16 @@ export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingT | ... | @@ -928,15 +928,16 @@ export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingT |
| 928 | readonly credential: string; | 928 | readonly credential: string; |
| 929 | }, | 929 | }, |
| 930 | SshCommandError | SshInvalidTargetError | SshPairingError, | 930 | SshCommandError | SshInvalidTargetError | SshPairingError, |
| 931 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | 931 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path |
| 932 | > { | 932 | > { |
| 933 | const stateKey = yield* remoteStateKey(target); | ||
| 933 | yield* Effect.logDebug("ssh.remoteServer.pairingToken.start", { | 934 | yield* Effect.logDebug("ssh.remoteServer.pairingToken.start", { |
| 934 | ...sshTargetLogFields(target), | 935 | ...sshTargetLogFields(target), |
| 935 | stateKey: remoteStateKey(target), | 936 | stateKey, |
| 936 | }); | 937 | }); |
| 937 | const result = yield* runSshCommand(target, { | 938 | const result = yield* runSshCommand(target, { |
| 938 | remoteCommandArgs: ["sh", "-s"], | 939 | remoteCommandArgs: ["sh", "-s"], |
| 939 | stdin: buildRemotePairingScript(target, runner), | 940 | stdin: buildRemotePairingScript(stateKey, runner), |
| 940 | // Pairing may be the first command on a cold remote, so it can install | 941 | // Pairing may be the first command on a cold remote, so it can install |
| 941 | // the archive on the way. | 942 | // the archive on the way. |
| 942 | ...(isNodeScriptRunner(runner) ? {} : { timeoutMs: REMOTE_ARCHIVE_LAUNCH_TIMEOUT_MS }), | 943 | ...(isNodeScriptRunner(runner) ? {} : { timeoutMs: REMOTE_ARCHIVE_LAUNCH_TIMEOUT_MS }), |
| ... | @@ -968,7 +969,7 @@ export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingT | ... | @@ -968,7 +969,7 @@ export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingT |
| 968 | } | 969 | } |
| 969 | yield* Effect.logDebug("ssh.remoteServer.pairingToken.created", { | 970 | yield* Effect.logDebug("ssh.remoteServer.pairingToken.created", { |
| 970 | ...sshTargetLogFields(target), | 971 | ...sshTargetLogFields(target), |
| 971 | stateKey: remoteStateKey(target), | 972 | stateKey, |
| 972 | }); | 973 | }); |
| 973 | return { | 974 | return { |
| 974 | credential: parsed.credential, | 975 | credential: parsed.credential, |
| ... | @@ -981,22 +982,23 @@ const stopRemoteServer = Effect.fn("ssh/tunnel.stopRemoteServer")(function* ( | ... | @@ -981,22 +982,23 @@ const stopRemoteServer = Effect.fn("ssh/tunnel.stopRemoteServer")(function* ( |
| 981 | ): Effect.fn.Return< | 982 | ): Effect.fn.Return< |
| 982 | void, | 983 | void, |
| 983 | SshCommandError | SshInvalidTargetError, | 984 | SshCommandError | SshInvalidTargetError, |
| 984 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | 985 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path |
| 985 | > { | 986 | > { |
| 987 | const stateKey = yield* remoteStateKey(target); | ||
| 986 | yield* Effect.logInfo("ssh.remoteServer.stop.start", { | 988 | yield* Effect.logInfo("ssh.remoteServer.stop.start", { |
| 987 | ...sshTargetLogFields(target), | 989 | ...sshTargetLogFields(target), |
| 988 | stateKey: remoteStateKey(target), | 990 | stateKey, |
| 989 | }); | 991 | }); |
| 990 | yield* runSshCommand(target, { | 992 | yield* runSshCommand(target, { |
| 991 | remoteCommandArgs: ["sh", "-s"], | 993 | remoteCommandArgs: ["sh", "-s"], |
| 992 | stdin: buildRemoteStopScript(target), | 994 | stdin: buildRemoteStopScript(stateKey), |
| 993 | ...(input?.authSecret === undefined ? {} : { authSecret: input.authSecret }), | 995 | ...(input?.authSecret === undefined ? {} : { authSecret: input.authSecret }), |
| 994 | ...(input?.batchMode === undefined ? {} : { batchMode: input.batchMode }), | 996 | ...(input?.batchMode === undefined ? {} : { batchMode: input.batchMode }), |
| 995 | ...(input?.interactiveAuth === undefined ? {} : { interactiveAuth: input.interactiveAuth }), | 997 | ...(input?.interactiveAuth === undefined ? {} : { interactiveAuth: input.interactiveAuth }), |
| 996 | }); | 998 | }); |
| 997 | yield* Effect.logInfo("ssh.remoteServer.stop.succeeded", { | 999 | yield* Effect.logInfo("ssh.remoteServer.stop.succeeded", { |
| 998 | ...sshTargetLogFields(target), | 1000 | ...sshTargetLogFields(target), |
| 999 | stateKey: remoteStateKey(target), | 1001 | stateKey, |
| 1000 | }); | 1002 | }); |
| 1001 | }); | 1003 | }); |
| 1002 | 1004 | ||
| ... | @@ -1006,11 +1008,11 @@ const readRemoteServerLogTail = Effect.fn("ssh/tunnel.readRemoteServerLogTail")( | ... | @@ -1006,11 +1008,11 @@ const readRemoteServerLogTail = Effect.fn("ssh/tunnel.readRemoteServerLogTail")( |
| 1006 | ): Effect.fn.Return< | 1008 | ): Effect.fn.Return< |
| 1007 | string, | 1009 | string, |
| 1008 | SshCommandError | SshInvalidTargetError, | 1010 | SshCommandError | SshInvalidTargetError, |
| 1009 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | 1011 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path |
| 1010 | > { | 1012 | > { |
| 1011 | const result = yield* runSshCommand(target, { | 1013 | const result = yield* runSshCommand(target, { |
| 1012 | remoteCommandArgs: ["sh", "-s"], | 1014 | remoteCommandArgs: ["sh", "-s"], |
| 1013 | stdin: buildRemoteLogTailScript(target), | 1015 | stdin: buildRemoteLogTailScript(yield* remoteStateKey(target)), |
| 1014 | timeoutMs: 10_000, | 1016 | timeoutMs: 10_000, |
| 1015 | ...(input?.authSecret === undefined ? {} : { authSecret: input.authSecret }), | 1017 | ...(input?.authSecret === undefined ? {} : { authSecret: input.authSecret }), |
| 1016 | ...(input?.batchMode === undefined ? {} : { batchMode: input.batchMode }), | 1018 | ...(input?.batchMode === undefined ? {} : { batchMode: input.batchMode }), |
| ... | @@ -1109,6 +1111,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: | ... | @@ -1109,6 +1111,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: |
| 1109 | SshTunnelEntry, | 1111 | SshTunnelEntry, |
| 1110 | SshCommandError | SshInvalidTargetError | SshReadinessError, | 1112 | SshCommandError | SshInvalidTargetError | SshReadinessError, |
| 1111 | | ChildProcessSpawner.ChildProcessSpawner | 1113 | | ChildProcessSpawner.ChildProcessSpawner |
| 1114 | | Crypto.Crypto | ||
| 1112 | | FileSystem.FileSystem | 1115 | | FileSystem.FileSystem |
| 1113 | | Path.Path | 1116 | | Path.Path |
| 1114 | | HttpClient.HttpClient | 1117 | | HttpClient.HttpClient |
| ... | @@ -1538,6 +1541,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma | ... | @@ -1538,6 +1541,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma |
| 1538 | ); | 1541 | ); |
| 1539 | tunnels.set(input.key, tunnelEntry); | 1542 | tunnels.set(input.key, tunnelEntry); |
| 1540 | const spawnerService = yield* ChildProcessSpawner.ChildProcessSpawner; | 1543 | const spawnerService = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 1544 | const cryptoService = yield* Crypto.Crypto; | ||
| 1541 | const fileSystemService = yield* FileSystem.FileSystem; | 1545 | const fileSystemService = yield* FileSystem.FileSystem; |
| 1542 | const pathService = yield* Path.Path; | 1546 | const pathService = yield* Path.Path; |
| 1543 | yield* Scope.addFinalizer( | 1547 | yield* Scope.addFinalizer( |
| ... | @@ -1577,6 +1581,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma | ... | @@ -1577,6 +1581,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma |
| 1577 | }, | 1581 | }, |
| 1578 | ).pipe( | 1582 | ).pipe( |
| 1579 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawnerService), | 1583 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawnerService), |
| 1584 | Effect.provideService(Crypto.Crypto, cryptoService), | ||
| 1580 | Effect.provideService(FileSystem.FileSystem, fileSystemService), | 1585 | Effect.provideService(FileSystem.FileSystem, fileSystemService), |
| 1581 | Effect.provideService(Path.Path, pathService), | 1586 | Effect.provideService(Path.Path, pathService), |
| 1582 | ); | 1587 | ); |
| ... | @@ -1757,7 +1762,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma | ... | @@ -1757,7 +1762,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma |
| 1757 | }); | 1762 | }); |
| 1758 | 1763 | ||
| 1759 | /** | 1764 | /** |
| 1760 | * @effect-expect-leaking ChildProcessSpawner | FileSystem | HttpClient | NetService | Path | SshPasswordPrompt | 1765 | * @effect-expect-leaking ChildProcessSpawner | Crypto | FileSystem | HttpClient | NetService | Path | SshPasswordPrompt |
| 1761 | */ | 1766 | */ |
| 1762 | export class SshEnvironmentManager extends Context.Service< | 1767 | export class SshEnvironmentManager extends Context.Service< |
| 1763 | SshEnvironmentManager, | 1768 | SshEnvironmentManager, |
pnpm-lock.yaml+37-34| ... | @@ -13,8 +13,8 @@ catalogs: | ... | @@ -13,8 +13,8 @@ catalogs: |
| 13 | specifier: 4.0.1 | 13 | specifier: 4.0.1 |
| 14 | version: 4.0.1 | 14 | version: 4.0.1 |
| 15 | '@effect/tsgo': | 15 | '@effect/tsgo': |
| 16 | specifier: 0.41.0 | 16 | specifier: 0.46.1 |
| 17 | version: 0.41.0 | 17 | version: 0.46.1 |
| 18 | '@legendapp/list': | 18 | '@legendapp/list': |
| 19 | specifier: 3.3.5 | 19 | specifier: 3.3.5 |
| 20 | version: 3.3.5 | 20 | version: 3.3.5 |
| ... | @@ -131,7 +131,7 @@ importers: | ... | @@ -131,7 +131,7 @@ importers: |
| 131 | version: 1.1.0 | 131 | version: 1.1.0 |
| 132 | '@effect/tsgo': | 132 | '@effect/tsgo': |
| 133 | specifier: 'catalog:' | 133 | specifier: 'catalog:' |
| 134 | version: 0.41.0 | 134 | version: 0.46.1 |
| 135 | '@shadcn/lint': | 135 | '@shadcn/lint': |
| 136 | specifier: 0.1.5 | 136 | specifier: 0.1.5 |
| 137 | version: 0.1.5 | 137 | version: 0.1.5 |
| ... | @@ -540,6 +540,9 @@ importers: | ... | @@ -540,6 +540,9 @@ importers: |
| 540 | '@napi-rs/keyring': | 540 | '@napi-rs/keyring': |
| 541 | specifier: ^1.3.0 | 541 | specifier: ^1.3.0 |
| 542 | version: 1.3.0 | 542 | version: 1.3.0 |
| 543 | '@noble/hashes': | ||
| 544 | specifier: 'catalog:' | ||
| 545 | version: 1.8.0 | ||
| 543 | '@opencode-ai/sdk': | 546 | '@opencode-ai/sdk': |
| 544 | specifier: ^1.3.15 | 547 | specifier: ^1.3.15 |
| 545 | version: 1.15.13 | 548 | version: 1.15.13 |
| ... | @@ -2384,43 +2387,43 @@ packages: | ... | @@ -2384,43 +2387,43 @@ packages: |
| 2384 | peerDependencies: | 2387 | peerDependencies: |
| 2385 | effect: 4.0.1 | 2388 | effect: 4.0.1 |
| 2386 | 2389 | ||
| 2387 | '@effect/tsgo-darwin-arm64@0.41.0': | 2390 | '@effect/tsgo-darwin-arm64@0.46.1': |
| 2388 | resolution: {integrity: sha512-3iEPtcHF72yDjv7T5YpnX+Io1LDLywJb1oGG3Fp/Fth4xmqiT1Vacyz5wnPCfEWGQ6CyrlyCZSSjUxPyJ70+DQ==} | 2391 | resolution: {integrity: sha512-ErehNqI9p8me4XKO8N4bzdDWgBbSNlJ3CIZGfXoAW6Fo4Oo7x4ZbZV7KNsMSn9aJSiUUOQUoXjGBdslU5OYMdw==} |
| 2389 | cpu: [arm64] | 2392 | cpu: [arm64] |
| 2390 | os: [darwin] | 2393 | os: [darwin] |
| 2391 | 2394 | ||
| 2392 | '@effect/tsgo-darwin-x64@0.41.0': | 2395 | '@effect/tsgo-darwin-x64@0.46.1': |
| 2393 | resolution: {integrity: sha512-rrVVNacJ/Qh8DfdfgNJDuzcVmR3xPnqes3z+F1kio99umJwPIRB8iPWRAY0SrO+Y84A/y2SSb0AjMGqA5V3Aiw==} | 2396 | resolution: {integrity: sha512-zsQI1gjGMYF/C2FWDUwbStBAf0tJDVgXeWwTAZ+VUiNY06fR1SrZHK0L+22PE49Nim3biWK2h+XXRFVzql+ojg==} |
| 2394 | cpu: [x64] | 2397 | cpu: [x64] |
| 2395 | os: [darwin] | 2398 | os: [darwin] |
| 2396 | 2399 | ||
| 2397 | '@effect/tsgo-linux-arm64@0.41.0': | 2400 | '@effect/tsgo-linux-arm64@0.46.1': |
| 2398 | resolution: {integrity: sha512-RVI+7pG3tP56LfH06a2aq4KLp2RQ0HohW8mmoGmrBPyNEL9DnzFEGB98ZoMJ+ERYUJoIp74Jy3nKRVbnzVhxFw==} | 2401 | resolution: {integrity: sha512-e283BZEQC3mJpk1N7vdUhoF/aYHc4YwCE/fwz/80d11OSvG35U+c49grxS7V7+dAl2ld6QiT8D1rcL8PYu8DLQ==} |
| 2399 | cpu: [arm64] | 2402 | cpu: [arm64] |
| 2400 | os: [linux] | 2403 | os: [linux] |
| 2401 | 2404 | ||
| 2402 | '@effect/tsgo-linux-arm@0.41.0': | 2405 | '@effect/tsgo-linux-arm@0.46.1': |
| 2403 | resolution: {integrity: sha512-JddS91IaupLa22oc4YQiYppe7iAZAEyx/2iR9sAAAhpVanWACsIAcyUy4RJrs8sPLLoLsZQaKQa+9rbQO1FLsg==} | 2406 | resolution: {integrity: sha512-Y12wOcJ6HgwiCgg/oDQH4tdRQKRsWnul/KjCOwMxzVT4kO+ndczu1sKUZT+qIxV7qNk4Sq+uSWKEnFBA3ETI8w==} |
| 2404 | cpu: [arm] | 2407 | cpu: [arm] |
| 2405 | os: [linux] | 2408 | os: [linux] |
| 2406 | 2409 | ||
| 2407 | '@effect/tsgo-linux-x64@0.41.0': | 2410 | '@effect/tsgo-linux-x64@0.46.1': |
| 2408 | resolution: {integrity: sha512-U3+kMDVe2Opa5mxbN4B6PgOizWx5B4LXLN98CZDUl2vYtRGfwrRRhKQiB+de3ELaZ0MuDjPlCvU7S2mXQ8UiuQ==} | 2411 | resolution: {integrity: sha512-NYGBC3Scfo9yZJfs7cg0KMcHgtukJ4+xff18nqIrON9Zzc/qD6w1AwEwYKYCsbai5hTQBb3VLIWmHM8BJ54WJg==} |
| 2409 | cpu: [x64] | 2412 | cpu: [x64] |
| 2410 | os: [linux] | 2413 | os: [linux] |
| 2411 | 2414 | ||
| 2412 | '@effect/tsgo-win32-arm64@0.41.0': | 2415 | '@effect/tsgo-win32-arm64@0.46.1': |
| 2413 | resolution: {integrity: sha512-H6/xkn+B4B63OPKc0UAzQQFvLs0MXKigiD9dtvQeCU7czWGztOQILuLkyHnM1Dlc+uy4cJ5eXHiMXd+lwl+E8g==} | 2416 | resolution: {integrity: sha512-MK93SN9qjowrCjch+2wGGncd6Y7KgDyJvLKVVFCJV8cPpHoL3xDCpxFCGqQWe120mVpLmnzbM2evKdh0M5l5PA==} |
| 2414 | cpu: [arm64] | 2417 | cpu: [arm64] |
| 2415 | os: [win32] | 2418 | os: [win32] |
| 2416 | 2419 | ||
| 2417 | '@effect/tsgo-win32-x64@0.41.0': | 2420 | '@effect/tsgo-win32-x64@0.46.1': |
| 2418 | resolution: {integrity: sha512-vRN/Mhi381xEGrvV68IN/VO4Lc0pnlVQzHSVmNCJXLwGLmJUvuvPoyZ7Lv4wwHRX9iZe8Rch21xUGu5jSQtUSw==} | 2421 | resolution: {integrity: sha512-hlRbeNlp0N/dfZZFl+xVzT3XfHYqPmkXzCadR6e5yoe/vyA3O9RnO5UrOBHsXvL2RTE6hrh8bRfx2Uu6g3LL5w==} |
| 2419 | cpu: [x64] | 2422 | cpu: [x64] |
| 2420 | os: [win32] | 2423 | os: [win32] |
| 2421 | 2424 | ||
| 2422 | '@effect/tsgo@0.41.0': | 2425 | '@effect/tsgo@0.46.1': |
| 2423 | resolution: {integrity: sha512-C/U7lFM0AXsfpqRilDOgwu+llBhAo8bcdIlzgbRWf1LWlU4KZKB2UsUvBPL3qPnm+wmbCvQLeTQc4BjV9oJrcg==} | 2426 | resolution: {integrity: sha512-DMcBCg5nWLufejRlyqFEkXmbtaLUJ56NKny6L1LTtimvz0U3VNj9H6ZUXV4WeeWvJzVyL0y7G2wsCBAFUgy1jw==} |
| 2424 | hasBin: true | 2427 | hasBin: true |
| 2425 | 2428 | ||
| 2426 | '@effect/vitest@4.0.1': | 2429 | '@effect/vitest@4.0.1': |
| ... | @@ -13055,36 +13058,36 @@ snapshots: | ... | @@ -13055,36 +13058,36 @@ snapshots: |
| 13055 | dependencies: | 13058 | dependencies: |
| 13056 | effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) | 13059 | effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) |
| 13057 | 13060 | ||
| 13058 | '@effect/tsgo-darwin-arm64@0.41.0': | 13061 | '@effect/tsgo-darwin-arm64@0.46.1': |
| 13059 | optional: true | 13062 | optional: true |
| 13060 | 13063 | ||
| 13061 | '@effect/tsgo-darwin-x64@0.41.0': | 13064 | '@effect/tsgo-darwin-x64@0.46.1': |
| 13062 | optional: true | 13065 | optional: true |
| 13063 | 13066 | ||
| 13064 | '@effect/tsgo-linux-arm64@0.41.0': | 13067 | '@effect/tsgo-linux-arm64@0.46.1': |
| 13065 | optional: true | 13068 | optional: true |
| 13066 | 13069 | ||
| 13067 | '@effect/tsgo-linux-arm@0.41.0': | 13070 | '@effect/tsgo-linux-arm@0.46.1': |
| 13068 | optional: true | 13071 | optional: true |
| 13069 | 13072 | ||
| 13070 | '@effect/tsgo-linux-x64@0.41.0': | 13073 | '@effect/tsgo-linux-x64@0.46.1': |
| 13071 | optional: true | 13074 | optional: true |
| 13072 | 13075 | ||
| 13073 | '@effect/tsgo-win32-arm64@0.41.0': | 13076 | '@effect/tsgo-win32-arm64@0.46.1': |
| 13074 | optional: true | 13077 | optional: true |
| 13075 | 13078 | ||
| 13076 | '@effect/tsgo-win32-x64@0.41.0': | 13079 | '@effect/tsgo-win32-x64@0.46.1': |
| 13077 | optional: true | 13080 | optional: true |
| 13078 | 13081 | ||
| 13079 | '@effect/tsgo@0.41.0': | 13082 | '@effect/tsgo@0.46.1': |
| 13080 | optionalDependencies: | 13083 | optionalDependencies: |
| 13081 | '@effect/tsgo-darwin-arm64': 0.41.0 | 13084 | '@effect/tsgo-darwin-arm64': 0.46.1 |
| 13082 | '@effect/tsgo-darwin-x64': 0.41.0 | 13085 | '@effect/tsgo-darwin-x64': 0.46.1 |
| 13083 | '@effect/tsgo-linux-arm': 0.41.0 | 13086 | '@effect/tsgo-linux-arm': 0.46.1 |
| 13084 | '@effect/tsgo-linux-arm64': 0.41.0 | 13087 | '@effect/tsgo-linux-arm64': 0.46.1 |
| 13085 | '@effect/tsgo-linux-x64': 0.41.0 | 13088 | '@effect/tsgo-linux-x64': 0.46.1 |
| 13086 | '@effect/tsgo-win32-arm64': 0.41.0 | 13089 | '@effect/tsgo-win32-arm64': 0.46.1 |
| 13087 | '@effect/tsgo-win32-x64': 0.41.0 | 13090 | '@effect/tsgo-win32-x64': 0.46.1 |
| 13088 | 13091 | ||
| 13089 | '@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)': | 13092 | '@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)': |
| 13090 | dependencies: | 13093 | dependencies: |
pnpm-workspace.yaml+1-1| ... | @@ -36,7 +36,7 @@ catalog: | ... | @@ -36,7 +36,7 @@ catalog: |
| 36 | "@effect/platform-node-shared": 4.0.1 | 36 | "@effect/platform-node-shared": 4.0.1 |
| 37 | "@effect/sql-pg": 4.0.1 | 37 | "@effect/sql-pg": 4.0.1 |
| 38 | "@effect/sql-sqlite-do": 4.0.1 | 38 | "@effect/sql-sqlite-do": 4.0.1 |
| 39 | "@effect/tsgo": 0.41.0 | 39 | "@effect/tsgo": 0.46.1 |
| 40 | "@effect/vitest": 4.0.1 | 40 | "@effect/vitest": 4.0.1 |
| 41 | "@legendapp/list": 3.3.5 | 41 | "@legendapp/list": 3.3.5 |
| 42 | "@noble/curves": 1.9.1 | 42 | "@noble/curves": 1.9.1 |
tsconfig.base.json-1| ... | @@ -23,7 +23,6 @@ | ... | @@ -23,7 +23,6 @@ |
| 23 | "name": "@effect/language-service", | 23 | "name": "@effect/language-service", |
| 24 | "namespaceImportPackages": ["@effect/platform-node", "effect"], | 24 | "namespaceImportPackages": ["@effect/platform-node", "effect"], |
| 25 | "diagnosticSeverity": { | 25 | "diagnosticSeverity": { |
| 26 | "importFromBarrel": "error", | ||
| 27 | "anyUnknownInErrorContext": "error", | 26 | "anyUnknownInErrorContext": "error", |
| 28 | "unsafeEffectTypeAssertion": "error", | 27 | "unsafeEffectTypeAssertion": "error", |
| 29 | "instanceOfSchema": "error", | 28 | "instanceOfSchema": "error", |