Unified · Split
| @@ -40,6 +40,8 @@ · 39 unmodified lines | ||
| 40 | 40 | |
| 41 | 41 | // Monorepo-specific rules. |
| 42 | 42 | "executor-monorepo/no-cross-workspace-relative-imports": "error", |
| 43 | + "executor-monorepo/no-raw-effect-fail-errors": "error", | |
| 44 | + "executor-monorepo/no-yield-effect-fail": "error", | |
| 43 | 45 | "executor-monorepo/no-workspace-src-imports": "warn" |
| 44 | 46 | }, |
| 45 | 47 | |
| @@ -4,6 +4,7 @@ import { spawn } from "node:child_process"; · 3 unmodified lines | ||
| 4 | 4 | import { dirname, resolve } from "node:path"; |
| 5 | 5 | import { createInterface } from "node:readline/promises"; |
| 6 | 6 | import { fileURLToPath } from "node:url"; |
| 7 | +import { FileSystem } from "@effect/platform"; | |
| 7 | 8 | import { Args, Command, Options } from "@effect/cli"; |
| 8 | 9 | import { |
| 9 | 10 | NodeFileSystem, |
| @@ -52,6 +53,11 @@ import { · 42 unmodified lines | ||
| 52 | 53 | parseInteractionPayload, |
| 53 | 54 | } from "./pending-interaction-output"; |
| 54 | 55 | import { decideInteractionHandling } from "./interaction-handling"; |
| 56 | +import { | |
| 57 | + executorAppEffectError, | |
| 58 | + type LocalServerReachabilityTimeoutError, | |
| 59 | + localServerReachabilityTimeoutError, | |
| 60 | +} from "../effect-errors"; | |
| 55 | 61 | |
| 56 | 62 | const toError = (cause: unknown): Error => |
| 57 | 63 | cause instanceof Error ? cause : new Error(String(cause)); |
| @@ -138,9 +144,7 @@ const readCode = (input: { · 80 unmodified lines | ||
| 138 | 144 | } |
| 139 | 145 | } |
| 140 | 146 | |
| 141 | − return yield* Effect.fail( | |
| 142 | − new Error("Provide code as a positional argument, use --file, or pipe code over stdin."), | |
| 143 | − ); | |
| 147 | + return yield* executorAppEffectError("cli/main", "Provide code as a positional argument, use --file, or pipe code over stdin."); | |
| 144 | 148 | }); |
| 145 | 149 | |
| 146 | 150 | const getBootstrapClient = (baseUrl: string = DEFAULT_SERVER_BASE_URL) => |
| @@ -471,36 +475,42 @@ const isPidRunning = (pid: number): boolean => { · 324 unmodified lines | ||
| 471 | 475 | }; |
| 472 | 476 | |
| 473 | 477 | |
| 474 | −const readServerLogTail = async ( | |
| 478 | +const readServerLogTail = ( | |
| 475 | 479 | logFile: string = DEFAULT_SERVER_LOG_FILE, |
| 476 | 480 | maxLines: number = 40, |
| 477 | 481 | maxChars: number = 6000, |
| 478 | −): Promise<string | null> => { | |
| 479 | − try { | |
| 480 | − const contents = await readFile(logFile, "utf8"); | |
| 482 | +): Effect.Effect<string | null, never, FileSystem.FileSystem> => | |
| 483 | + Effect.gen(function* () { | |
| 484 | + const fs = yield* FileSystem.FileSystem; | |
| 485 | + const contents = yield* fs.readFileString(logFile, "utf8").pipe( | |
| 486 | + Effect.catchAll(() => Effect.succeed<string | null>(null)), | |
| 487 | + ); | |
| 488 | + | |
| 489 | + if (contents === null) { | |
| 490 | + return null; | |
| 491 | + } | |
| 492 | + | |
| 481 | 493 | const lines = contents.split(/\r?\n/u).filter((line) => line.length > 0); |
| 482 | 494 | const tail = lines.slice(-maxLines).join("\n"); |
| 483 | 495 | return tail.length > maxChars ? tail.slice(-maxChars) : tail; |
| 484 | − } catch { | |
| 485 | − return null; | |
| 486 | − } | |
| 487 | −}; | |
| 488 | − | |
| 489 | −const buildReachabilityTimeoutError = async ( | |
| 490 | − baseUrl: string, | |
| 491 | − expected: boolean, | |
| 492 | − logFile: string = DEFAULT_SERVER_LOG_FILE, | |
| 493 | −): Promise<Error> => { | |
| 494 | − const action = expected ? "start" : "shutdown"; | |
| 495 | − const prefix = `Timed out waiting for local executor server ${action} at ${baseUrl}`; | |
| 496 | − const logTail = await readServerLogTail(logFile); | |
| 496 | + }); | |
| 497 | 497 | |
| 498 | − if (!logTail) { | |
| 499 | − return new Error(`${prefix}\n\nDaemon log: ${logFile}`); | |
| 500 | − } | |
| 498 | +const failReachabilityTimeout = (input: { | |
| 499 | + baseUrl: string; | |
| 500 | + expected: boolean; | |
| 501 | + logFile?: string; | |
| 502 | +}): Effect.Effect<never, LocalServerReachabilityTimeoutError, FileSystem.FileSystem> => | |
| 503 | + Effect.gen(function* () { | |
| 504 | + const logFile = input.logFile ?? DEFAULT_SERVER_LOG_FILE; | |
| 505 | + const logTail = yield* readServerLogTail(logFile); | |
| 501 | 506 | |
| 502 | − return new Error(`${prefix}\n\nRecent daemon log (${logFile}):\n${logTail}`); | |
| 503 | −}; | |
| 507 | + return yield* localServerReachabilityTimeoutError({ | |
| 508 | + baseUrl: input.baseUrl, | |
| 509 | + expected: input.expected, | |
| 510 | + logFile, | |
| 511 | + logTail, | |
| 512 | + }); | |
| 513 | + }); | |
| 504 | 514 | |
| 505 | 515 | const waitForReachability = (baseUrl: string, expected: boolean) => |
| 506 | 516 | Effect.gen(function* () { |
| @@ -513,12 +523,7 @@ const waitForReachability = (baseUrl: string, expected: boolean) => · 6 unmodified lines | ||
| 513 | 523 | yield* sleep(SERVER_POLL_INTERVAL_MS); |
| 514 | 524 | } |
| 515 | 525 | |
| 516 | − const error = yield* Effect.tryPromise({ | |
| 517 | − try: () => buildReachabilityTimeoutError(baseUrl, expected), | |
| 518 | − catch: toError, | |
| 519 | − }); | |
| 520 | − | |
| 521 | − return yield* Effect.fail(error); | |
| 526 | + return yield* failReachabilityTimeout({ baseUrl, expected }); | |
| 522 | 527 | }); |
| 523 | 528 | |
| 524 | 529 | type LocalServerStatus = { |
| @@ -668,7 +673,7 @@ const stopServer = (baseUrl: string) => · 143 unmodified lines | ||
| 668 | 673 | Effect.catchAll(() => |
| 669 | 674 | Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe( |
| 670 | 675 | Effect.ignore, |
| 671 | − Effect.zipRight(Effect.fail(new Error(`Timed out stopping local executor server pid ${pid}`))), | |
| 676 | + Effect.zipRight(Effect.fail(executorAppEffectError("cli/main", `Timed out stopping local executor server pid ${pid}`))), | |
| 672 | 677 | ), |
| 673 | 678 | ), |
| 674 | 679 | ); |
| @@ -9,6 +9,7 @@ import * as Effect from "effect/Effect"; · 8 unmodified lines | ||
| 9 | 9 | import * as Layer from "effect/Layer"; |
| 10 | 10 | |
| 11 | 11 | import { buildDistributionPackage } from "./artifact"; |
| 12 | +import { executorAppEffectError } from "../effect-errors"; | |
| 12 | 13 | |
| 13 | 14 | type CommandResult = { |
| 14 | 15 | readonly stdout: string; |
| @@ -96,7 +97,7 @@ const runCommand = (input: { · 81 unmodified lines | ||
| 96 | 97 | return; |
| 97 | 98 | } |
| 98 | 99 | |
| 99 | − resume(Effect.fail(new Error( | |
| 100 | + resume(Effect.fail(executorAppEffectError("distribution/harness", | |
| 100 | 101 | [ |
| 101 | 102 | `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`, |
| 102 | 103 | stdout.length > 0 ? `stdout:\n${stdout.trim()}` : null, |
| @@ -161,7 +162,7 @@ const packPackage = (packageDir: string, outputDir: string) => · 58 unmodified lines | ||
| 161 | 162 | |
| 162 | 163 | if (!tarballName) { |
| 163 | 164 | return Effect.fail( |
| 164 | − new Error(`Unable to determine tarball name from npm pack output: ${result.stdout}`), | |
| 165 | + executorAppEffectError("distribution/harness", `Unable to determine tarball name from npm pack output: ${result.stdout}`), | |
| 165 | 166 | ); |
| 166 | 167 | } |
| 167 | 168 | |
| @@ -0,0 +1,40 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class ExecutorAppEffectError extends Data.TaggedError( | |
| 4 | + "ExecutorAppEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const executorAppEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new ExecutorAppEffectError({ module, message }); | |
| 14 | + | |
| 15 | +export class LocalServerReachabilityTimeoutError extends Data.TaggedError( | |
| 16 | + "LocalServerReachabilityTimeoutError", | |
| 17 | +)<{ | |
| 18 | + readonly baseUrl: string; | |
| 19 | + readonly action: "start" | "shutdown"; | |
| 20 | + readonly logFile: string; | |
| 21 | + readonly logTail: string | null; | |
| 22 | + readonly message: string; | |
| 23 | +}> {} | |
| 24 | + | |
| 25 | +export const localServerReachabilityTimeoutError = (input: { | |
| 26 | + baseUrl: string; | |
| 27 | + expected: boolean; | |
| 28 | + logFile: string; | |
| 29 | + logTail: string | null; | |
| 30 | +}) => | |
| 31 | + new LocalServerReachabilityTimeoutError({ | |
| 32 | + baseUrl: input.baseUrl, | |
| 33 | + action: input.expected ? "start" : "shutdown", | |
| 34 | + logFile: input.logFile, | |
| 35 | + logTail: input.logTail, | |
| 36 | + message: | |
| 37 | + input.logTail === null | |
| 38 | + ? `Timed out waiting for local executor server ${input.expected ? "start" : "shutdown"} at ${input.baseUrl}\n\nDaemon log: ${input.logFile}` | |
| 39 | + : `Timed out waiting for local executor server ${input.expected ? "start" : "shutdown"} at ${input.baseUrl}\n\nRecent daemon log (${input.logFile}):\n${input.logTail}`, | |
| 40 | + }); | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class McpOAuthEffectError extends Data.TaggedError( | |
| 4 | + "McpOAuthEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const mcpOAuthEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new McpOAuthEffectError({ module, message }); | |
| @@ -8,6 +8,7 @@ import type { · 7 unmodified lines | ||
| 8 | 8 | OAuthTokens, |
| 9 | 9 | } from "@modelcontextprotocol/sdk/shared/auth.js"; |
| 10 | 10 | import * as Effect from "effect/Effect"; |
| 11 | +import { mcpOAuthEffectError } from "./effect-errors"; | |
| 11 | 12 | |
| 12 | 13 | type JsonValue = |
| 13 | 14 | | string |
| @@ -122,9 +123,7 @@ export const startMcpOAuthAuthorization = (input: { · 108 unmodified lines | ||
| 122 | 123 | }); |
| 123 | 124 | |
| 124 | 125 | if (result !== "REDIRECT" || !captured.authorizationUrl || !captured.codeVerifier) { |
| 125 | − return yield* Effect.fail( | |
| 126 | − new Error("OAuth flow did not produce an authorization redirect"), | |
| 127 | − ); | |
| 126 | + return yield* mcpOAuthEffectError("index", "OAuth flow did not produce an authorization redirect"); | |
| 128 | 127 | } |
| 129 | 128 | |
| 130 | 129 | return { |
| @@ -197,9 +196,7 @@ export const exchangeMcpOAuthAuthorizationCode = (input: { · 66 unmodified lines | ||
| 197 | 196 | }); |
| 198 | 197 | |
| 199 | 198 | if (result !== "AUTHORIZED" || !captured.tokens) { |
| 200 | − return yield* Effect.fail( | |
| 201 | − new Error("OAuth redirect did not complete MCP OAuth setup"), | |
| 202 | − ); | |
| 199 | + return yield* mcpOAuthEffectError("index", "OAuth redirect did not complete MCP OAuth setup"); | |
| 203 | 200 | } |
| 204 | 201 | |
| 205 | 202 | return { |
| @@ -0,0 +1,13 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class KitchenSinkEffectError extends Data.TaggedError( | |
| 4 | + "KitchenSinkEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const kitchenSinkEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new KitchenSinkEffectError({ module, message }); | |
| @@ -11,6 +11,7 @@ import { · 10 unmodified lines | ||
| 11 | 11 | toExecutorTool, |
| 12 | 12 | } from "@executor/codemode-core"; |
| 13 | 13 | import { makeSesExecutor } from "@executor/runtime-ses"; |
| 14 | +import { kitchenSinkEffectError } from "./effect-errors"; | |
| 14 | 15 | |
| 15 | 16 | const numberPairInputSchema = Schema.standardSchemaV1( |
| 16 | 17 | Schema.Struct({ |
| @@ -100,7 +101,7 @@ describe("kitchen-sink", () => { · 83 unmodified lines | ||
| 100 | 101 | }).execute; |
| 101 | 102 | |
| 102 | 103 | if (!execute) { |
| 103 | − return yield* Effect.fail(new Error("Code tool execute function is missing")); | |
| 104 | + return yield* kitchenSinkEffectError("kitchen-sink.test", "Code tool execute function is missing"); | |
| 104 | 105 | } |
| 105 | 106 | |
| 106 | 107 | const output = yield* Effect.tryPromise({ |
| @@ -0,0 +1,13 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class AiSdkEffectError extends Data.TaggedError( | |
| 4 | + "AiSdkEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const aiSdkEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new AiSdkEffectError({ module, message }); | |
| @@ -12,6 +12,7 @@ import { · 11 unmodified lines | ||
| 12 | 12 | type ToolMap, |
| 13 | 13 | type ToolInvoker, |
| 14 | 14 | } from "@executor/codemode-core"; |
| 15 | +import { aiSdkEffectError } from "./effect-errors"; | |
| 15 | 16 | |
| 16 | 17 | export type AiSdkToolMap = ToolSet; |
| 17 | 18 | |
| @@ -49,7 +50,7 @@ export function createCodeTool(input: { · 31 unmodified lines | ||
| 49 | 50 | input.executor.execute(code, input.toolInvoker).pipe( |
| 50 | 51 | Effect.flatMap((result: ExecuteResult) => |
| 51 | 52 | result.error |
| 52 | − ? Effect.fail(new Error(result.error)) | |
| 53 | + ? Effect.fail(aiSdkEffectError("index", result.error)) | |
| 53 | 54 | : Effect.succeed({ |
| 54 | 55 | code, |
| 55 | 56 | result: result.result, |
| @@ -0,0 +1,13 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class KernelCoreEffectError extends Data.TaggedError( | |
| 4 | + "KernelCoreEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const kernelCoreEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new KernelCoreEffectError({ module, message }); | |
| @@ -22,6 +22,7 @@ import type { · 21 unmodified lines | ||
| 22 | 22 | ToolMetadata, |
| 23 | 23 | ToolPath, |
| 24 | 24 | } from "./types"; |
| 25 | +import { kernelCoreEffectError } from "./effect-errors"; | |
| 25 | 26 | |
| 26 | 27 | type ResolvedTool = { |
| 27 | 28 | path: ToolPath; |
| @@ -88,7 +89,7 @@ const parseInput = (input: { · 60 unmodified lines | ||
| 88 | 89 | const validate = getSchemaValidator(input.schema); |
| 89 | 90 | if (!validate) { |
| 90 | 91 | return Effect.fail( |
| 91 | − new Error(`Tool ${input.path} has no Standard Schema validator on inputSchema`), | |
| 92 | + kernelCoreEffectError("tool-map", `Tool ${input.path} has no Standard Schema validator on inputSchema`), | |
| 92 | 93 | ); |
| 93 | 94 | } |
| 94 | 95 | |
| @@ -99,7 +100,7 @@ const parseInput = (input: { · 4 unmodified lines | ||
| 99 | 100 | Effect.flatMap((result) => { |
| 100 | 101 | if ("issues" in result && result.issues) { |
| 101 | 102 | return Effect.fail( |
| 102 | − new Error( | |
| 103 | + kernelCoreEffectError("tool-map", | |
| 103 | 104 | `Input validation failed for ${input.path}: ${formatIssues(result.issues)}`, |
| 104 | 105 | ), |
| 105 | 106 | ); |
| @@ -213,7 +214,7 @@ const mergeAcceptedElicitationContent = (input: { · 107 unmodified lines | ||
| 213 | 214 | |
| 214 | 215 | if (!isRecord(input.args)) { |
| 215 | 216 | return Effect.fail( |
| 216 | − new Error( | |
| 217 | + kernelCoreEffectError("tool-map", | |
| 217 | 218 | `Tool ${input.path} cannot merge elicitation content into non-object arguments`, |
| 218 | 219 | ), |
| 219 | 220 | ); |
| @@ -492,7 +493,7 @@ export const makeToolInvokerFromTools = (input: { · 272 unmodified lines | ||
| 492 | 493 | Effect.gen(function* () { |
| 493 | 494 | const entry = byPath.get(path); |
| 494 | 495 | if (!entry) { |
| 495 | − return yield* Effect.fail(new Error(`Unknown tool path: ${path}`)); | |
| 496 | + return yield* kernelCoreEffectError("tool-map", `Unknown tool path: ${path}`); | |
| 496 | 497 | } |
| 497 | 498 | |
| 498 | 499 | const parsedInput = yield* parseInput({ |
| @@ -127,22 +127,18 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 126 unmodified lines | ||
| 127 | 127 | : parseSecretStoreProviderId(payload.providerId); |
| 128 | 128 | |
| 129 | 129 | if (name.length === 0) { |
| 130 | − return yield* Effect.fail( | |
| 131 | − new ControlPlaneBadRequestError({ | |
| 130 | + return yield* new ControlPlaneBadRequestError({ | |
| 132 | 131 | operation: "secrets.create", |
| 133 | 132 | message: "Secret name is required.", |
| 134 | 133 | details: "Secret name is required.", |
| 135 | − }), | |
| 136 | − ); | |
| 134 | + }); | |
| 137 | 135 | } |
| 138 | 136 | if (payload.providerId !== undefined && requestedProviderId === null) { |
| 139 | − return yield* Effect.fail( | |
| 140 | − new ControlPlaneBadRequestError({ | |
| 137 | + return yield* new ControlPlaneBadRequestError({ | |
| 141 | 138 | operation: "secrets.create", |
| 142 | 139 | message: `Unsupported secret provider: ${payload.providerId}`, |
| 143 | 140 | details: `Unsupported secret provider: ${payload.providerId}`, |
| 144 | − }), | |
| 145 | − ); | |
| 141 | + }); | |
| 146 | 142 | } |
| 147 | 143 | |
| 148 | 144 | const store = yield* ControlPlaneStore; |
| @@ -165,7 +161,7 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 16 unmodified lines | ||
| 165 | 161 | ); |
| 166 | 162 | |
| 167 | 163 | if (Option.isNone(created)) { |
| 168 | − return yield* Effect.fail(storageError(`Created secret not found: ${ref.handle}`)); | |
| 164 | + return yield* storageError(`Created secret not found: ${ref.handle}`); | |
| 169 | 165 | } |
| 170 | 166 | |
| 171 | 167 | return { |
| @@ -188,13 +184,11 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 16 unmodified lines | ||
| 188 | 184 | ); |
| 189 | 185 | |
| 190 | 186 | if (Option.isNone(existing)) { |
| 191 | − return yield* Effect.fail( | |
| 192 | − new ControlPlaneNotFoundError({ | |
| 187 | + return yield* new ControlPlaneNotFoundError({ | |
| 193 | 188 | operation: "secrets.update", |
| 194 | 189 | message: `Secret not found: ${path.secretId}`, |
| 195 | 190 | details: `Secret not found: ${path.secretId}`, |
| 196 | − }), | |
| 197 | − ); | |
| 191 | + }); | |
| 198 | 192 | } |
| 199 | 193 | |
| 200 | 194 | const update: { name?: string | null; value?: string } = {}; |
| @@ -234,13 +228,11 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 33 unmodified lines | ||
| 234 | 228 | ); |
| 235 | 229 | |
| 236 | 230 | if (Option.isNone(existing)) { |
| 237 | − return yield* Effect.fail( | |
| 238 | − new ControlPlaneNotFoundError({ | |
| 231 | + return yield* new ControlPlaneNotFoundError({ | |
| 239 | 232 | operation: "secrets.delete", |
| 240 | 233 | message: `Secret not found: ${path.secretId}`, |
| 241 | 234 | details: `Secret not found: ${path.secretId}`, |
| 242 | − }), | |
| 243 | − ); | |
| 235 | + }); | |
| 244 | 236 | } |
| 245 | 237 | |
| 246 | 238 | const deleteSecretMaterial = createDefaultSecretMaterialDeleter({ |
| @@ -254,7 +246,7 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 7 unmodified lines | ||
| 254 | 246 | ); |
| 255 | 247 | |
| 256 | 248 | if (!removed) { |
| 257 | − return yield* Effect.fail(storageError(`Failed removing secret: ${path.secretId}`)); | |
| 249 | + return yield* storageError(`Failed removing secret: ${path.secretId}`); | |
| 258 | 250 | } |
| 259 | 251 | |
| 260 | 252 | return { removed: true }; |
| @@ -35,6 +35,7 @@ import type { · 34 unmodified lines | ||
| 35 | 35 | ResolveSecretMaterial, |
| 36 | 36 | SecretMaterialResolveContext, |
| 37 | 37 | } from "../local/secret-material-providers"; |
| 38 | +import { runtimeEffectError } from "../effect-errors"; | |
| 38 | 39 | |
| 39 | 40 | export type ResolvedSourceAuthMaterial = { |
| 40 | 41 | placements: ReadonlyArray<RequestPlacement>; |
| @@ -456,9 +457,7 @@ export const resolveAuthArtifactMaterial = (input: { · 415 unmodified lines | ||
| 456 | 457 | const providerGrantRefConfig = decodeProviderGrantRefAuthArtifactConfig(input.artifact); |
| 457 | 458 | if (providerGrantRefConfig !== null) { |
| 458 | 459 | if (input.lease === null || input.lease === undefined) { |
| 459 | − return yield* Effect.fail( | |
| 460 | − new Error(`Provider grant auth artifact ${input.artifact.id} requires a lease`), | |
| 461 | − ); | |
| 460 | + return yield* runtimeEffectError("auth/auth-artifacts", `Provider grant auth artifact ${input.artifact.id} requires a lease`); | |
| 462 | 461 | } |
| 463 | 462 | } |
| 464 | 463 | |
| @@ -470,18 +469,16 @@ export const resolveAuthArtifactMaterial = (input: { · 5 unmodified lines | ||
| 470 | 469 | } |
| 471 | 470 | |
| 472 | 471 | if (input.lease === null || input.lease === undefined) { |
| 473 | − return yield* Effect.fail( | |
| 474 | − new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`), | |
| 475 | − ); | |
| 472 | + return yield* runtimeEffectError("auth/auth-artifacts", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`); | |
| 476 | 473 | } |
| 477 | 474 | |
| 478 | 475 | const templatesEither = decodeRequestPlacementTemplates( |
| 479 | 476 | input.lease.placementsTemplateJson, |
| 480 | 477 | ); |
| 481 | 478 | if (templatesEither._tag === "Left") { |
| 482 | − return yield* Effect.fail(new Error( | |
| 479 | + return yield* runtimeEffectError("auth/auth-artifacts", | |
| 483 | 480 | `Invalid auth lease placements for artifact ${input.artifact.id}`, |
| 484 | − )); | |
| 481 | + ); | |
| 485 | 482 | } |
| 486 | 483 | |
| 487 | 484 | const placements = yield* Effect.forEach(templatesEither.right, (placement) => |
| @@ -27,6 +27,7 @@ import { · 26 unmodified lines | ||
| 27 | 27 | import { refreshOAuth2AccessToken } from "./oauth2-pkce"; |
| 28 | 28 | import type { OAuth2TokenResponse } from "./oauth2-pkce"; |
| 29 | 29 | import { createPersistedMcpAuthProvider } from "./mcp-auth-provider"; |
| 30 | +import { runtimeEffectError } from "../effect-errors"; | |
| 30 | 31 | |
| 31 | 32 | const LEASE_REFRESH_SKEW_MS = 60_000; |
| 32 | 33 | |
| @@ -92,9 +93,7 @@ const refreshRefreshableOauth2AuthorizedUserArtifact = (input: { · 59 unmodified lines | ||
| 92 | 93 | decoded === null |
| 93 | 94 | || decoded.artifactKind !== RefreshableOAuth2AuthorizedUserAuthArtifactKind |
| 94 | 95 | ) { |
| 95 | − return yield* Effect.fail( | |
| 96 | − new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`), | |
| 97 | − ); | |
| 96 | + return yield* runtimeEffectError("auth/auth-leases", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`); | |
| 98 | 97 | } |
| 99 | 98 | |
| 100 | 99 | const refreshToken = yield* input.resolveSecretMaterial({ |
| @@ -204,24 +203,18 @@ const refreshProviderGrantRefArtifact = (input: { · 103 unmodified lines | ||
| 204 | 203 | Effect.gen(function* () { |
| 205 | 204 | const config = decodeProviderGrantRefAuthArtifactConfig(input.artifact); |
| 206 | 205 | if (config === null) { |
| 207 | − return yield* Effect.fail( | |
| 208 | − new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`), | |
| 209 | − ); | |
| 206 | + return yield* runtimeEffectError("auth/auth-leases", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`); | |
| 210 | 207 | } |
| 211 | 208 | |
| 212 | 209 | const grantOption = yield* input.rows.providerAuthGrants.getById(config.grantId); |
| 213 | 210 | if (Option.isNone(grantOption)) { |
| 214 | − return yield* Effect.fail( | |
| 215 | − new Error(`Provider auth grant not found: ${config.grantId}`), | |
| 216 | − ); | |
| 211 | + return yield* runtimeEffectError("auth/auth-leases", `Provider auth grant not found: ${config.grantId}`); | |
| 217 | 212 | } |
| 218 | 213 | const grant = grantOption.value; |
| 219 | 214 | |
| 220 | 215 | const oauthClientOption = yield* input.rows.workspaceOauthClients.getById(grant.oauthClientId); |
| 221 | 216 | if (Option.isNone(oauthClientOption)) { |
| 222 | − return yield* Effect.fail( | |
| 223 | − new Error(`Workspace OAuth client not found: ${grant.oauthClientId}`), | |
| 224 | − ); | |
| 217 | + return yield* runtimeEffectError("auth/auth-leases", `Workspace OAuth client not found: ${grant.oauthClientId}`); | |
| 225 | 218 | } |
| 226 | 219 | const oauthClient = oauthClientOption.value; |
| 227 | 220 | |
| @@ -349,9 +342,7 @@ export const upsertOauth2AuthorizedUserLeaseFromTokenResponse = (input: { · 121 unmodified lines | ||
| 349 | 342 | decoded === null |
| 350 | 343 | || decoded.artifactKind !== RefreshableOAuth2AuthorizedUserAuthArtifactKind |
| 351 | 344 | ) { |
| 352 | − return yield* Effect.fail( | |
| 353 | − new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`), | |
| 354 | − ); | |
| 345 | + return yield* runtimeEffectError("auth/auth-leases", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`); | |
| 355 | 346 | } |
| 356 | 347 | |
| 357 | 348 | const existingLeaseOption = yield* input.rows.authLeases.getByAuthArtifactId(input.artifact.id); |
| @@ -21,6 +21,7 @@ import type { · 20 unmodified lines | ||
| 21 | 21 | SecretMaterialResolveContext, |
| 22 | 22 | } from "../local/secret-material-providers"; |
| 23 | 23 | import { SecretMaterialResolverService } from "../local/secret-material-providers"; |
| 24 | +import { runtimeEffectError } from "../effect-errors"; | |
| 24 | 25 | |
| 25 | 26 | const authForSlot = (input: { |
| 26 | 27 | source: Source; |
| @@ -114,9 +115,7 @@ export const resolveSourceAuthMaterialWithDeps = (input: { · 87 unmodified lines | ||
| 114 | 115 | || artifact?.artifactKind === "provider_grant_ref" |
| 115 | 116 | || artifact?.artifactKind === "mcp_oauth" |
| 116 | 117 | ) { |
| 117 | − return yield* Effect.fail( | |
| 118 | − new Error("Dynamic auth artifacts require persistence-backed lease resolution"), | |
| 119 | − ); | |
| 118 | + return yield* runtimeEffectError("auth/source-auth-material", "Dynamic auth artifacts require persistence-backed lease resolution"); | |
| 120 | 119 | } |
| 121 | 120 | |
| 122 | 121 | return yield* resolveAuthArtifactMaterial({ |
| @@ -46,6 +46,7 @@ import { · 45 unmodified lines | ||
| 46 | 46 | RuntimeSourceStoreService, |
| 47 | 47 | type RuntimeSourceStore, |
| 48 | 48 | } from "../../sources/source-store"; |
| 49 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 49 | 50 | |
| 50 | 51 | type CatalogImportMetadata = CatalogSnapshotV1["import"]; |
| 51 | 52 | |
| @@ -672,7 +673,7 @@ const ensureRuntimeCatalogWorkspace = ( · 620 unmodified lines | ||
| 672 | 673 | ) => { |
| 673 | 674 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { |
| 674 | 675 | return Effect.fail( |
| 675 | − new Error( | |
| 676 | + runtimeEffectError("catalog/source/runtime", | |
| 676 | 677 | `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`, |
| 677 | 678 | ), |
| 678 | 679 | ); |
| @@ -762,12 +763,10 @@ const loadSourceWithCatalogWithDeps = (deps: RuntimeSourceCatalogStoreDeps, inpu · 83 unmodified lines | ||
| 762 | 763 | sourceId: source.id, |
| 763 | 764 | }); |
| 764 | 765 | if (artifact === null) { |
| 765 | − return yield* Effect.fail( | |
| 766 | − new LocalSourceArtifactMissingError({ | |
| 766 | + return yield* new LocalSourceArtifactMissingError({ | |
| 767 | 767 | message: `Catalog artifact missing for source ${input.sourceId}`, |
| 768 | 768 | sourceId: input.sourceId, |
| 769 | − }), | |
| 770 | − ); | |
| 769 | + }); | |
| 771 | 770 | } |
| 772 | 771 | |
| 773 | 772 | const snapshot = buildSnapshotFromArtifact({ |
| @@ -35,6 +35,7 @@ import { snapshotFromSourceCatalogSyncResult } from "../../sources/catalog-sync- · 34 unmodified lines | ||
| 35 | 35 | import { |
| 36 | 36 | refreshSourceTypeDeclarationInBackground, |
| 37 | 37 | } from "./type-declarations"; |
| 38 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 38 | 39 | |
| 39 | 40 | const shouldIndexSource = (source: Source): boolean => |
| 40 | 41 | source.enabled |
| @@ -77,7 +78,7 @@ const ensureRuntimeCatalogSyncWorkspace = ( · 36 unmodified lines | ||
| 77 | 78 | ) => { |
| 78 | 79 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { |
| 79 | 80 | return Effect.fail( |
| 80 | − new Error( | |
| 81 | + runtimeEffectError("catalog/source/sync", | |
| 81 | 82 | `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`, |
| 82 | 83 | ), |
| 83 | 84 | ); |
| @@ -32,6 +32,7 @@ import { createSourceFromPayload } from "./sources/source-definitions"; · 31 unmodified lines | ||
| 32 | 32 | import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions"; |
| 33 | 33 | import { persistSource } from "./sources/source-store"; |
| 34 | 34 | import { withControlPlaneClient } from "./execution/test-http-client"; |
| 35 | +import { runtimeEffectError } from "./effect-errors"; | |
| 35 | 36 | |
| 36 | 37 | const makeRuntime = Effect.gen(function* () { |
| 37 | 38 | const fs = yield* FileSystem.FileSystem; |
| @@ -369,7 +370,7 @@ const expectLeft = <A, E>(effect: Effect.Effect<A, E, never>) => · 331 unmodified lines | ||
| 369 | 370 | Effect.flatMap((result) => |
| 370 | 371 | result._tag === "Left" |
| 371 | 372 | ? Effect.succeed(result.left) |
| 372 | − : Effect.fail(new Error("Expected effect to fail")), | |
| 373 | + : Effect.fail(runtimeEffectError("control-plane-runtime.test", "Expected effect to fail")), | |
| 373 | 374 | ), |
| 374 | 375 | ); |
| 375 | 376 | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class RuntimeEffectError extends Data.TaggedError( | |
| 4 | + "RuntimeEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const runtimeEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new RuntimeEffectError({ module, message }); | |
| @@ -5,6 +5,7 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines | ||
| 5 | 5 | import type { LoadedSourceCatalogToolIndexEntry } from "../catalog/source/runtime"; |
| 6 | 6 | import type { ResolvedSourceAuthMaterial } from "../auth/source-auth-material"; |
| 7 | 7 | import { getSourceAdapter } from "../sources/source-adapters"; |
| 8 | +import { runtimeEffectError } from "../effect-errors"; | |
| 8 | 9 | |
| 9 | 10 | export const invocationDescriptorFromTool = (input: { |
| 10 | 11 | tool: LoadedSourceCatalogToolIndexEntry; |
| @@ -49,7 +50,7 @@ export const invokeIrTool = (input: { · 38 unmodified lines | ||
| 49 | 50 | const adapter = getSourceAdapter(input.tool.executable.adapterKey); |
| 50 | 51 | if (adapter.key !== input.tool.source.kind) { |
| 51 | 52 | return Effect.fail( |
| 52 | − new Error( | |
| 53 | + runtimeEffectError("execution/ir-execution", | |
| 53 | 54 | `Executable ${input.tool.executable.id} expects adapter ${adapter.key}, but source ${input.tool.source.id} is ${input.tool.source.kind}`, |
| 54 | 55 | ), |
| 55 | 56 | ); |
| @@ -23,6 +23,7 @@ import { · 22 unmodified lines | ||
| 23 | 23 | type ExecutionInteraction, |
| 24 | 24 | type WorkspaceId, |
| 25 | 25 | } from "#schema"; |
| 26 | +import * as Data from "effect/Data"; | |
| 26 | 27 | import * as Deferred from "effect/Deferred"; |
| 27 | 28 | import * as Effect from "effect/Effect"; |
| 28 | 29 | import * as Option from "effect/Option"; |
| @@ -48,6 +49,7 @@ import { · 19 unmodified lines | ||
| 48 | 49 | type ControlPlaneStoreShape, |
| 49 | 50 | } from "../store"; |
| 50 | 51 | import { RuntimeExecutionResolverService } from "./workspace/environment"; |
| 52 | +import { runtimeEffectError } from "../effect-errors"; | |
| 51 | 53 | |
| 52 | 54 | const executionOps = { |
| 53 | 55 | create: operationErrors("executions.create"), |
| @@ -164,15 +166,29 @@ const resolveInteractionMode = ( · 110 unmodified lines | ||
| 164 | 166 | ): InteractionMode => |
| 165 | 167 | value === "live" || value === "live_form" ? value : DEFAULT_INTERACTION_MODE; |
| 166 | 168 | |
| 169 | +class ExecutionSuspendedError extends Data.TaggedError( | |
| 170 | + "ExecutionSuspendedError", | |
| 171 | +)<{ | |
| 172 | + readonly executionId: Execution["id"]; | |
| 173 | + readonly interactionId: string; | |
| 174 | + readonly message: string; | |
| 175 | +}> {} | |
| 176 | + | |
| 167 | 177 | const createExecutionSuspendedError = (input: { |
| 168 | 178 | executionId: Execution["id"]; |
| 169 | 179 | interactionId: string; |
| 170 | −}): Error => | |
| 171 | − new Error( | |
| 172 | − `${EXECUTION_SUSPENDED_SENTINEL}:${input.executionId}:${input.interactionId}`, | |
| 173 | − ); | |
| 180 | +}): ExecutionSuspendedError => | |
| 181 | + new ExecutionSuspendedError({ | |
| 182 | + executionId: input.executionId, | |
| 183 | + interactionId: input.interactionId, | |
| 184 | + message: `${EXECUTION_SUSPENDED_SENTINEL}:${input.executionId}:${input.interactionId}`, | |
| 185 | + }); | |
| 174 | 186 | |
| 175 | 187 | const isExecutionSuspendedValue = (value: unknown): boolean => { |
| 188 | + if (value instanceof ExecutionSuspendedError) { | |
| 189 | + return true; | |
| 190 | + } | |
| 191 | + | |
| 176 | 192 | if (value instanceof Error) { |
| 177 | 193 | return value.message.includes(EXECUTION_SUSPENDED_SENTINEL); |
| 178 | 194 | } |
| @@ -242,12 +258,10 @@ const fetchExecution = ( · 63 unmodified lines | ||
| 242 | 258 | ); |
| 243 | 259 | |
| 244 | 260 | if (Option.isNone(existing)) { |
| 245 | − return yield* Effect.fail( | |
| 246 | − errors.notFound( | |
| 261 | + return yield* errors.notFound( | |
| 247 | 262 | "Execution not found", |
| 248 | 263 | `workspaceId=${input.workspaceId} executionId=${input.executionId}`, |
| 249 | − ), | |
| 250 | − ); | |
| 264 | + ); | |
| 251 | 265 | } |
| 252 | 266 | |
| 253 | 267 | return existing.value; |
| @@ -378,12 +392,10 @@ const suspendExecutionForInteraction = (input: { · 124 unmodified lines | ||
| 378 | 392 | state: "waiting_for_interaction", |
| 379 | 393 | }); |
| 380 | 394 | |
| 381 | − return yield* Effect.fail( | |
| 382 | − createExecutionSuspendedError({ | |
| 395 | + return yield* createExecutionSuspendedError({ | |
| 383 | 396 | executionId: input.executionId, |
| 384 | 397 | interactionId: input.interactionId, |
| 385 | − }), | |
| 386 | − ); | |
| 398 | + }); | |
| 387 | 399 | }); |
| 388 | 400 | |
| 389 | 401 | const createHybridOnElicitation = (input: { |
| @@ -487,12 +499,10 @@ const createReplayToolInvoker = (input: { · 97 unmodified lines | ||
| 487 | 499 | } |
| 488 | 500 | |
| 489 | 501 | if (existing.value.status === "failed") { |
| 490 | − return yield* Effect.fail( | |
| 491 | − new Error( | |
| 502 | + return yield* runtimeEffectError("execution/service", | |
| 492 | 503 | existing.value.errorText |
| 493 | 504 | ?? `Stored tool step ${String(stepSequence)} failed`, |
| 494 | − ), | |
| 495 | − ); | |
| 505 | + ); | |
| 496 | 506 | } |
| 497 | 507 | } else { |
| 498 | 508 | const now = Date.now(); |
| @@ -834,12 +844,10 @@ const createExecutionWithDependencies = ( · 335 unmodified lines | ||
| 834 | 844 | ); |
| 835 | 845 | |
| 836 | 846 | if (Option.isNone(running)) { |
| 837 | − return yield* Effect.fail( | |
| 838 | − executionOps.create.notFound( | |
| 847 | + return yield* executionOps.create.notFound( | |
| 839 | 848 | "Execution not found after insert", |
| 840 | 849 | `executionId=${execution.id}`, |
| 841 | − ), | |
| 842 | − ); | |
| 850 | + ); | |
| 843 | 851 | } |
| 844 | 852 | |
| 845 | 853 | const nextState = yield* liveExecutionManager.registerStateWaiter(execution.id); |
| @@ -936,12 +944,10 @@ export const resumeExecution = (input: { · 90 unmodified lines | ||
| 936 | 944 | && existing.pendingInteraction !== null |
| 937 | 945 | ) |
| 938 | 946 | ) { |
| 939 | − return yield* Effect.fail( | |
| 940 | − executionOps.resume.badRequest( | |
| 947 | + return yield* executionOps.resume.badRequest( | |
| 941 | 948 | "Execution is not waiting for interaction", |
| 942 | 949 | `executionId=${input.executionId} status=${existing.execution.status}`, |
| 943 | − ), | |
| 944 | − ); | |
| 950 | + ); | |
| 945 | 951 | } |
| 946 | 952 | |
| 947 | 953 | const responseJson = input.payload.responseJson; |
| @@ -988,12 +994,10 @@ export const resumeExecution = (input: { · 40 unmodified lines | ||
| 988 | 994 | ); |
| 989 | 995 | |
| 990 | 996 | if (!resumed) { |
| 991 | − return yield* Effect.fail( | |
| 992 | − executionOps.resume.badRequest( | |
| 997 | + return yield* executionOps.resume.badRequest( | |
| 993 | 998 | "Resume is unavailable for this execution", |
| 994 | 999 | `executionId=${input.executionId}`, |
| 995 | − ), | |
| 996 | − ); | |
| 1000 | + ); | |
| 997 | 1001 | } |
| 998 | 1002 | } |
| 999 | 1003 | |
| @@ -10,6 +10,7 @@ import type { WorkspaceStorageServices } from "../../local/storage"; · 9 unmodified lines | ||
| 10 | 10 | import { invocationDescriptorFromTool } from "../ir-execution"; |
| 11 | 11 | import { evaluateInvocationPolicy } from "../../policy/invocation-policy-engine"; |
| 12 | 12 | import { loadRuntimeLocalWorkspacePolicies } from "../../policy/policies-operations"; |
| 13 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 13 | 14 | |
| 14 | 15 | const asToolPath = (value: string): ToolPath => value as ToolPath; |
| 15 | 16 | |
| @@ -98,15 +99,13 @@ export const authorizePersistedToolInvocation = (input: { · 82 unmodified lines | ||
| 98 | 99 | } |
| 99 | 100 | |
| 100 | 101 | if (decision.kind === "deny") { |
| 101 | − return yield* Effect.fail(new Error(decision.reason)); | |
| 102 | + return yield* runtimeEffectError("execution/workspace/authorization", decision.reason); | |
| 102 | 103 | } |
| 103 | 104 | |
| 104 | 105 | if (!input.onElicitation) { |
| 105 | − return yield* Effect.fail( | |
| 106 | − new Error( | |
| 106 | + return yield* runtimeEffectError("execution/workspace/authorization", | |
| 107 | 107 | `Approval required for ${descriptor.toolPath}, but no elicitation-capable host is available`, |
| 108 | − ), | |
| 109 | − ); | |
| 108 | + ); | |
| 110 | 109 | } |
| 111 | 110 | |
| 112 | 111 | const interactionId = |
| @@ -145,10 +144,8 @@ export const authorizePersistedToolInvocation = (input: { · 32 unmodified lines | ||
| 145 | 144 | ); |
| 146 | 145 | |
| 147 | 146 | if (response.action !== "accept") { |
| 148 | − return yield* Effect.fail( | |
| 149 | − new Error( | |
| 147 | + return yield* runtimeEffectError("execution/workspace/authorization", | |
| 150 | 148 | `Tool invocation not approved for ${descriptor.toolPath}`, |
| 151 | − ), | |
| 152 | − ); | |
| 149 | + ); | |
| 153 | 150 | } |
| 154 | 151 | }); |
| @@ -36,6 +36,7 @@ import { · 35 unmodified lines | ||
| 36 | 36 | createWorkspaceSourceCatalog, |
| 37 | 37 | loadWorkspaceCatalogToolByPath, |
| 38 | 38 | } from "./source-catalog"; |
| 39 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 39 | 40 | |
| 40 | 41 | export const createWorkspaceToolInvoker = (input: { |
| 41 | 42 | workspaceId: Source["workspaceId"]; |
| @@ -120,9 +121,7 @@ export const createWorkspaceToolInvoker = (input: { · 78 unmodified lines | ||
| 120 | 121 | includeSchemas: false, |
| 121 | 122 | }); |
| 122 | 123 | if (!catalogTool) { |
| 123 | − return yield* Effect.fail( | |
| 124 | − new Error(`Unknown tool path: ${invocation.path}`), | |
| 125 | − ); | |
| 124 | + return yield* runtimeEffectError("execution/workspace/tool-invoker", `Unknown tool path: ${invocation.path}`); | |
| 126 | 125 | } |
| 127 | 126 | |
| 128 | 127 | yield* authorizePersistedToolInvocation({ |
| @@ -137,12 +137,10 @@ const loadSourceCredentialInteraction = (input: { · 136 unmodified lines | ||
| 137 | 137 | ); |
| 138 | 138 | |
| 139 | 139 | if (Option.isNone(stored)) { |
| 140 | − return yield* Effect.fail( | |
| 141 | − input.operation.notFound( | |
| 140 | + return yield* input.operation.notFound( | |
| 142 | 141 | "Source credential request not found", |
| 143 | 142 | `interactionId=${input.interactionId}`, |
| 144 | − ), | |
| 145 | − ); | |
| 143 | + ); | |
| 146 | 144 | } |
| 147 | 145 | |
| 148 | 146 | const decoded = decodeSourceCredentialInteraction(stored.value); |
| @@ -151,12 +149,10 @@ const loadSourceCredentialInteraction = (input: { · 2 unmodified lines | ||
| 151 | 149 | || decoded.workspaceId !== input.workspaceId |
| 152 | 150 | || decoded.sourceId !== input.sourceId |
| 153 | 151 | ) { |
| 154 | − return yield* Effect.fail( | |
| 155 | − input.operation.notFound( | |
| 152 | + return yield* input.operation.notFound( | |
| 156 | 153 | "Source credential request not found", |
| 157 | 154 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`, |
| 158 | − ), | |
| 159 | − ); | |
| 155 | + ); | |
| 160 | 156 | } |
| 161 | 157 | |
| 162 | 158 | const source = yield* sourceAuthService.getSourceById({ |
| @@ -226,12 +222,10 @@ export const submitSourceCredentialInteraction = (input: { · 63 unmodified lines | ||
| 226 | 222 | }); |
| 227 | 223 | |
| 228 | 224 | if (interaction.status !== "pending") { |
| 229 | − return yield* Effect.fail( | |
| 230 | − localOps.sourceCredentialSubmit.badRequest( | |
| 225 | + return yield* localOps.sourceCredentialSubmit.badRequest( | |
| 231 | 226 | "Source credential request is no longer active", |
| 232 | 227 | `interactionId=${interaction.interactionId} status=${interaction.status}`, |
| 233 | − ), | |
| 234 | − ); | |
| 228 | + ); | |
| 235 | 229 | } |
| 236 | 230 | |
| 237 | 231 | const liveExecutionManager = yield* LiveExecutionManagerService; |
| @@ -260,12 +254,10 @@ export const submitSourceCredentialInteraction = (input: { · 22 unmodified lines | ||
| 260 | 254 | ); |
| 261 | 255 | |
| 262 | 256 | if (!persisted) { |
| 263 | − return yield* Effect.fail( | |
| 264 | − localOps.sourceCredentialSubmit.badRequest( | |
| 257 | + return yield* localOps.sourceCredentialSubmit.badRequest( | |
| 265 | 258 | "Source credential request is no longer resumable", |
| 266 | 259 | `interactionId=${interaction.interactionId}`, |
| 267 | − ), | |
| 268 | − ); | |
| 260 | + ); | |
| 269 | 261 | } |
| 270 | 262 | } |
| 271 | 263 | |
| @@ -302,12 +294,10 @@ export const submitSourceCredentialInteraction = (input: { · 30 unmodified lines | ||
| 302 | 294 | ); |
| 303 | 295 | |
| 304 | 296 | if (!persisted) { |
| 305 | − return yield* Effect.fail( | |
| 306 | − localOps.sourceCredentialSubmit.badRequest( | |
| 297 | + return yield* localOps.sourceCredentialSubmit.badRequest( | |
| 307 | 298 | "Source credential request is no longer resumable", |
| 308 | 299 | `interactionId=${interaction.interactionId}`, |
| 309 | − ), | |
| 310 | − ); | |
| 300 | + ); | |
| 311 | 301 | } |
| 312 | 302 | } |
| 313 | 303 | |
| @@ -320,12 +310,10 @@ export const submitSourceCredentialInteraction = (input: { · 6 unmodified lines | ||
| 320 | 310 | |
| 321 | 311 | const token = trimOrNull(input.token); |
| 322 | 312 | if (token === null) { |
| 323 | − return yield* Effect.fail( | |
| 324 | − localOps.sourceCredentialSubmit.badRequest( | |
| 313 | + return yield* localOps.sourceCredentialSubmit.badRequest( | |
| 325 | 314 | "Missing token", |
| 326 | 315 | `interactionId=${interaction.interactionId}`, |
| 327 | − ), | |
| 328 | − ); | |
| 316 | + ); | |
| 329 | 317 | } |
| 330 | 318 | |
| 331 | 319 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| @@ -365,12 +353,10 @@ export const submitSourceCredentialInteraction = (input: { · 33 unmodified lines | ||
| 365 | 353 | ); |
| 366 | 354 | |
| 367 | 355 | if (!persisted) { |
| 368 | − return yield* Effect.fail( | |
| 369 | − localOps.sourceCredentialSubmit.badRequest( | |
| 356 | + return yield* localOps.sourceCredentialSubmit.badRequest( | |
| 370 | 357 | "Source credential request is no longer resumable", |
| 371 | 358 | `interactionId=${interaction.interactionId}`, |
| 372 | − ), | |
| 373 | − ); | |
| 359 | + ); | |
| 374 | 360 | } |
| 375 | 361 | } |
| 376 | 362 | |
| @@ -55,24 +55,20 @@ export const requireRuntimeLocalWorkspace = ( · 54 unmodified lines | ||
| 55 | 55 | Effect.gen(function* () { |
| 56 | 56 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| 57 | 57 | if (runtimeLocalWorkspace === null) { |
| 58 | − return yield* Effect.fail( | |
| 59 | − new RuntimeLocalWorkspaceUnavailableError({ | |
| 58 | + return yield* new RuntimeLocalWorkspaceUnavailableError({ | |
| 60 | 59 | message: "Runtime local workspace is unavailable", |
| 61 | − }), | |
| 62 | − ); | |
| 60 | + }); | |
| 63 | 61 | } |
| 64 | 62 | |
| 65 | 63 | if ( |
| 66 | 64 | workspaceId !== undefined |
| 67 | 65 | && runtimeLocalWorkspace.installation.workspaceId !== workspaceId |
| 68 | 66 | ) { |
| 69 | − return yield* Effect.fail( | |
| 70 | − new RuntimeLocalWorkspaceMismatchError({ | |
| 67 | + return yield* new RuntimeLocalWorkspaceMismatchError({ | |
| 71 | 68 | message: `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`, |
| 72 | 69 | requestedWorkspaceId: workspaceId, |
| 73 | 70 | activeWorkspaceId: runtimeLocalWorkspace.installation.workspaceId, |
| 74 | − }), | |
| 75 | − ); | |
| 71 | + }); | |
| 76 | 72 | } |
| 77 | 73 | |
| 78 | 74 | return runtimeLocalWorkspace; |
| @@ -22,6 +22,7 @@ import { fromConfigSecretProviderId } from "./config-secrets"; · 21 unmodified lines | ||
| 22 | 22 | import { getRuntimeLocalWorkspaceOption } from "./runtime-context"; |
| 23 | 23 | import { ControlPlaneStore } from "../store"; |
| 24 | 24 | import type { ControlPlaneStoreShape } from "../store"; |
| 25 | +import { runtimeEffectError } from "../effect-errors"; | |
| 25 | 26 | |
| 26 | 27 | export const ENV_SECRET_PROVIDER_ID = "env"; |
| 27 | 28 | export const PARAMS_SECRET_PROVIDER_ID = "params"; |
| @@ -304,7 +305,7 @@ const ensureCommandSuccess = (input: { · 276 unmodified lines | ||
| 304 | 305 | ?? ensureNonEmptyString(input.result.stdout) |
| 305 | 306 | ?? "command returned non-zero exit code"; |
| 306 | 307 | |
| 307 | − return Effect.fail(new Error(`${input.operation}: ${input.message}: ${details}`)); | |
| 308 | + return Effect.fail(runtimeEffectError("local/secret-material-providers", `${input.operation}: ${input.message}: ${details}`)); | |
| 308 | 309 | }; |
| 309 | 310 | |
| 310 | 311 | const commandAvailabilityCache = new Map<string, Promise<boolean>>(); |
| @@ -389,9 +390,7 @@ const loadStoredSecretMaterial = (input: { · 78 unmodified lines | ||
| 389 | 390 | const materialId = SecretMaterialIdSchema.make(input.id); |
| 390 | 391 | const stored = yield* input.runtime.rows.secretMaterials.getById(materialId); |
| 391 | 392 | if (Option.isNone(stored)) { |
| 392 | − return yield* Effect.fail( | |
| 393 | − new Error(`${input.operation}: secret material not found: ${input.id}`), | |
| 394 | − ); | |
| 393 | + return yield* runtimeEffectError("local/secret-material-providers", `${input.operation}: secret material not found: ${input.id}`); | |
| 395 | 394 | } |
| 396 | 395 | |
| 397 | 396 | return stored.value; |
| @@ -437,11 +436,9 @@ const loadManagedKeychainRef = (input: { · 39 unmodified lines | ||
| 437 | 436 | operation: input.operation, |
| 438 | 437 | }); |
| 439 | 438 | if (material.providerId !== KEYCHAIN_SECRET_PROVIDER_ID) { |
| 440 | − return yield* Effect.fail( | |
| 441 | − new Error( | |
| 439 | + return yield* runtimeEffectError("local/secret-material-providers", | |
| 442 | 440 | `${input.operation}: secret ${material.id} is stored in provider '${material.providerId}', not '${KEYCHAIN_SECRET_PROVIDER_ID}'`, |
| 443 | − ), | |
| 444 | − ); | |
| 441 | + ); | |
| 445 | 442 | } |
| 446 | 443 | |
| 447 | 444 | return { |
| @@ -502,7 +499,7 @@ const readKeychainSecretValue = (input: { · 54 unmodified lines | ||
| 502 | 499 | ); |
| 503 | 500 | default: |
| 504 | 501 | return Effect.fail( |
| 505 | − new Error(`keychain.get: keychain provider is unsupported on platform '${process.platform}'`), | |
| 502 | + runtimeEffectError("local/secret-material-providers", `keychain.get: keychain provider is unsupported on platform '${process.platform}'`), | |
| 506 | 503 | ); |
| 507 | 504 | } |
| 508 | 505 | }; |
| @@ -567,7 +564,7 @@ const writeKeychainSecretValue = (input: { · 58 unmodified lines | ||
| 567 | 564 | ); |
| 568 | 565 | default: |
| 569 | 566 | return Effect.fail( |
| 570 | − new Error(`keychain.put: keychain provider is unsupported on platform '${process.platform}'`), | |
| 567 | + runtimeEffectError("local/secret-material-providers", `keychain.put: keychain provider is unsupported on platform '${process.platform}'`), | |
| 571 | 568 | ); |
| 572 | 569 | } |
| 573 | 570 | }; |
| @@ -609,7 +606,7 @@ const deleteKeychainSecretValue = (input: { · 35 unmodified lines | ||
| 609 | 606 | ); |
| 610 | 607 | default: |
| 611 | 608 | return Effect.fail( |
| 612 | − new Error(`keychain.delete: keychain provider is unsupported on platform '${process.platform}'`), | |
| 609 | + runtimeEffectError("local/secret-material-providers", `keychain.delete: keychain provider is unsupported on platform '${process.platform}'`), | |
| 613 | 610 | ); |
| 614 | 611 | } |
| 615 | 612 | }; |
| @@ -618,7 +615,7 @@ const createParamsSecretMaterialProvider = (): SecretMaterialProvider => ({ · 2 unmodified lines | ||
| 618 | 615 | resolve: ({ ref, context }) => { |
| 619 | 616 | const value = ensureNonEmptyString(context.params?.[ref.handle]); |
| 620 | 617 | if (value === null) { |
| 621 | − return Effect.fail(new Error(`Secret parameter ${ref.handle} is not set`)); | |
| 618 | + return Effect.fail(runtimeEffectError("local/secret-material-providers", `Secret parameter ${ref.handle} is not set`)); | |
| 622 | 619 | } |
| 623 | 620 | |
| 624 | 621 | return Effect.succeed(value); |
| @@ -631,7 +628,7 @@ const createEnvSecretMaterialProvider = (): SecretMaterialProvider => ({ · 6 unmodified lines | ||
| 631 | 628 | resolve: ({ ref, runtime }) => { |
| 632 | 629 | if (!runtime.dangerouslyAllowEnvSecrets) { |
| 633 | 630 | return Effect.fail( |
| 634 | − new Error( | |
| 631 | + runtimeEffectError("local/secret-material-providers", | |
| 635 | 632 | `Env-backed secrets are disabled. Set ${DANGEROUSLY_ALLOW_ENV_SECRETS_ENV}=true to allow provider '${ENV_SECRET_PROVIDER_ID}'.`, |
| 636 | 633 | ), |
| 637 | 634 | ); |
| @@ -639,7 +636,7 @@ const createEnvSecretMaterialProvider = (): SecretMaterialProvider => ({ · 1 unmodified line | ||
| 639 | 636 | |
| 640 | 637 | const value = ensureNonEmptyString(runtime.env[ref.handle]); |
| 641 | 638 | if (value === null) { |
| 642 | − return Effect.fail(new Error(`Environment variable ${ref.handle} is not set`)); | |
| 639 | + return Effect.fail(runtimeEffectError("local/secret-material-providers", `Environment variable ${ref.handle} is not set`)); | |
| 643 | 640 | } |
| 644 | 641 | |
| 645 | 642 | return Effect.succeed(value); |
| @@ -657,16 +654,12 @@ const createLocalSecretMaterialProvider = (): SecretMaterialProvider => ({ · 11 unmodified lines | ||
| 657 | 654 | operation: "local.get", |
| 658 | 655 | }); |
| 659 | 656 | if (stored.providerId !== LOCAL_SECRET_PROVIDER_ID) { |
| 660 | − return yield* Effect.fail( | |
| 661 | − new Error( | |
| 657 | + return yield* runtimeEffectError("local/secret-material-providers", | |
| 662 | 658 | `local.get: secret ${stored.id} is stored in provider '${stored.providerId}', not '${LOCAL_SECRET_PROVIDER_ID}'`, |
| 663 | − ), | |
| 664 | − ); | |
| 659 | + ); | |
| 665 | 660 | } |
| 666 | 661 | if (stored.value === null) { |
| 667 | − return yield* Effect.fail( | |
| 668 | − new Error(`local.get: secret ${stored.id} does not have a local value`), | |
| 669 | − ); | |
| 662 | + return yield* runtimeEffectError("local/secret-material-providers", `local.get: secret ${stored.id} does not have a local value`); | |
| 670 | 663 | } |
| 671 | 664 | |
| 672 | 665 | return stored.value; |
| @@ -690,11 +683,9 @@ const createLocalSecretMaterialProvider = (): SecretMaterialProvider => ({ · 17 unmodified lines | ||
| 690 | 683 | operation: "local.update", |
| 691 | 684 | }); |
| 692 | 685 | if (stored.providerId !== LOCAL_SECRET_PROVIDER_ID) { |
| 693 | − return yield* Effect.fail( | |
| 694 | − new Error( | |
| 686 | + return yield* runtimeEffectError("local/secret-material-providers", | |
| 695 | 687 | `local.update: secret ${stored.id} is stored in provider '${stored.providerId}', not '${LOCAL_SECRET_PROVIDER_ID}'`, |
| 696 | − ), | |
| 697 | − ); | |
| 688 | + ); | |
| 698 | 689 | } |
| 699 | 690 | |
| 700 | 691 | if (name === undefined && value === undefined) { |
| @@ -709,7 +700,7 @@ const createLocalSecretMaterialProvider = (): SecretMaterialProvider => ({ · 8 unmodified lines | ||
| 709 | 700 | }, |
| 710 | 701 | ); |
| 711 | 702 | if (Option.isNone(updated)) { |
| 712 | − return yield* Effect.fail(new Error(`local.update: secret material not found: ${stored.id}`)); | |
| 703 | + return yield* runtimeEffectError("local/secret-material-providers", `local.update: secret material not found: ${stored.id}`); | |
| 713 | 704 | } |
| 714 | 705 | |
| 715 | 706 | return { |
| @@ -797,9 +788,7 @@ const createKeychainSecretMaterialProvider = (): SecretMaterialProvider => ({ · 81 unmodified lines | ||
| 797 | 788 | }, |
| 798 | 789 | ); |
| 799 | 790 | if (Option.isNone(updated)) { |
| 800 | − return yield* Effect.fail( | |
| 801 | − new Error(`keychain.update: secret material not found: ${loaded.material.id}`), | |
| 802 | − ); | |
| 791 | + return yield* runtimeEffectError("local/secret-material-providers", `keychain.update: secret material not found: ${loaded.material.id}`); | |
| 803 | 792 | } |
| 804 | 793 | |
| 805 | 794 | return { |
| @@ -849,7 +838,7 @@ const getSecretMaterialProvider = (input: { · 43 unmodified lines | ||
| 849 | 838 | return Effect.succeed(provider); |
| 850 | 839 | } |
| 851 | 840 | |
| 852 | − return Effect.fail(new Error(`Unsupported secret provider: ${input.providerId}`)); | |
| 841 | + return Effect.fail(runtimeEffectError("local/secret-material-providers", `Unsupported secret provider: ${input.providerId}`)); | |
| 853 | 842 | }; |
| 854 | 843 | |
| 855 | 844 | const createSecretMaterialProviderRuntime = (input: { |
| @@ -946,25 +935,25 @@ const resolveConfiguredSecretProvider = (input: { · 90 unmodified lines | ||
| 946 | 935 | }): Effect.Effect<string, Error, never> => { |
| 947 | 936 | const providerAlias = fromConfigSecretProviderId(input.ref.providerId); |
| 948 | 937 | if (providerAlias === null) { |
| 949 | − return Effect.fail(new Error(`Unsupported secret provider: ${input.ref.providerId}`)); | |
| 938 | + return Effect.fail(runtimeEffectError("local/secret-material-providers", `Unsupported secret provider: ${input.ref.providerId}`)); | |
| 950 | 939 | } |
| 951 | 940 | |
| 952 | 941 | const provider = input.runtime.localConfig?.secrets?.providers?.[providerAlias]; |
| 953 | 942 | if (!provider) { |
| 954 | 943 | return Effect.fail( |
| 955 | − new Error(`Config secret provider "${providerAlias}" is not configured`), | |
| 944 | + runtimeEffectError("local/secret-material-providers", `Config secret provider "${providerAlias}" is not configured`), | |
| 956 | 945 | ); |
| 957 | 946 | } |
| 958 | 947 | if (input.runtime.workspaceRoot === null) { |
| 959 | 948 | return Effect.fail( |
| 960 | − new Error(`Config secret provider "${providerAlias}" requires a workspace root`), | |
| 949 | + runtimeEffectError("local/secret-material-providers", `Config secret provider "${providerAlias}" requires a workspace root`), | |
| 961 | 950 | ); |
| 962 | 951 | } |
| 963 | 952 | |
| 964 | 953 | if (provider.source === "env") { |
| 965 | 954 | const value = ensureNonEmptyString(input.runtime.env[input.ref.handle]); |
| 966 | 955 | return value === null |
| 967 | − ? Effect.fail(new Error(`Environment variable ${input.ref.handle} is not set`)) | |
| 956 | + ? Effect.fail(runtimeEffectError("local/secret-material-providers", `Environment variable ${input.ref.handle} is not set`)) | |
| 968 | 957 | : Effect.succeed(value); |
| 969 | 958 | } |
| 970 | 959 | |
| @@ -979,17 +968,17 @@ const resolveConfiguredSecretProvider = (input: { · 8 unmodified lines | ||
| 979 | 968 | const command = provider.command.trim(); |
| 980 | 969 | if (!isAbsolute(command)) { |
| 981 | 970 | return Effect.fail( |
| 982 | − new Error(`Exec secret provider command must be absolute: ${command}`), | |
| 971 | + runtimeEffectError("local/secret-material-providers", `Exec secret provider command must be absolute: ${command}`), | |
| 983 | 972 | ); |
| 984 | 973 | } |
| 985 | 974 | if (!isRegularFilePath(command, provider.allowSymlinkCommand ?? false)) { |
| 986 | 975 | return Effect.fail( |
| 987 | − new Error(`Exec secret provider command is not an allowed regular file: ${command}`), | |
| 976 | + runtimeEffectError("local/secret-material-providers", `Exec secret provider command is not an allowed regular file: ${command}`), | |
| 988 | 977 | ); |
| 989 | 978 | } |
| 990 | 979 | if (!ensureTrustedDir(command, provider.trustedDirs)) { |
| 991 | 980 | return Effect.fail( |
| 992 | − new Error(`Exec secret provider command is outside trustedDirs: ${command}`), | |
| 981 | + runtimeEffectError("local/secret-material-providers", `Exec secret provider command is outside trustedDirs: ${command}`), | |
| 993 | 982 | ); |
| 994 | 983 | } |
| 995 | 984 | |
| @@ -1032,7 +1021,7 @@ export const createDefaultSecretMaterialResolver = (input: { · 36 unmodified lines | ||
| 1032 | 1021 | Effect.catchAll(() => |
| 1033 | 1022 | fromConfigSecretProviderId(ref.providerId) !== null |
| 1034 | 1023 | ? Effect.succeed(null) |
| 1035 | − : Effect.fail(new Error(`Unsupported secret provider: ${ref.providerId}`)), | |
| 1024 | + : Effect.fail(runtimeEffectError("local/secret-material-providers", `Unsupported secret provider: ${ref.providerId}`)), | |
| 1036 | 1025 | ), |
| 1037 | 1026 | ); |
| 1038 | 1027 | |
| @@ -1072,9 +1061,7 @@ export const createDefaultSecretMaterialStorer = (input: { · 33 unmodified lines | ||
| 1072 | 1061 | }); |
| 1073 | 1062 | |
| 1074 | 1063 | if (!provider.store) { |
| 1075 | − return yield* Effect.fail( | |
| 1076 | − new Error(`Secret provider ${defaultStoreProviderId} does not support storing secret material`), | |
| 1077 | − ); | |
| 1064 | + return yield* runtimeEffectError("local/secret-material-providers", `Secret provider ${defaultStoreProviderId} does not support storing secret material`); | |
| 1078 | 1065 | } |
| 1079 | 1066 | |
| 1080 | 1067 | return yield* provider.store({ |
| @@ -1102,9 +1089,7 @@ export const createDefaultSecretMaterialUpdater = (input: { · 21 unmodified lines | ||
| 1102 | 1089 | }); |
| 1103 | 1090 | |
| 1104 | 1091 | if (!provider.update) { |
| 1105 | − return yield* Effect.fail( | |
| 1106 | − new Error(`Secret provider ${ref.providerId} does not support updating secret material`), | |
| 1107 | − ); | |
| 1092 | + return yield* runtimeEffectError("local/secret-material-providers", `Secret provider ${ref.providerId} does not support updating secret material`); | |
| 1108 | 1093 | } |
| 1109 | 1094 | |
| 1110 | 1095 | return yield* provider.update({ |
| @@ -136,25 +136,21 @@ const toToolPath = ( · 135 unmodified lines | ||
| 136 | 136 | rawSegments.at(-1) === "index" ? rawSegments.slice(0, -1) : rawSegments; |
| 137 | 137 | |
| 138 | 138 | if (segments.length === 0) { |
| 139 | − return yield* Effect.fail( | |
| 140 | − new LocalToolDefinitionError({ | |
| 139 | + return yield* new LocalToolDefinitionError({ | |
| 141 | 140 | message: `Invalid local tool path ${relativePath}: root index files are not supported`, |
| 142 | 141 | path: relativePath, |
| 143 | 142 | details: |
| 144 | 143 | "Root index files are not supported. Use a named file such as hello.ts or a nested index.ts.", |
| 145 | − }), | |
| 146 | − ); | |
| 144 | + }); | |
| 147 | 145 | } |
| 148 | 146 | |
| 149 | 147 | for (const segment of segments) { |
| 150 | 148 | if (!LOCAL_TOOL_SEGMENT_PATTERN.test(segment)) { |
| 151 | − return yield* Effect.fail( | |
| 152 | − new LocalToolDefinitionError({ | |
| 149 | + return yield* new LocalToolDefinitionError({ | |
| 153 | 150 | message: `Invalid local tool path ${relativePath}: segment ${segment} contains unsupported characters`, |
| 154 | 151 | path: relativePath, |
| 155 | 152 | details: `Tool path segments may only contain letters, numbers, underscores, and hyphens. Invalid segment: ${segment}`, |
| 156 | − }), | |
| 157 | − ); | |
| 153 | + }); | |
| 158 | 154 | } |
| 159 | 155 | } |
| 160 | 156 | |
| @@ -329,7 +325,7 @@ const transpileSourceFile = (input: { · 168 unmodified lines | ||
| 329 | 325 | transpiled.diagnostics, |
| 330 | 326 | ); |
| 331 | 327 | if (diagnosticError) { |
| 332 | − return yield* Effect.fail(diagnosticError); | |
| 328 | + return yield* diagnosticError; | |
| 333 | 329 | } |
| 334 | 330 | |
| 335 | 331 | return rewriteRelativeImportSpecifiers(transpiled.outputText); |
| @@ -608,26 +604,22 @@ export const loadLocalToolRuntime = ( · 272 unmodified lines | ||
| 608 | 604 | const toolPath = yield* toToolPath(relativePath); |
| 609 | 605 | const existing = seenToolPaths.get(toolPath); |
| 610 | 606 | if (existing) { |
| 611 | − return yield* Effect.fail( | |
| 612 | − new LocalToolPathConflictError({ | |
| 607 | + return yield* new LocalToolPathConflictError({ | |
| 613 | 608 | message: `Local tool path conflict for ${toolPath}`, |
| 614 | 609 | path: sourcePath, |
| 615 | 610 | otherPath: existing, |
| 616 | 611 | toolPath, |
| 617 | − }), | |
| 618 | − ); | |
| 612 | + }); | |
| 619 | 613 | } |
| 620 | 614 | |
| 621 | 615 | const artifactPath = artifactPaths.get(sourcePath); |
| 622 | 616 | if (!artifactPath) { |
| 623 | − return yield* Effect.fail( | |
| 624 | − new LocalToolImportError({ | |
| 617 | + return yield* new LocalToolImportError({ | |
| 625 | 618 | message: `Missing compiled artifact for local tool ${sourcePath}`, |
| 626 | 619 | path: sourcePath, |
| 627 | 620 | details: |
| 628 | 621 | "Expected a compiled local tool artifact, but none was produced.", |
| 629 | − }), | |
| 630 | − ); | |
| 622 | + }); | |
| 631 | 623 | } |
| 632 | 624 | |
| 633 | 625 | tools[toolPath] = yield* importLocalToolModule({ |
| @@ -248,12 +248,10 @@ export const getPolicy = (input: { · 247 unmodified lines | ||
| 248 | 248 | ); |
| 249 | 249 | const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; |
| 250 | 250 | if (policy === null) { |
| 251 | − return yield* Effect.fail( | |
| 252 | − policyOps.get.notFound( | |
| 251 | + return yield* policyOps.get.notFound( | |
| 253 | 252 | "Policy not found", |
| 254 | 253 | `workspaceId=${input.workspaceId} policyId=${input.policyId}`, |
| 255 | − ), | |
| 256 | − ); | |
| 254 | + ); | |
| 257 | 255 | } |
| 258 | 256 | return policy; |
| 259 | 257 | }); |
| @@ -277,12 +275,10 @@ export const updatePolicy = (input: { · 17 unmodified lines | ||
| 277 | 275 | ); |
| 278 | 276 | const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; |
| 279 | 277 | if (existing === null) { |
| 280 | − return yield* Effect.fail( | |
| 281 | − policyOps.update.notFound( | |
| 278 | + return yield* policyOps.update.notFound( | |
| 282 | 279 | "Policy not found", |
| 283 | 280 | `workspaceId=${input.workspaceId} policyId=${input.policyId}`, |
| 284 | − ), | |
| 285 | − ); | |
| 281 | + ); | |
| 286 | 282 | } |
| 287 | 283 | |
| 288 | 284 | const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); |
| @@ -29,6 +29,7 @@ import { · 28 unmodified lines | ||
| 29 | 29 | makeLocalStorageLayer, |
| 30 | 30 | } from "../local/storage"; |
| 31 | 31 | import { provideOptionalRuntimeLocalWorkspace } from "../local/runtime-context"; |
| 32 | +import { runtimeEffectError } from "../effect-errors"; | |
| 32 | 33 | |
| 33 | 34 | /** Run an Effect as a Promise, preserving the original error (not FiberFailure). */ |
| 34 | 35 | const runEffect = async <A>( |
| @@ -251,15 +252,11 @@ const promptForSourceCredentialSelection = (input: { · 216 unmodified lines | ||
| 251 | 252 | }) => |
| 252 | 253 | Effect.gen(function* () { |
| 253 | 254 | if (!input.onElicitation) { |
| 254 | − return yield* Effect.fail( | |
| 255 | − new Error("executor.sources.add requires an elicitation-capable host"), | |
| 256 | − ); | |
| 255 | + return yield* runtimeEffectError("sources/executor-tools", "executor.sources.add requires an elicitation-capable host"); | |
| 257 | 256 | } |
| 258 | 257 | |
| 259 | 258 | if (input.localServerBaseUrl === null) { |
| 260 | − return yield* Effect.fail( | |
| 261 | − new Error("executor.sources.add requires a local server base URL for credential capture"), | |
| 262 | − ); | |
| 259 | + return yield* runtimeEffectError("sources/executor-tools", "executor.sources.add requires a local server base URL for credential capture"); | |
| 263 | 260 | } |
| 264 | 261 | |
| 265 | 262 | const response: ElicitationResponse = yield* input.onElicitation({ |
| @@ -287,9 +284,7 @@ const promptForSourceCredentialSelection = (input: { · 21 unmodified lines | ||
| 287 | 284 | }).pipe(Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause)))); |
| 288 | 285 | |
| 289 | 286 | if (response.action !== "accept") { |
| 290 | − return yield* Effect.fail( | |
| 291 | − new Error(`Source credential setup was not completed for ${input.source.name}`), | |
| 292 | − ); | |
| 287 | + return yield* runtimeEffectError("sources/executor-tools", `Source credential setup was not completed for ${input.source.name}`); | |
| 293 | 288 | } |
| 294 | 289 | |
| 295 | 290 | const content = yield* Effect.try({ |
| @@ -8,6 +8,7 @@ import * as Effect from "effect/Effect"; · 7 unmodified lines | ||
| 8 | 8 | import { googleDiscoverySourceAdapter } from "./google-discovery"; |
| 9 | 9 | import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result"; |
| 10 | 10 | import { createSourceFromPayload } from "../source-definitions"; |
| 11 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 11 | 12 | |
| 12 | 13 | const fetchLiveDiscoveryDocument = async (): Promise<string> => { |
| 13 | 14 | const response = await fetch("https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest", { |
| @@ -90,7 +91,7 @@ describe("google discovery source adapter", () => { · 76 unmodified lines | ||
| 90 | 91 | const syncResult = await Effect.runPromise( |
| 91 | 92 | googleDiscoverySourceAdapter.syncCatalog({ |
| 92 | 93 | source, |
| 93 | − resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")), | |
| 94 | + resolveSecretMaterial: () => Effect.fail(runtimeEffectError("sources/source-adapters/google-discovery.test", "unexpected secret lookup")), | |
| 94 | 95 | resolveAuthMaterialForSlot: () => |
| 95 | 96 | Effect.succeed({ |
| 96 | 97 | placements: [], |
| @@ -7,6 +7,7 @@ import { vi } from "vitest"; · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 8 | import { createSourceFromPayload } from "../source-definitions"; |
| 9 | 9 | import { graphqlSourceAdapter } from "./graphql"; |
| 10 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 10 | 11 | |
| 11 | 12 | describe("graphql source adapter", () => { |
| 12 | 13 | it("fails sync when introspection never responds", async () => { |
| @@ -71,7 +72,7 @@ describe("graphql source adapter", () => { · 58 unmodified lines | ||
| 71 | 72 | Effect.runPromise( |
| 72 | 73 | graphqlSourceAdapter.syncCatalog({ |
| 73 | 74 | source, |
| 74 | − resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")), | |
| 75 | + resolveSecretMaterial: () => Effect.fail(runtimeEffectError("sources/source-adapters/graphql.test", "unexpected secret lookup")), | |
| 75 | 76 | resolveAuthMaterialForSlot: () => |
| 76 | 77 | Effect.succeed({ |
| 77 | 78 | placements: [], |
| @@ -10,6 +10,7 @@ import { · 9 unmodified lines | ||
| 10 | 10 | type Source, |
| 11 | 11 | type SourceAdapter, |
| 12 | 12 | } from "@executor/source-core"; |
| 13 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 13 | 14 | |
| 14 | 15 | const InternalBindingConfigSchema = Schema.Struct({}); |
| 15 | 16 | |
| @@ -37,9 +38,7 @@ const internalBindingConfigFromSource = ( · 21 unmodified lines | ||
| 37 | 38 | "headers", |
| 38 | 39 | ]) |
| 39 | 40 | ) { |
| 40 | − return yield* Effect.fail( | |
| 41 | − new Error("internal sources cannot define HTTP source settings"), | |
| 42 | − ); | |
| 41 | + return yield* runtimeEffectError("sources/source-adapters/internal", "internal sources cannot define HTTP source settings"); | |
| 43 | 42 | } |
| 44 | 43 | |
| 45 | 44 | return yield* decodeSourceBindingPayload({ |
| @@ -123,6 +122,6 @@ export const internalSourceAdapter: SourceAdapter = { · 77 unmodified lines | ||
| 123 | 122 | ), |
| 124 | 123 | invoke: () => |
| 125 | 124 | Effect.fail( |
| 126 | − new Error("Internal sources do not support persisted adapter invocation"), | |
| 125 | + runtimeEffectError("sources/source-adapters/internal", "Internal sources do not support persisted adapter invocation"), | |
| 127 | 126 | ), |
| 128 | 127 | }; |
| @@ -30,6 +30,7 @@ import { · 29 unmodified lines | ||
| 30 | 30 | import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result"; |
| 31 | 31 | import { createSourceFromPayload } from "../source-definitions"; |
| 32 | 32 | import { mcpSourceAdapter } from "./mcp"; |
| 33 | +import { runtimeEffectError } from "../../effect-errors"; | |
| 33 | 34 | |
| 34 | 35 | type RealMcpServer = { |
| 35 | 36 | endpoint: string; |
| @@ -390,7 +391,7 @@ describe("mcp source adapter", () => { · 354 unmodified lines | ||
| 390 | 391 | const syncResult = yield* mcpSourceAdapter.syncCatalog({ |
| 391 | 392 | source, |
| 392 | 393 | resolveSecretMaterial: () => |
| 393 | − Effect.fail(new Error("unexpected secret lookup")), | |
| 394 | + Effect.fail(runtimeEffectError("sources/source-adapters/mcp.test", "unexpected secret lookup")), | |
| 394 | 395 | resolveAuthMaterialForSlot: () => |
| 395 | 396 | Effect.succeed({ |
| 396 | 397 | placements: [], |
| @@ -478,7 +479,7 @@ describe("mcp source adapter", () => { · 81 unmodified lines | ||
| 478 | 479 | const syncResult = yield* mcpSourceAdapter.syncCatalog({ |
| 479 | 480 | source, |
| 480 | 481 | resolveSecretMaterial: () => |
| 481 | − Effect.fail(new Error("unexpected secret lookup")), | |
| 482 | + Effect.fail(runtimeEffectError("sources/source-adapters/mcp.test", "unexpected secret lookup")), | |
| 482 | 483 | resolveAuthMaterialForSlot: () => |
| 483 | 484 | Effect.succeed({ |
| 484 | 485 | placements: [], |
| @@ -576,7 +577,7 @@ describe("mcp source adapter", () => { · 91 unmodified lines | ||
| 576 | 577 | const syncResult = yield* mcpSourceAdapter.syncCatalog({ |
| 577 | 578 | source, |
| 578 | 579 | resolveSecretMaterial: () => |
| 579 | − Effect.fail(new Error("unexpected secret lookup")), | |
| 580 | + Effect.fail(runtimeEffectError("sources/source-adapters/mcp.test", "unexpected secret lookup")), | |
| 580 | 581 | resolveAuthMaterialForSlot: () => Effect.succeed(resolvedAuth), |
| 581 | 582 | }); |
| 582 | 583 | const snapshot = snapshotFromSourceCatalogSyncResult(syncResult); |
| @@ -99,6 +99,7 @@ import { · 98 unmodified lines | ||
| 99 | 99 | } from "./source-store"; |
| 100 | 100 | import type { WorkspaceStorageServices } from "../local/storage"; |
| 101 | 101 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "../store"; |
| 102 | +import { runtimeEffectError } from "../effect-errors"; | |
| 102 | 103 | |
| 103 | 104 | const trimOrNull = (value: string | null | undefined): string | null => { |
| 104 | 105 | if (value === null || value === undefined) { |
| @@ -214,7 +215,7 @@ const probeMcpSourceWithoutAuth = ( · 109 unmodified lines | ||
| 214 | 215 | ) => |
| 215 | 216 | Effect.gen(function* () { |
| 216 | 217 | if (!sourceAdapterRequiresInteractiveConnect(source.kind)) { |
| 217 | − return yield* Effect.fail(new Error(`Expected MCP source, received ${source.kind}`)); | |
| 218 | + return yield* runtimeEffectError("sources/source-auth-service", `Expected MCP source, received ${source.kind}`); | |
| 218 | 219 | } |
| 219 | 220 | const bindingState = yield* sourceBindingStateFromSource(source); |
| 220 | 221 | |
| @@ -739,9 +740,7 @@ const materializeExecutorHttpAuth = (input: { · 518 unmodified lines | ||
| 739 | 740 | storeSecretMaterial: input.storeSecretMaterial, |
| 740 | 741 | }); |
| 741 | 742 | if (tokenRef === null) { |
| 742 | − return yield* Effect.fail( | |
| 743 | − new Error("Bearer auth requires token or tokenRef"), | |
| 744 | − ); | |
| 743 | + return yield* runtimeEffectError("sources/source-auth-service", "Bearer auth requires token or tokenRef"); | |
| 745 | 744 | } |
| 746 | 745 | |
| 747 | 746 | return { |
| @@ -770,9 +769,7 @@ const materializeExecutorHttpAuth = (input: { · 22 unmodified lines | ||
| 770 | 769 | storeSecretMaterial: input.storeSecretMaterial, |
| 771 | 770 | }); |
| 772 | 771 | if (accessTokenRef === null) { |
| 773 | − return yield* Effect.fail( | |
| 774 | − new Error("OAuth2 auth requires accessToken or accessTokenRef"), | |
| 775 | − ); | |
| 772 | + return yield* runtimeEffectError("sources/source-auth-service", "OAuth2 auth requires accessToken or accessTokenRef"); | |
| 776 | 773 | } |
| 777 | 774 | |
| 778 | 775 | const refreshTokenRef = yield* materializeSecretRefInput({ |
| @@ -908,9 +905,7 @@ const upsertSourceOauthClient = (input: { · 129 unmodified lines | ||
| 908 | 905 | }) |
| 909 | 906 | : null; |
| 910 | 907 | if (setupConfig === null) { |
| 911 | − return yield* Effect.fail( | |
| 912 | − new Error(`Source ${input.source.id} does not support OAuth client configuration`), | |
| 913 | − ); | |
| 908 | + return yield* runtimeEffectError("sources/source-auth-service", `Source ${input.source.id} does not support OAuth client configuration`); | |
| 914 | 909 | } |
| 915 | 910 | |
| 916 | 911 | const existing = yield* input.rows.sourceOauthClients.getByWorkspaceSourceAndProvider({ |
| @@ -1075,9 +1070,7 @@ const resolveWorkspaceOauthClientById = (input: { · 158 unmodified lines | ||
| 1075 | 1070 | existing.value.workspaceId !== input.workspaceId |
| 1076 | 1071 | || existing.value.providerKey !== input.providerKey |
| 1077 | 1072 | ) { |
| 1078 | − return yield* Effect.fail( | |
| 1079 | − new Error(`Workspace OAuth client ${input.oauthClientId} is not valid for ${input.providerKey}`), | |
| 1080 | − ); | |
| 1073 | + return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client ${input.oauthClientId} is not valid for ${input.providerKey}`); | |
| 1081 | 1074 | } |
| 1082 | 1075 | |
| 1083 | 1076 | return { |
| @@ -1152,9 +1145,7 @@ const upsertProviderAuthGrant = (input: { · 68 unmodified lines | ||
| 1152 | 1145 | } |
| 1153 | 1146 | |
| 1154 | 1147 | if (refreshTokenRef === null) { |
| 1155 | − return yield* Effect.fail( | |
| 1156 | − new Error(`Provider auth grant for ${input.providerKey} is missing a refresh token`), | |
| 1157 | − ); | |
| 1148 | + return yield* runtimeEffectError("sources/source-auth-service", `Provider auth grant for ${input.providerKey} is missing a refresh token`); | |
| 1158 | 1149 | } |
| 1159 | 1150 | |
| 1160 | 1151 | const now = Date.now(); |
| @@ -1340,7 +1331,7 @@ const startProviderOauthBatchCredentialSetup = (input: { · 179 unmodified lines | ||
| 1340 | 1331 | }, Error, WorkspaceStorageServices> => |
| 1341 | 1332 | Effect.gen(function* () { |
| 1342 | 1333 | if (input.targetSources.length === 0) { |
| 1343 | − return yield* Effect.fail(new Error("Provider OAuth setup requires at least one target source")); | |
| 1334 | + return yield* runtimeEffectError("sources/source-auth-service", "Provider OAuth setup requires at least one target source"); | |
| 1344 | 1335 | } |
| 1345 | 1336 | |
| 1346 | 1337 | const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`); |
| @@ -1580,7 +1571,7 @@ const connectMcpSourceInternal = (input: { · 233 unmodified lines | ||
| 1580 | 1571 | Effect.flatMap((source) => |
| 1581 | 1572 | sourceAdapterRequiresInteractiveConnect(source.kind) |
| 1582 | 1573 | ? Effect.succeed(source) |
| 1583 | − : Effect.fail(new Error(`Expected MCP source, received ${source.kind}`)), | |
| 1574 | + : Effect.fail(runtimeEffectError("sources/source-auth-service", `Expected MCP source, received ${source.kind}`)), | |
| 1584 | 1575 | ), |
| 1585 | 1576 | ) |
| 1586 | 1577 | : input.sourceStore.loadSourcesInWorkspace(input.workspaceId, { |
| @@ -1712,9 +1703,7 @@ const connectMcpSourceInternal = (input: { · 125 unmodified lines | ||
| 1712 | 1703 | |
| 1713 | 1704 | const localServerBaseUrl = trimOrNull(input.baseUrl) ?? input.getLocalServerBaseUrl?.() ?? null; |
| 1714 | 1705 | if (!localServerBaseUrl) { |
| 1715 | − return yield* Effect.fail( | |
| 1716 | − new Error("Local executor server base URL is unavailable for source credential setup"), | |
| 1717 | − ); | |
| 1706 | + return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for source credential setup"); | |
| 1718 | 1707 | } |
| 1719 | 1708 | |
| 1720 | 1709 | const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`); |
| @@ -2145,9 +2134,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 424 unmodified lines | ||
| 2145 | 2134 | } |
| 2146 | 2135 | |
| 2147 | 2136 | if (workspaceOauthClient === null) { |
| 2148 | − return yield* Effect.fail( | |
| 2149 | − new Error("Google shared auth requires a workspace OAuth client"), | |
| 2150 | − ); | |
| 2137 | + return yield* runtimeEffectError("sources/source-auth-service", "Google shared auth requires a workspace OAuth client"); | |
| 2151 | 2138 | } |
| 2152 | 2139 | |
| 2153 | 2140 | const requiredScopes = normalizeScopes(googleSetupConfig.scopes); |
| @@ -2184,9 +2171,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 30 unmodified lines | ||
| 2184 | 2171 | const requestBaseUrl = trimOrNull(input.baseUrl); |
| 2185 | 2172 | const baseUrl = requestBaseUrl ?? input.getLocalServerBaseUrl?.() ?? null; |
| 2186 | 2173 | if (baseUrl === null) { |
| 2187 | − return yield* Effect.fail( | |
| 2188 | − new Error("Local executor server base URL is unavailable for Google OAuth setup"), | |
| 2189 | − ); | |
| 2174 | + return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for Google OAuth setup"); | |
| 2190 | 2175 | } |
| 2191 | 2176 | |
| 2192 | 2177 | const oauthRequired = yield* startProviderOauthBatchCredentialSetup({ |
| @@ -2318,7 +2303,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 125 unmodified lines | ||
| 2318 | 2303 | }): Effect.Effect<ConnectGoogleDiscoveryBatchResult, Error, WorkspaceStorageServices> => |
| 2319 | 2304 | Effect.gen(function* () { |
| 2320 | 2305 | if (input.sourceInput.sources.length === 0) { |
| 2321 | − return yield* Effect.fail(new Error("Google batch connect requires at least one source")); | |
| 2306 | + return yield* runtimeEffectError("sources/source-auth-service", "Google batch connect requires at least one source"); | |
| 2322 | 2307 | } |
| 2323 | 2308 | |
| 2324 | 2309 | const existingSources = yield* input.sourceStore.loadSourcesInWorkspace( |
| @@ -2426,9 +2411,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 101 unmodified lines | ||
| 2426 | 2411 | }) |
| 2427 | 2412 | : null; |
| 2428 | 2413 | if (setupConfig === null) { |
| 2429 | − return yield* Effect.fail( | |
| 2430 | − new Error(`Source ${persistedDraft.id} does not support Google shared auth`), | |
| 2431 | − ); | |
| 2414 | + return yield* runtimeEffectError("sources/source-auth-service", `Source ${persistedDraft.id} does not support Google shared auth`); | |
| 2432 | 2415 | } |
| 2433 | 2416 | |
| 2434 | 2417 | return { |
| @@ -2451,9 +2434,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 16 unmodified lines | ||
| 2451 | 2434 | providerKey: baseSetupConfig.providerKey, |
| 2452 | 2435 | }); |
| 2453 | 2436 | if (workspaceOauthClient === null) { |
| 2454 | − return yield* Effect.fail( | |
| 2455 | − new Error(`Workspace OAuth client not found: ${input.sourceInput.workspaceOauthClientId}`), | |
| 2456 | − ); | |
| 2437 | + return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client not found: ${input.sourceInput.workspaceOauthClientId}`); | |
| 2457 | 2438 | } |
| 2458 | 2439 | |
| 2459 | 2440 | const reusableGrant = yield* findReusableProviderGrant({ |
| @@ -2491,9 +2472,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 31 unmodified lines | ||
| 2491 | 2472 | |
| 2492 | 2473 | const baseUrl = trimOrNull(input.sourceInput.baseUrl) ?? input.getLocalServerBaseUrl?.() ?? null; |
| 2493 | 2474 | if (baseUrl === null) { |
| 2494 | − return yield* Effect.fail( | |
| 2495 | − new Error("Local executor server base URL is unavailable for Google OAuth setup"), | |
| 2496 | − ); | |
| 2475 | + return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for Google OAuth setup"); | |
| 2497 | 2476 | } |
| 2498 | 2477 | |
| 2499 | 2478 | const oauthRequired = yield* startProviderOauthBatchCredentialSetup({ |
| @@ -2706,7 +2685,7 @@ const createRuntimeSourceConnectionService = ( · 206 unmodified lines | ||
| 2706 | 2685 | baseUrl: options?.baseUrl, |
| 2707 | 2686 | resolveSecretMaterial: input.resolveSecretMaterial, |
| 2708 | 2687 | }) |
| 2709 | − : Effect.fail(new Error(`Unsupported executor source input: ${JSON.stringify(sourceInput)}`))).pipe( | |
| 2688 | + : Effect.fail(runtimeEffectError("sources/source-auth-service", `Unsupported executor source input: ${JSON.stringify(sourceInput)}`))).pipe( | |
| 2710 | 2689 | Effect.flatMap(mirrorLocalSourceResult), |
| 2711 | 2690 | ), |
| 2712 | 2691 | ), |
| @@ -2772,9 +2751,7 @@ const createRuntimeSourceConnectionService = ( · 59 unmodified lines | ||
| 2772 | 2751 | }); |
| 2773 | 2752 | const stored = yield* input.rows.workspaceOauthClients.getById(created.id); |
| 2774 | 2753 | if (Option.isNone(stored)) { |
| 2775 | − return yield* Effect.fail( | |
| 2776 | − new Error(`Workspace OAuth client ${created.id} was not persisted`), | |
| 2777 | − ); | |
| 2754 | + return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client ${created.id} was not persisted`); | |
| 2778 | 2755 | } |
| 2779 | 2756 | |
| 2780 | 2757 | return stored.value; |
| @@ -2792,11 +2769,9 @@ const createRuntimeSourceConnectionService = ( · 11 unmodified lines | ||
| 2792 | 2769 | const grants = yield* input.rows.providerAuthGrants.listByWorkspaceId(workspaceId); |
| 2793 | 2770 | const dependentGrant = grants.find((grant) => grant.oauthClientId === oauthClientId); |
| 2794 | 2771 | if (dependentGrant) { |
| 2795 | − return yield* Effect.fail( | |
| 2796 | − new Error( | |
| 2772 | + return yield* runtimeEffectError("sources/source-auth-service", | |
| 2797 | 2773 | `Workspace OAuth client ${oauthClientId} is still referenced by provider grant ${dependentGrant.id}`, |
| 2798 | − ), | |
| 2799 | − ); | |
| 2774 | + ); | |
| 2800 | 2775 | } |
| 2801 | 2776 | |
| 2802 | 2777 | const secretRef = sourceOauthClientSecretRef(oauthClient.value); |
| @@ -2835,9 +2810,7 @@ const createRuntimeSourceOAuthSessionService = ( · 32 unmodified lines | ||
| 2835 | 2810 | Effect.gen(function* () { |
| 2836 | 2811 | const resolvedBaseUrl = trimOrNull(oauthInput.baseUrl) ?? input.getLocalServerBaseUrl?.() ?? null; |
| 2837 | 2812 | if (!resolvedBaseUrl) { |
| 2838 | − return yield* Effect.fail( | |
| 2839 | − new Error("Local executor server base URL is unavailable for OAuth setup"), | |
| 2840 | − ); | |
| 2813 | + return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for OAuth setup"); | |
| 2841 | 2814 | } |
| 2842 | 2815 | |
| 2843 | 2816 | const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`); |
| @@ -2895,17 +2868,17 @@ const createRuntimeSourceOAuthSessionService = ( · 51 unmodified lines | ||
| 2895 | 2868 | provideLocalWorkspace(Effect.gen(function* () { |
| 2896 | 2869 | const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state); |
| 2897 | 2870 | if (Option.isNone(sessionOption)) { |
| 2898 | − return yield* Effect.fail(new Error(`Source auth session not found for state ${state}`)); | |
| 2871 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session not found for state ${state}`); | |
| 2899 | 2872 | } |
| 2900 | 2873 | |
| 2901 | 2874 | const session = sessionOption.value; |
| 2902 | 2875 | const sessionData = decodeMcpSourceAuthSessionData(session); |
| 2903 | 2876 | if (session.status === "completed") { |
| 2904 | − return yield* Effect.fail(new Error(`Source auth session ${session.id} is already completed`)); | |
| 2877 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is already completed`); | |
| 2905 | 2878 | } |
| 2906 | 2879 | |
| 2907 | 2880 | if (session.status !== "pending") { |
| 2908 | − return yield* Effect.fail(new Error(`Source auth session ${session.id} is not pending`)); | |
| 2881 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is not pending`); | |
| 2909 | 2882 | } |
| 2910 | 2883 | |
| 2911 | 2884 | if (trimOrNull(error) !== null) { |
| @@ -2922,20 +2895,20 @@ const createRuntimeSourceOAuthSessionService = ( · 10 unmodified lines | ||
| 2922 | 2895 | }), |
| 2923 | 2896 | ); |
| 2924 | 2897 | |
| 2925 | − return yield* Effect.fail(new Error(reason)); | |
| 2898 | + return yield* runtimeEffectError("sources/source-auth-service", reason); | |
| 2926 | 2899 | } |
| 2927 | 2900 | |
| 2928 | 2901 | const authorizationCode = trimOrNull(code); |
| 2929 | 2902 | if (authorizationCode === null) { |
| 2930 | − return yield* Effect.fail(new Error("Missing OAuth authorization code")); | |
| 2903 | + return yield* runtimeEffectError("sources/source-auth-service", "Missing OAuth authorization code"); | |
| 2931 | 2904 | } |
| 2932 | 2905 | |
| 2933 | 2906 | if (sessionData.codeVerifier === null) { |
| 2934 | − return yield* Effect.fail(new Error("OAuth session is missing the PKCE code verifier")); | |
| 2907 | + return yield* runtimeEffectError("sources/source-auth-service", "OAuth session is missing the PKCE code verifier"); | |
| 2935 | 2908 | } |
| 2936 | 2909 | |
| 2937 | 2910 | if (sessionData.scope !== null && sessionData.scope !== "mcp") { |
| 2938 | − return yield* Effect.fail(new Error(`Unsupported OAuth provider: ${sessionData.scope}`)); | |
| 2911 | + return yield* runtimeEffectError("sources/source-auth-service", `Unsupported OAuth provider: ${sessionData.scope}`); | |
| 2939 | 2912 | } |
| 2940 | 2913 | |
| 2941 | 2914 | const exchanged = yield* exchangeMcpOAuthAuthorizationCode({ |
| @@ -3007,24 +2980,18 @@ const createRuntimeSourceOAuthSessionService = ( · 65 unmodified lines | ||
| 3007 | 2980 | provideLocalWorkspace(Effect.gen(function* () { |
| 3008 | 2981 | const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state); |
| 3009 | 2982 | if (Option.isNone(sessionOption)) { |
| 3010 | − return yield* Effect.fail(new Error(`Source auth session not found for state ${state}`)); | |
| 2983 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session not found for state ${state}`); | |
| 3011 | 2984 | } |
| 3012 | 2985 | |
| 3013 | 2986 | const session = sessionOption.value; |
| 3014 | 2987 | if (session.workspaceId !== workspaceId) { |
| 3015 | − return yield* Effect.fail( | |
| 3016 | − new Error(`Source auth session ${session.id} does not match workspaceId=${workspaceId}`), | |
| 3017 | − ); | |
| 2988 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} does not match workspaceId=${workspaceId}`); | |
| 3018 | 2989 | } |
| 3019 | 2990 | if ((session.actorAccountId ?? null) !== (actorAccountId ?? null)) { |
| 3020 | − return yield* Effect.fail( | |
| 3021 | − new Error(`Source auth session ${session.id} does not match the active account`), | |
| 3022 | − ); | |
| 2991 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} does not match the active account`); | |
| 3023 | 2992 | } |
| 3024 | 2993 | if (session.providerKind !== "oauth2_provider_batch") { |
| 3025 | − return yield* Effect.fail( | |
| 3026 | − new Error(`Source auth session ${session.id} is not a provider batch OAuth session`), | |
| 3027 | − ); | |
| 2994 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is not a provider batch OAuth session`); | |
| 3028 | 2995 | } |
| 3029 | 2996 | if (session.status === "completed") { |
| 3030 | 2997 | const sessionData = decodeProviderOauthBatchSourceAuthSessionData(session); |
| @@ -3044,9 +3011,7 @@ const createRuntimeSourceOAuthSessionService = ( · 13 unmodified lines | ||
| 3044 | 3011 | } satisfies CompleteProviderOauthCallbackResult; |
| 3045 | 3012 | } |
| 3046 | 3013 | if (session.status !== "pending") { |
| 3047 | − return yield* Effect.fail( | |
| 3048 | − new Error(`Source auth session ${session.id} is not pending`), | |
| 3049 | − ); | |
| 3014 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is not pending`); | |
| 3050 | 3015 | } |
| 3051 | 3016 | |
| 3052 | 3017 | const sessionData = decodeProviderOauthBatchSourceAuthSessionData(session); |
| @@ -3061,15 +3026,15 @@ const createRuntimeSourceOAuthSessionService = ( · 8 unmodified lines | ||
| 3061 | 3026 | errorText: reason, |
| 3062 | 3027 | }), |
| 3063 | 3028 | ); |
| 3064 | − return yield* Effect.fail(new Error(reason)); | |
| 3029 | + return yield* runtimeEffectError("sources/source-auth-service", reason); | |
| 3065 | 3030 | } |
| 3066 | 3031 | |
| 3067 | 3032 | const authorizationCode = trimOrNull(code); |
| 3068 | 3033 | if (authorizationCode === null) { |
| 3069 | − return yield* Effect.fail(new Error("Missing OAuth authorization code")); | |
| 3034 | + return yield* runtimeEffectError("sources/source-auth-service", "Missing OAuth authorization code"); | |
| 3070 | 3035 | } |
| 3071 | 3036 | if (sessionData.codeVerifier === null) { |
| 3072 | − return yield* Effect.fail(new Error("OAuth session is missing the PKCE code verifier")); | |
| 3037 | + return yield* runtimeEffectError("sources/source-auth-service", "OAuth session is missing the PKCE code verifier"); | |
| 3073 | 3038 | } |
| 3074 | 3039 | |
| 3075 | 3040 | const oauthClient = yield* resolveWorkspaceOauthClientById({ |
| @@ -3079,9 +3044,7 @@ const createRuntimeSourceOAuthSessionService = ( · 3 unmodified lines | ||
| 3079 | 3044 | providerKey: sessionData.providerKey, |
| 3080 | 3045 | }); |
| 3081 | 3046 | if (oauthClient === null) { |
| 3082 | − return yield* Effect.fail( | |
| 3083 | − new Error(`Workspace OAuth client not found: ${sessionData.oauthClientId}`), | |
| 3084 | − ); | |
| 3047 | + return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client not found: ${sessionData.oauthClientId}`); | |
| 3085 | 3048 | } |
| 3086 | 3049 | |
| 3087 | 3050 | let clientSecret: string | null = null; |
| @@ -3185,24 +3148,20 @@ const createRuntimeSourceOAuthSessionService = ( · 97 unmodified lines | ||
| 3185 | 3148 | provideLocalWorkspace(Effect.gen(function* () { |
| 3186 | 3149 | const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state); |
| 3187 | 3150 | if (Option.isNone(sessionOption)) { |
| 3188 | − return yield* Effect.fail(new Error(`Source auth session not found for state ${state}`)); | |
| 3151 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session not found for state ${state}`); | |
| 3189 | 3152 | } |
| 3190 | 3153 | |
| 3191 | 3154 | const session = sessionOption.value; |
| 3192 | 3155 | if (session.workspaceId !== workspaceId || session.sourceId !== sourceId) { |
| 3193 | − return yield* Effect.fail( | |
| 3194 | − new Error( | |
| 3156 | + return yield* runtimeEffectError("sources/source-auth-service", | |
| 3195 | 3157 | `Source auth session ${session.id} does not match workspaceId=${workspaceId} sourceId=${sourceId}`, |
| 3196 | − ), | |
| 3197 | − ); | |
| 3158 | + ); | |
| 3198 | 3159 | } |
| 3199 | 3160 | if ( |
| 3200 | 3161 | actorAccountId !== undefined |
| 3201 | 3162 | && (session.actorAccountId ?? null) !== (actorAccountId ?? null) |
| 3202 | 3163 | ) { |
| 3203 | − return yield* Effect.fail( | |
| 3204 | − new Error(`Source auth session ${session.id} does not match the active account`), | |
| 3205 | − ); | |
| 3164 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} does not match the active account`); | |
| 3206 | 3165 | } |
| 3207 | 3166 | |
| 3208 | 3167 | const source = yield* input.sourceStore.loadSourceById({ |
| @@ -3219,9 +3178,7 @@ const createRuntimeSourceOAuthSessionService = ( · 10 unmodified lines | ||
| 3219 | 3178 | } |
| 3220 | 3179 | |
| 3221 | 3180 | if (session.status !== "pending") { |
| 3222 | − return yield* Effect.fail( | |
| 3223 | − new Error(`Source auth session ${session.id} is not pending`), | |
| 3224 | − ); | |
| 3181 | + return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is not pending`); | |
| 3225 | 3182 | } |
| 3226 | 3183 | |
| 3227 | 3184 | const sessionData = session.providerKind === "oauth2_pkce" |
| @@ -3260,16 +3217,16 @@ const createRuntimeSourceOAuthSessionService = ( · 32 unmodified lines | ||
| 3260 | 3217 | }, |
| 3261 | 3218 | }); |
| 3262 | 3219 | |
| 3263 | − return yield* Effect.fail(new Error(reason)); | |
| 3220 | + return yield* runtimeEffectError("sources/source-auth-service", reason); | |
| 3264 | 3221 | } |
| 3265 | 3222 | |
| 3266 | 3223 | const authorizationCode = trimOrNull(code); |
| 3267 | 3224 | if (authorizationCode === null) { |
| 3268 | − return yield* Effect.fail(new Error("Missing OAuth authorization code")); | |
| 3225 | + return yield* runtimeEffectError("sources/source-auth-service", "Missing OAuth authorization code"); | |
| 3269 | 3226 | } |
| 3270 | 3227 | |
| 3271 | 3228 | if (sessionData.codeVerifier === null) { |
| 3272 | − return yield* Effect.fail(new Error("OAuth session is missing the PKCE code verifier")); | |
| 3229 | + return yield* runtimeEffectError("sources/source-auth-service", "OAuth session is missing the PKCE code verifier"); | |
| 3273 | 3230 | } |
| 3274 | 3231 | |
| 3275 | 3232 | const now = Date.now(); |
| @@ -3294,9 +3251,7 @@ const createRuntimeSourceOAuthSessionService = ( · 18 unmodified lines | ||
| 3294 | 3251 | }); |
| 3295 | 3252 | const refreshToken = trimOrNull(exchanged.refresh_token); |
| 3296 | 3253 | if (refreshToken === null) { |
| 3297 | − return yield* Effect.fail( | |
| 3298 | − new Error("OAuth authorization did not return a refresh token"), | |
| 3299 | − ); | |
| 3254 | + return yield* runtimeEffectError("sources/source-auth-service", "OAuth authorization did not return a refresh token"); | |
| 3300 | 3255 | } |
| 3301 | 3256 | |
| 3302 | 3257 | const refreshTokenRef = yield* input.storeSecretMaterial({ |
| @@ -32,6 +32,7 @@ import { · 31 unmodified lines | ||
| 32 | 32 | authArtifactFromSourceAuth, |
| 33 | 33 | sourceAuthFromAuthArtifact, |
| 34 | 34 | } from "../auth/auth-artifacts"; |
| 35 | +import { runtimeEffectError } from "../effect-errors"; | |
| 35 | 36 | |
| 36 | 37 | const trimOrNull = (value: string | null | undefined): string | null => { |
| 37 | 38 | if (value === null || value === undefined) { |
| @@ -96,7 +97,7 @@ const normalizeAuth = ( · 58 unmodified lines | ||
| 96 | 97 | const providerId = trimOrNull(auth.token.providerId); |
| 97 | 98 | const handle = trimOrNull(auth.token.handle); |
| 98 | 99 | if (providerId === null || handle === null) { |
| 99 | − return yield* Effect.fail(new Error("Bearer auth requires a token secret ref")); | |
| 100 | + return yield* runtimeEffectError("sources/source-definitions", "Bearer auth requires a token secret ref"); | |
| 100 | 101 | } |
| 101 | 102 | |
| 102 | 103 | return { |
| @@ -116,9 +117,7 @@ const normalizeAuth = ( · 13 unmodified lines | ||
| 116 | 117 | const refreshProviderId = trimOrNull(auth.refreshToken.providerId); |
| 117 | 118 | const refreshHandle = trimOrNull(auth.refreshToken.handle); |
| 118 | 119 | if (refreshProviderId === null || refreshHandle === null) { |
| 119 | − return yield* Effect.fail( | |
| 120 | − new Error("OAuth2 authorized-user auth requires a refresh token secret ref"), | |
| 121 | − ); | |
| 120 | + return yield* runtimeEffectError("sources/source-definitions", "OAuth2 authorized-user auth requires a refresh token secret ref"); | |
| 122 | 121 | } |
| 123 | 122 | |
| 124 | 123 | let clientSecret: { providerId: string; handle: string } | null = null; |
| @@ -126,9 +125,7 @@ const normalizeAuth = ( · 1 unmodified line | ||
| 126 | 125 | const clientSecretProviderId = trimOrNull(auth.clientSecret.providerId); |
| 127 | 126 | const clientSecretHandle = trimOrNull(auth.clientSecret.handle); |
| 128 | 127 | if (clientSecretProviderId === null || clientSecretHandle === null) { |
| 129 | − return yield* Effect.fail( | |
| 130 | − new Error("OAuth2 authorized-user client secret ref must include providerId and handle"), | |
| 131 | − ); | |
| 128 | + return yield* runtimeEffectError("sources/source-definitions", "OAuth2 authorized-user client secret ref must include providerId and handle"); | |
| 132 | 129 | } |
| 133 | 130 | clientSecret = { |
| 134 | 131 | providerId: clientSecretProviderId, |
| @@ -139,9 +136,7 @@ const normalizeAuth = ( · 4 unmodified lines | ||
| 139 | 136 | const tokenEndpoint = trimOrNull(auth.tokenEndpoint); |
| 140 | 137 | const clientId = trimOrNull(auth.clientId); |
| 141 | 138 | if (tokenEndpoint === null || clientId === null) { |
| 142 | − return yield* Effect.fail( | |
| 143 | − new Error("OAuth2 authorized-user auth requires tokenEndpoint and clientId"), | |
| 144 | − ); | |
| 139 | + return yield* runtimeEffectError("sources/source-definitions", "OAuth2 authorized-user auth requires tokenEndpoint and clientId"); | |
| 145 | 140 | } |
| 146 | 141 | |
| 147 | 142 | return { |
| @@ -165,9 +160,7 @@ const normalizeAuth = ( · 17 unmodified lines | ||
| 165 | 160 | const prefix = auth.prefix ?? "Bearer "; |
| 166 | 161 | const grantId = trimOrNull(auth.grantId); |
| 167 | 162 | if (grantId === null) { |
| 168 | − return yield* Effect.fail( | |
| 169 | − new Error("Provider grant auth requires a grantId"), | |
| 170 | − ); | |
| 163 | + return yield* runtimeEffectError("sources/source-definitions", "Provider grant auth requires a grantId"); | |
| 171 | 164 | } |
| 172 | 165 | |
| 173 | 166 | return { |
| @@ -187,9 +180,7 @@ const normalizeAuth = ( · 13 unmodified lines | ||
| 187 | 180 | const accessProviderId = trimOrNull(auth.accessToken.providerId); |
| 188 | 181 | const accessHandle = trimOrNull(auth.accessToken.handle); |
| 189 | 182 | if (redirectUri === null || accessProviderId === null || accessHandle === null) { |
| 190 | − return yield* Effect.fail( | |
| 191 | − new Error("MCP OAuth auth requires redirectUri and access token secret ref"), | |
| 192 | − ); | |
| 183 | + return yield* runtimeEffectError("sources/source-definitions", "MCP OAuth auth requires redirectUri and access token secret ref"); | |
| 193 | 184 | } |
| 194 | 185 | |
| 195 | 186 | let refreshToken: { providerId: string; handle: string } | null = null; |
| @@ -197,9 +188,7 @@ const normalizeAuth = ( · 1 unmodified line | ||
| 197 | 188 | const refreshProviderId = trimOrNull(auth.refreshToken.providerId); |
| 198 | 189 | const refreshHandle = trimOrNull(auth.refreshToken.handle); |
| 199 | 190 | if (refreshProviderId === null || refreshHandle === null) { |
| 200 | − return yield* Effect.fail( | |
| 201 | − new Error("MCP OAuth refresh token ref must include providerId and handle"), | |
| 202 | − ); | |
| 191 | + return yield* runtimeEffectError("sources/source-definitions", "MCP OAuth refresh token ref must include providerId and handle"); | |
| 203 | 192 | } |
| 204 | 193 | |
| 205 | 194 | refreshToken = { |
| @@ -234,7 +223,7 @@ const normalizeAuth = ( · 28 unmodified lines | ||
| 234 | 223 | const accessProviderId = trimOrNull(auth.accessToken.providerId); |
| 235 | 224 | const accessHandle = trimOrNull(auth.accessToken.handle); |
| 236 | 225 | if (accessProviderId === null || accessHandle === null) { |
| 237 | − return yield* Effect.fail(new Error("OAuth2 auth requires an access token secret ref")); | |
| 226 | + return yield* runtimeEffectError("sources/source-definitions", "OAuth2 auth requires an access token secret ref"); | |
| 238 | 227 | } |
| 239 | 228 | |
| 240 | 229 | let refreshToken: { providerId: string; handle: string } | null = null; |
| @@ -242,9 +231,7 @@ const normalizeAuth = ( · 1 unmodified line | ||
| 242 | 231 | const refreshProviderId = trimOrNull(auth.refreshToken.providerId); |
| 243 | 232 | const refreshHandle = trimOrNull(auth.refreshToken.handle); |
| 244 | 233 | if (refreshProviderId === null || refreshHandle === null) { |
| 245 | − return yield* Effect.fail( | |
| 246 | − new Error("OAuth2 refresh token ref must include providerId and handle"), | |
| 247 | − ); | |
| 234 | + return yield* runtimeEffectError("sources/source-definitions", "OAuth2 refresh token ref must include providerId and handle"); | |
| 248 | 235 | } |
| 249 | 236 | |
| 250 | 237 | refreshToken = { |
| @@ -273,9 +260,7 @@ const normalizeImportAuthPolicy = ( · 22 unmodified lines | ||
| 273 | 260 | const validateSourceImportAuth = (source: Source): Effect.Effect<Source, Error, never> => |
| 274 | 261 | Effect.gen(function* () { |
| 275 | 262 | if (source.importAuthPolicy !== "separate" && source.importAuth.kind !== "none") { |
| 276 | − return yield* Effect.fail( | |
| 277 | − new Error("importAuth must be none unless importAuthPolicy is separate"), | |
| 278 | − ); | |
| 263 | + return yield* runtimeEffectError("sources/source-definitions", "importAuth must be none unless importAuthPolicy is separate"); | |
| 279 | 264 | } |
| 280 | 265 | |
| 281 | 266 | return source; |
| @@ -57,7 +57,7 @@ const loadSourceForMissingCatalog = (input: { · 56 unmodified lines | ||
| 57 | 57 | }); |
| 58 | 58 | |
| 59 | 59 | if (!canInspectSourceWithoutCatalog(source)) { |
| 60 | − return yield* Effect.fail(input.cause); | |
| 60 | + return yield* input.cause; | |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | 63 | return source; |
| @@ -414,12 +414,10 @@ export const getSourceInspectionToolDetail = (input: { · 350 unmodified lines | ||
| 414 | 414 | const tool = inspection.tool; |
| 415 | 415 | |
| 416 | 416 | if (!tool) { |
| 417 | − return yield* Effect.fail( | |
| 418 | − sourceInspectOps.tool.notFound( | |
| 417 | + return yield* sourceInspectOps.tool.notFound( | |
| 419 | 418 | "Tool not found", |
| 420 | 419 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`, |
| 421 | − ), | |
| 422 | − ); | |
| 420 | + ); | |
| 423 | 421 | } |
| 424 | 422 | |
| 425 | 423 | return inspectionToolDetailFromTool(tool); |
| @@ -66,13 +66,11 @@ export const resolveRuntimeLocalWorkspaceFromDeps = ( · 65 unmodified lines | ||
| 66 | 66 | > => |
| 67 | 67 | Effect.gen(function* () { |
| 68 | 68 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { |
| 69 | − return yield* Effect.fail( | |
| 70 | − new RuntimeLocalWorkspaceMismatchError({ | |
| 69 | + return yield* new RuntimeLocalWorkspaceMismatchError({ | |
| 71 | 70 | message: `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`, |
| 72 | 71 | requestedWorkspaceId: workspaceId, |
| 73 | 72 | activeWorkspaceId: deps.runtimeLocalWorkspace.installation.workspaceId, |
| 74 | − }), | |
| 75 | − ); | |
| 73 | + }); | |
| 76 | 74 | } |
| 77 | 75 | |
| 78 | 76 | const loadedConfig = yield* deps.workspaceConfigStore.load( |
| @@ -52,12 +52,10 @@ export const buildLocalSourceRecord = (input: { · 51 unmodified lines | ||
| 52 | 52 | Effect.gen(function* () { |
| 53 | 53 | const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId]; |
| 54 | 54 | if (!sourceConfig) { |
| 55 | − return yield* Effect.fail( | |
| 56 | − new LocalConfiguredSourceNotFoundError({ | |
| 55 | + return yield* new LocalConfiguredSourceNotFoundError({ | |
| 57 | 56 | message: `Configured source not found for id ${input.sourceId}`, |
| 58 | 57 | sourceId: input.sourceId, |
| 59 | − }), | |
| 60 | − ); | |
| 58 | + }); | |
| 61 | 59 | } |
| 62 | 60 | |
| 63 | 61 | const existingState = input.workspaceState.sources[input.sourceId]; |
| @@ -313,12 +311,10 @@ export const loadSourceByIdWithDeps = ( · 249 unmodified lines | ||
| 313 | 311 | input.workspaceId, |
| 314 | 312 | ); |
| 315 | 313 | if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) { |
| 316 | − return yield* Effect.fail( | |
| 317 | − new LocalConfiguredSourceNotFoundError({ | |
| 314 | + return yield* new LocalConfiguredSourceNotFoundError({ | |
| 318 | 315 | message: `Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, |
| 319 | 316 | sourceId: input.sourceId, |
| 320 | − }), | |
| 321 | − ); | |
| 317 | + }); | |
| 322 | 318 | } |
| 323 | 319 | |
| 324 | 320 | const localSource = yield* buildLocalSourceRecord({ |
| @@ -121,9 +121,7 @@ const syncArtifactsForSource = (input: { · 120 unmodified lines | ||
| 121 | 121 | ); |
| 122 | 122 | } |
| 123 | 123 | |
| 124 | − return yield* Effect.fail( | |
| 125 | − input.operation.unknownStorage(error, "Failed syncing source tools"), | |
| 126 | − ); | |
| 124 | + return yield* input.operation.unknownStorage(error, "Failed syncing source tools"); | |
| 127 | 125 | }), |
| 128 | 126 | }); |
| 129 | 127 | }); |
| @@ -0,0 +1,13 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class PlatformServerEffectError extends Data.TaggedError( | |
| 4 | + "PlatformServerEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const platformServerEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new PlatformServerEffectError({ module, message }); | |
| @@ -36,6 +36,7 @@ import { · 35 unmodified lines | ||
| 36 | 36 | createLocalTracingRuntimeFromEnv, |
| 37 | 37 | tracingSearchUrl, |
| 38 | 38 | } from "./tracing"; |
| 39 | +import { platformServerEffectError } from "./effect-errors"; | |
| 39 | 40 | |
| 40 | 41 | export { |
| 41 | 42 | DEFAULT_EXECUTOR_DATA_DIR, |
| @@ -403,7 +404,7 @@ export const createLocalExecutorServer = ( · 361 unmodified lines | ||
| 403 | 404 | |
| 404 | 405 | const address = server.address(); |
| 405 | 406 | if (!address || typeof address === "string") { |
| 406 | − return yield* Effect.fail(new Error("Failed to resolve local executor server address")); | |
| 407 | + return yield* platformServerEffectError("index", "Failed to resolve local executor server address"); | |
| 407 | 408 | } |
| 408 | 409 | |
| 409 | 410 | const resolvedAddress = address as AddressInfo; |
| @@ -0,0 +1,13 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 2 | + | |
| 3 | +export class SourceCoreEffectError extends Data.TaggedError( | |
| 4 | + "SourceCoreEffectError", | |
| 5 | +)<{ | |
| 6 | + readonly module: string; | |
| 7 | + readonly message: string; | |
| 8 | +}> {} | |
| 9 | + | |
| 10 | +export const sourceCoreEffectError = ( | |
| 11 | + module: string, | |
| 12 | + message: string, | |
| 13 | +) => new SourceCoreEffectError({ module, message }); | |
| @@ -2,6 +2,7 @@ export * from "./catalog"; · 1 unmodified line | ||
| 2 | 2 | export * from "./catalog-sync-result"; |
| 3 | 3 | export * from "./discovery"; |
| 4 | 4 | export * from "./discovery-models"; |
| 5 | +export * from "./effect-errors"; | |
| 5 | 6 | export * from "./registry"; |
| 6 | 7 | export * from "./shared"; |
| 7 | 8 | export * from "./source-models"; |
| @@ -1,6 +1,8 @@ | ||
| 1 | +import * as Data from "effect/Data"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Schema from "effect/Schema"; |
| 3 | 4 | |
| 5 | +import { sourceCoreEffectError } from "./effect-errors"; | |
| 4 | 6 | import { |
| 5 | 7 | SecretRefSchema, |
| 6 | 8 | SourceBindingVersionSchema, |
| @@ -86,14 +88,17 @@ export const parseJsonValue = <T>(input: { · 79 unmodified lines | ||
| 86 | 88 | : new Error(`Invalid ${input.label}: ${String(cause)}`), |
| 87 | 89 | }); |
| 88 | 90 | |
| 89 | −export class SourceCredentialRequiredError extends Error { | |
| 90 | − readonly _tag = "SourceCredentialRequiredError"; | |
| 91 | − | |
| 91 | +export class SourceCredentialRequiredError extends Data.TaggedError( | |
| 92 | + "SourceCredentialRequiredError", | |
| 93 | +)<{ | |
| 94 | + readonly slot: CredentialSlot; | |
| 95 | + readonly message: string; | |
| 96 | +}> { | |
| 92 | 97 | constructor( |
| 93 | − readonly slot: CredentialSlot, | |
| 98 | + slot: CredentialSlot, | |
| 94 | 99 | message: string, |
| 95 | 100 | ) { |
| 96 | − super(message); | |
| 101 | + super({ slot, message }); | |
| 97 | 102 | } |
| 98 | 103 | } |
| 99 | 104 | |
| @@ -150,7 +155,7 @@ export const decodeBindingConfig = <A>(input: { · 50 unmodified lines | ||
| 150 | 155 | }): Effect.Effect<SourceBinding & { payload: A }, Error, never> => { |
| 151 | 156 | if (input.value === null) { |
| 152 | 157 | return Effect.fail( |
| 153 | − new Error(`Missing ${input.label} binding config for ${input.sourceId}`), | |
| 158 | + sourceCoreEffectError("core/shared", `Missing ${input.label} binding config for ${input.sourceId}`), | |
| 154 | 159 | ); |
| 155 | 160 | } |
| 156 | 161 | |
| @@ -173,7 +178,7 @@ export const decodeBindingConfig = <A>(input: { · 16 unmodified lines | ||
| 173 | 178 | payload: decoded.payload, |
| 174 | 179 | } as SourceBinding & { payload: A }) |
| 175 | 180 | : Effect.fail( |
| 176 | − new Error( | |
| 181 | + sourceCoreEffectError("core/shared", | |
| 177 | 182 | `Unsupported ${input.label} binding config version ${decoded.version} for ${input.sourceId}; expected ${input.version}`, |
| 178 | 183 | ), |
| 179 | 184 | ), |
| @@ -192,7 +197,7 @@ export const decodeSourceBindingPayload = <A>(input: { · 12 unmodified lines | ||
| 192 | 197 | }): Effect.Effect<A, Error, never> => |
| 193 | 198 | input.version !== input.expectedVersion |
| 194 | 199 | ? Effect.fail( |
| 195 | − new Error( | |
| 200 | + sourceCoreEffectError("core/shared", | |
| 196 | 201 | `Unsupported ${input.label} binding version ${input.version} for ${input.sourceId}; expected ${input.expectedVersion}`, |
| 197 | 202 | ), |
| 198 | 203 | ) |
| @@ -39,6 +39,7 @@ import { · 38 unmodified lines | ||
| 39 | 39 | WorkspaceOauthClientIdSchema, |
| 40 | 40 | createCatalogImportMetadata, |
| 41 | 41 | EXECUTABLE_BINDING_VERSION, |
| 42 | + sourceCoreEffectError, | |
| 42 | 43 | type Source, |
| 43 | 44 | type SourceAdapter, |
| 44 | 45 | } from "@executor/source-core"; |
| @@ -116,11 +117,9 @@ const googleDiscoveryBindingConfigFromSource = ( · 71 unmodified lines | ||
| 116 | 117 | "specUrl", |
| 117 | 118 | ]) |
| 118 | 119 | ) { |
| 119 | − return yield* Effect.fail( | |
| 120 | − new Error( | |
| 120 | + return yield* sourceCoreEffectError("google-discovery/adapter", | |
| 121 | 121 | "Google Discovery sources cannot define MCP or OpenAPI binding fields", |
| 122 | − ), | |
| 123 | − ); | |
| 122 | + ); | |
| 124 | 123 | } |
| 125 | 124 | |
| 126 | 125 | const bindingConfig = yield* decodeSourceBindingPayload({ |
| @@ -142,9 +141,7 @@ const googleDiscoveryBindingConfigFromSource = ( · 15 unmodified lines | ||
| 142 | 141 | const service = bindingConfig.service.trim(); |
| 143 | 142 | const version = bindingConfig.version.trim(); |
| 144 | 143 | if (service.length === 0 || version.length === 0) { |
| 145 | − return yield* Effect.fail( | |
| 146 | − new Error("Google Discovery sources require service and version"), | |
| 147 | − ); | |
| 144 | + return yield* sourceCoreEffectError("google-discovery/adapter", "Google Discovery sources require service and version"); | |
| 148 | 145 | } |
| 149 | 146 | |
| 150 | 147 | const explicitDiscoveryUrl = |
| @@ -194,19 +191,15 @@ const fetchGoogleDiscoveryDocumentWithHeaders = (input: { · 43 unmodified lines | ||
| 194 | 191 | ); |
| 195 | 192 | |
| 196 | 193 | if (response.status === 401 || response.status === 403) { |
| 197 | − return yield* Effect.fail( | |
| 198 | − new SourceCredentialRequiredError( | |
| 194 | + return yield* new SourceCredentialRequiredError( | |
| 199 | 195 | "import", |
| 200 | 196 | `Google Discovery fetch requires credentials (status ${response.status})`, |
| 201 | − ), | |
| 202 | − ); | |
| 197 | + ); | |
| 203 | 198 | } |
| 204 | 199 | if (response.status < 200 || response.status >= 300) { |
| 205 | − return yield* Effect.fail( | |
| 206 | − new Error( | |
| 200 | + return yield* sourceCoreEffectError("google-discovery/adapter", | |
| 207 | 201 | `Google Discovery fetch failed with status ${response.status}`, |
| 208 | − ), | |
| 209 | − ); | |
| 202 | + ); | |
| 210 | 203 | } |
| 211 | 204 | |
| 212 | 205 | return yield* response.text.pipe( |
| @@ -14,6 +14,7 @@ import { · 13 unmodified lines | ||
| 14 | 14 | type HttpRequestPlacements, |
| 15 | 15 | type ToolMetadata, |
| 16 | 16 | } from "@executor/codemode-core"; |
| 17 | +import { sourceCoreEffectError } from "@executor/source-core"; | |
| 17 | 18 | import * as Effect from "effect/Effect"; |
| 18 | 19 | import * as Layer from "effect/Layer"; |
| 19 | 20 | import * as Schema from "effect/Schema"; |
| @@ -503,11 +504,9 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 483 unmodified lines | ||
| 503 | 504 | const body = yield* decodeResponseBody(response); |
| 504 | 505 | |
| 505 | 506 | if (response.status < 200 || response.status >= 300) { |
| 506 | − return yield* Effect.fail( | |
| 507 | − new Error( | |
| 507 | + return yield* sourceCoreEffectError("google-discovery/tools", | |
| 508 | 508 | `Google Discovery request failed with HTTP ${response.status}: ${typeof body === "string" ? body : JSON.stringify(body)}`, |
| 509 | − ), | |
| 510 | − ); | |
| 509 | + ); | |
| 511 | 510 | } |
| 512 | 511 | |
| 513 | 512 | return body; |
| @@ -33,6 +33,7 @@ import { · 32 unmodified lines | ||
| 33 | 33 | StringMapSchema, |
| 34 | 34 | createCatalogImportMetadata, |
| 35 | 35 | EXECUTABLE_BINDING_VERSION, |
| 36 | + sourceCoreEffectError, | |
| 36 | 37 | type Source, |
| 37 | 38 | type SourceAdapter, |
| 38 | 39 | } from "@executor/source-core"; |
| @@ -94,14 +95,10 @@ const graphqlBindingConfigFromSource = ( · 55 unmodified lines | ||
| 94 | 95 | "headers", |
| 95 | 96 | ]) |
| 96 | 97 | ) { |
| 97 | − return yield* Effect.fail( | |
| 98 | − new Error("GraphQL sources cannot define MCP transport settings"), | |
| 99 | − ); | |
| 98 | + return yield* sourceCoreEffectError("graphql/adapter", "GraphQL sources cannot define MCP transport settings"); | |
| 100 | 99 | } |
| 101 | 100 | if (bindingHasAnyField(source.binding, ["specUrl"])) { |
| 102 | − return yield* Effect.fail( | |
| 103 | − new Error("GraphQL sources cannot define specUrl"), | |
| 104 | − ); | |
| 101 | + return yield* sourceCoreEffectError("graphql/adapter", "GraphQL sources cannot define specUrl"); | |
| 105 | 102 | } |
| 106 | 103 | |
| 107 | 104 | const bindingConfig = yield* decodeSourceBindingPayload({ |
| @@ -12,6 +12,7 @@ import { · 11 unmodified lines | ||
| 12 | 12 | type ToolPath, |
| 13 | 13 | typeSignatureFromSchema, |
| 14 | 14 | } from "@executor/codemode-core"; |
| 15 | +import { sourceCoreEffectError } from "@executor/source-core"; | |
| 15 | 16 | import { |
| 16 | 17 | GraphqlToolProviderDataSchema, |
| 17 | 18 | type GraphqlToolProviderData, |
| @@ -1612,9 +1613,7 @@ const invokeRawGraphqlTool = (input: { · 1594 unmodified lines | ||
| 1612 | 1613 | const record = asRecord(input.args); |
| 1613 | 1614 | const query = asString(record.query); |
| 1614 | 1615 | if (query === null) { |
| 1615 | − return yield* Effect.fail( | |
| 1616 | − new Error(`GraphQL query must be a non-empty string for ${input.path}`), | |
| 1617 | − ); | |
| 1616 | + return yield* sourceCoreEffectError("graphql/graphql-tools", `GraphQL query must be a non-empty string for ${input.path}`); | |
| 1618 | 1617 | } |
| 1619 | 1618 | |
| 1620 | 1619 | return yield* invokeGraphqlHttpRequest({ |
| @@ -31,6 +31,7 @@ import { · 30 unmodified lines | ||
| 31 | 31 | StringMapSchema, |
| 32 | 32 | createCatalogImportMetadata, |
| 33 | 33 | EXECUTABLE_BINDING_VERSION, |
| 34 | + sourceCoreEffectError, | |
| 34 | 35 | type Source, |
| 35 | 36 | type SourceAdapter, |
| 36 | 37 | type SourceCatalogSyncResult, |
| @@ -131,12 +132,10 @@ const mcpBindingConfigFromSource = ( · 94 unmodified lines | ||
| 131 | 132 | ): Effect.Effect<McpBindingConfig, Error, never> => |
| 132 | 133 | Effect.gen(function* () { |
| 133 | 134 | if (bindingHasAnyField(source.binding, ["specUrl"])) { |
| 134 | − return yield* Effect.fail(new Error("MCP sources cannot define specUrl")); | |
| 135 | + return yield* sourceCoreEffectError("mcp/adapter", "MCP sources cannot define specUrl"); | |
| 135 | 136 | } |
| 136 | 137 | if (bindingHasAnyField(source.binding, ["defaultHeaders"])) { |
| 137 | − return yield* Effect.fail( | |
| 138 | − new Error("MCP sources cannot define HTTP source settings"), | |
| 139 | − ); | |
| 138 | + return yield* sourceCoreEffectError("mcp/adapter", "MCP sources cannot define HTTP source settings"); | |
| 140 | 139 | } |
| 141 | 140 | |
| 142 | 141 | const bindingConfig = yield* decodeSourceBindingPayload({ |
| @@ -349,7 +349,7 @@ const runMcpListToolsEffect = (input: { · 348 unmodified lines | ||
| 349 | 349 | } |
| 350 | 350 | |
| 351 | 351 | if (isExecutionSuspendedCause(attempt.left)) { |
| 352 | − return yield* Effect.fail(attempt.left as McpToolsError); | |
| 352 | + return yield* (attempt.left as McpToolsError); | |
| 353 | 353 | } |
| 354 | 354 | |
| 355 | 355 | if ( |
| @@ -370,13 +370,11 @@ const runMcpListToolsEffect = (input: { · 14 unmodified lines | ||
| 370 | 370 | continue; |
| 371 | 371 | } |
| 372 | 372 | |
| 373 | − return yield* Effect.fail( | |
| 374 | − new McpToolsError({ | |
| 373 | + return yield* new McpToolsError({ | |
| 375 | 374 | stage: "list_tools", |
| 376 | 375 | message: "Failed listing MCP tools", |
| 377 | 376 | details: toDetails(attempt.left), |
| 378 | − }), | |
| 379 | − ); | |
| 377 | + }); | |
| 380 | 378 | } |
| 381 | 379 | }); |
| 382 | 380 | |
| @@ -417,7 +415,7 @@ const runMcpToolCallEffect = (input: { · 34 unmodified lines | ||
| 417 | 415 | } |
| 418 | 416 | |
| 419 | 417 | if (isExecutionSuspendedCause(attempt.left)) { |
| 420 | − return yield* Effect.fail(attempt.left as McpToolsError); | |
| 418 | + return yield* (attempt.left as McpToolsError); | |
| 421 | 419 | } |
| 422 | 420 | |
| 423 | 421 | if ( |
| @@ -438,13 +436,11 @@ const runMcpToolCallEffect = (input: { · 14 unmodified lines | ||
| 438 | 436 | continue; |
| 439 | 437 | } |
| 440 | 438 | |
| 441 | − return yield* Effect.fail( | |
| 442 | − new McpToolsError({ | |
| 439 | + return yield* new McpToolsError({ | |
| 443 | 440 | stage: "call_tool", |
| 444 | 441 | message: `Failed invoking MCP tool: ${input.toolName}`, |
| 445 | 442 | details: toDetails(attempt.left), |
| 446 | − }), | |
| 447 | − ); | |
| 443 | + }); | |
| 448 | 444 | } |
| 449 | 445 | }); |
| 450 | 446 | |
| @@ -42,6 +42,7 @@ import { · 41 unmodified lines | ||
| 42 | 42 | StringMapSchema, |
| 43 | 43 | createCatalogImportMetadata, |
| 44 | 44 | EXECUTABLE_BINDING_VERSION, |
| 45 | + sourceCoreEffectError, | |
| 45 | 46 | type Source, |
| 46 | 47 | type SourceAdapter, |
| 47 | 48 | } from "@executor/source-core"; |
| @@ -109,9 +110,7 @@ const openApiBindingConfigFromSource = ( · 61 unmodified lines | ||
| 109 | 110 | "headers", |
| 110 | 111 | ]) |
| 111 | 112 | ) { |
| 112 | − return yield* Effect.fail( | |
| 113 | − new Error("OpenAPI sources cannot define MCP transport settings"), | |
| 114 | − ); | |
| 113 | + return yield* sourceCoreEffectError("openapi/adapter", "OpenAPI sources cannot define MCP transport settings"); | |
| 115 | 114 | } |
| 116 | 115 | |
| 117 | 116 | const bindingConfig = yield* decodeSourceBindingPayload({ |
| @@ -129,7 +128,7 @@ const openApiBindingConfigFromSource = ( · 11 unmodified lines | ||
| 129 | 128 | ? bindingConfig.specUrl.trim() |
| 130 | 129 | : ""; |
| 131 | 130 | if (specUrl.length === 0) { |
| 132 | − return yield* Effect.fail(new Error("OpenAPI sources require specUrl")); | |
| 131 | + return yield* sourceCoreEffectError("openapi/adapter", "OpenAPI sources require specUrl"); | |
| 133 | 132 | } |
| 134 | 133 | |
| 135 | 134 | return { |
| @@ -168,17 +167,13 @@ const fetchOpenApiDocumentWithHeaders = (input: { · 32 unmodified lines | ||
| 168 | 167 | ); |
| 169 | 168 | |
| 170 | 169 | if (response.status === 401 || response.status === 403) { |
| 171 | − return yield* Effect.fail( | |
| 172 | − new SourceCredentialRequiredError( | |
| 170 | + return yield* new SourceCredentialRequiredError( | |
| 173 | 171 | "import", |
| 174 | 172 | `OpenAPI spec fetch requires credentials (status ${response.status})`, |
| 175 | − ), | |
| 176 | − ); | |
| 173 | + ); | |
| 177 | 174 | } |
| 178 | 175 | if (response.status < 200 || response.status >= 300) { |
| 179 | − return yield* Effect.fail( | |
| 180 | − new Error(`OpenAPI spec fetch failed with status ${response.status}`), | |
| 181 | − ); | |
| 176 | + return yield* sourceCoreEffectError("openapi/adapter", `OpenAPI spec fetch failed with status ${response.status}`); | |
| 182 | 177 | } |
| 183 | 178 | |
| 184 | 179 | return yield* response.text.pipe( |
| @@ -11,6 +11,8 @@ Why it lives here: · 10 unmodified lines | ||
| 11 | 11 | Current rules: |
| 12 | 12 | |
| 13 | 13 | - `executor-monorepo/no-cross-workspace-relative-imports` |
| 14 | +- `executor-monorepo/no-raw-effect-fail-errors` | |
| 15 | +- `executor-monorepo/no-yield-effect-fail` | |
| 14 | 16 | - `executor-monorepo/no-workspace-src-imports` |
| 15 | 17 | |
| 16 | 18 | To add another rule: |
| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | import { definePlugin } from "@oxlint/plugins"; |
| 2 | 2 | |
| 3 | 3 | import noCrossWorkspaceRelativeImports from "./rules/no-cross-workspace-relative-imports.mjs"; |
| 4 | +import noRawEffectFailErrors from "./rules/no-raw-effect-fail-errors.mjs"; | |
| 5 | +import noYieldEffectFail from "./rules/no-yield-effect-fail.mjs"; | |
| 4 | 6 | import noWorkspaceSrcImports from "./rules/no-workspace-src-imports.mjs"; |
| 5 | 7 | |
| 6 | 8 | export default definePlugin({ |
| @@ -9,6 +11,8 @@ export default definePlugin({ · 2 unmodified lines | ||
| 9 | 11 | }, |
| 10 | 12 | rules: { |
| 11 | 13 | "no-cross-workspace-relative-imports": noCrossWorkspaceRelativeImports, |
| 14 | + "no-raw-effect-fail-errors": noRawEffectFailErrors, | |
| 15 | + "no-yield-effect-fail": noYieldEffectFail, | |
| 12 | 16 | "no-workspace-src-imports": noWorkspaceSrcImports, |
| 13 | 17 | }, |
| 14 | 18 | }); |
| @@ -0,0 +1,109 @@ | ||
| 1 | +import { defineRule } from "@oxlint/plugins"; | |
| 2 | + | |
| 3 | +const BUILTIN_ERROR_NAMES = new Set([ | |
| 4 | + "AggregateError", | |
| 5 | + "Error", | |
| 6 | + "EvalError", | |
| 7 | + "RangeError", | |
| 8 | + "ReferenceError", | |
| 9 | + "SyntaxError", | |
| 10 | + "TypeError", | |
| 11 | + "URIError", | |
| 12 | +]); | |
| 13 | + | |
| 14 | +const isIdentifierNamed = (node, name) => | |
| 15 | + node?.type === "Identifier" && node.name === name; | |
| 16 | + | |
| 17 | +const isEffectFailCall = (node) => | |
| 18 | + node?.type === "CallExpression" | |
| 19 | + && node.callee?.type === "MemberExpression" | |
| 20 | + && node.callee.computed === false | |
| 21 | + && isIdentifierNamed(node.callee.object, "Effect") | |
| 22 | + && isIdentifierNamed(node.callee.property, "fail"); | |
| 23 | + | |
| 24 | +const readBuiltinErrorName = (node) => { | |
| 25 | + if ( | |
| 26 | + node?.type === "NewExpression" | |
| 27 | + && node.callee?.type === "Identifier" | |
| 28 | + && BUILTIN_ERROR_NAMES.has(node.callee.name) | |
| 29 | + ) { | |
| 30 | + return node.callee.name; | |
| 31 | + } | |
| 32 | + | |
| 33 | + if ( | |
| 34 | + node?.type === "CallExpression" | |
| 35 | + && node.callee?.type === "Identifier" | |
| 36 | + && BUILTIN_ERROR_NAMES.has(node.callee.name) | |
| 37 | + ) { | |
| 38 | + return node.callee.name; | |
| 39 | + } | |
| 40 | + | |
| 41 | + return null; | |
| 42 | +}; | |
| 43 | + | |
| 44 | +const readBuiltinFailedErrorName = (node) => { | |
| 45 | + if (!isEffectFailCall(node) || node.arguments.length === 0) { | |
| 46 | + return null; | |
| 47 | + } | |
| 48 | + | |
| 49 | + return readBuiltinErrorName(node.arguments[0]); | |
| 50 | +}; | |
| 51 | + | |
| 52 | +export default defineRule({ | |
| 53 | + meta: { | |
| 54 | + type: "problem", | |
| 55 | + docs: { | |
| 56 | + description: | |
| 57 | + "Disallow failing Effects with builtin Error constructors instead of tagged Effect errors.", | |
| 58 | + recommended: true, | |
| 59 | + }, | |
| 60 | + messages: { | |
| 61 | + noRawEffectFailErrors: | |
| 62 | + "Do not pass builtin {{errorName}} to Effect.fail. Use a Data.TaggedError or Schema.TaggedError instead. If no suitable tagged error exists, define one near this module with structured fields and fail with that tagged error rather than interpolating context into a message string.", | |
| 63 | + noRawYieldedEffectFailErrors: | |
| 64 | + "Do not `yield* Effect.fail` with builtin {{errorName}}. In Effect.gen, prefer yielding the tagged error directly, for example `yield* new MyTaggedError({ ... })`. If no suitable Data.TaggedError or Schema.TaggedError exists, define one near this module with structured fields instead of interpolating context into a message string.", | |
| 65 | + }, | |
| 66 | + }, | |
| 67 | + create(context) { | |
| 68 | + return { | |
| 69 | + CallExpression(node) { | |
| 70 | + const errorName = readBuiltinFailedErrorName(node); | |
| 71 | + | |
| 72 | + if (!errorName) { | |
| 73 | + return; | |
| 74 | + } | |
| 75 | + | |
| 76 | + if ( | |
| 77 | + node.parent?.type === "YieldExpression" | |
| 78 | + && node.parent.argument === node | |
| 79 | + ) { | |
| 80 | + return; | |
| 81 | + } | |
| 82 | + | |
| 83 | + context.report({ | |
| 84 | + node: node.arguments[0], | |
| 85 | + messageId: "noRawEffectFailErrors", | |
| 86 | + data: { | |
| 87 | + errorName, | |
| 88 | + }, | |
| 89 | + }); | |
| 90 | + }, | |
| 91 | + YieldExpression(node) { | |
| 92 | + const failedCall = node.argument; | |
| 93 | + const errorName = readBuiltinFailedErrorName(failedCall); | |
| 94 | + | |
| 95 | + if (!errorName) { | |
| 96 | + return; | |
| 97 | + } | |
| 98 | + | |
| 99 | + context.report({ | |
| 100 | + node: failedCall.arguments[0], | |
| 101 | + messageId: "noRawYieldedEffectFailErrors", | |
| 102 | + data: { | |
| 103 | + errorName, | |
| 104 | + }, | |
| 105 | + }); | |
| 106 | + }, | |
| 107 | + }; | |
| 108 | + }, | |
| 109 | +}); | |
| @@ -0,0 +1,64 @@ | ||
| 1 | +import { defineRule } from "@oxlint/plugins"; | |
| 2 | + | |
| 3 | +const isIdentifierNamed = (node, name) => | |
| 4 | + node?.type === "Identifier" && node.name === name; | |
| 5 | + | |
| 6 | +const isEffectFailCall = (node) => | |
| 7 | + node?.type === "CallExpression" | |
| 8 | + && node.callee?.type === "MemberExpression" | |
| 9 | + && node.callee.computed === false | |
| 10 | + && isIdentifierNamed(node.callee.object, "Effect") | |
| 11 | + && isIdentifierNamed(node.callee.property, "fail"); | |
| 12 | + | |
| 13 | +const isDirectYieldCandidate = (node) => { | |
| 14 | + if (!node) { | |
| 15 | + return false; | |
| 16 | + } | |
| 17 | + | |
| 18 | + if (node.type === "CallExpression" || node.type === "NewExpression") { | |
| 19 | + return true; | |
| 20 | + } | |
| 21 | + | |
| 22 | + if (node.type === "ParenthesizedExpression") { | |
| 23 | + return isDirectYieldCandidate(node.expression); | |
| 24 | + } | |
| 25 | + | |
| 26 | + return node.type === "TSAsExpression" || node.type === "TSTypeAssertion"; | |
| 27 | +}; | |
| 28 | + | |
| 29 | +export default defineRule({ | |
| 30 | + meta: { | |
| 31 | + type: "suggestion", | |
| 32 | + docs: { | |
| 33 | + description: | |
| 34 | + "Disallow `yield* Effect.fail(...)` when the yielded failure value can be yielded directly in Effect.gen.", | |
| 35 | + recommended: true, | |
| 36 | + }, | |
| 37 | + messages: { | |
| 38 | + noYieldEffectFail: | |
| 39 | + "Avoid `yield* Effect.fail(...)` here. In Effect.gen, yield the tagged or yieldable error expression directly instead, for example `yield* new MyTaggedError({ ... })` or `yield* myTaggedError(...)`.", | |
| 40 | + }, | |
| 41 | + }, | |
| 42 | + create(context) { | |
| 43 | + return { | |
| 44 | + YieldExpression(node) { | |
| 45 | + if (!node.delegate) { | |
| 46 | + return; | |
| 47 | + } | |
| 48 | + | |
| 49 | + if (!isEffectFailCall(node.argument) || node.argument.arguments.length === 0) { | |
| 50 | + return; | |
| 51 | + } | |
| 52 | + | |
| 53 | + if (!isDirectYieldCandidate(node.argument.arguments[0])) { | |
| 54 | + return; | |
| 55 | + } | |
| 56 | + | |
| 57 | + context.report({ | |
| 58 | + node, | |
| 59 | + messageId: "noYieldEffectFail", | |
| 60 | + }); | |
| 61 | + }, | |
| 62 | + }; | |
| 63 | + }, | |
| 64 | +}); | |