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