History

Add Effect-focused Oxlint rules and direct-yield cleanup

Rhys Sullivan committed 8 months ago

Commit 444c8a71 parent 0205ac156 files · +626 −460Browse files

Unified · Split

56 files
  1. .oxlintrc.jsonc +2 −0
  2. apps/executor/src/cli/main.ts +37 −32
  3. apps/executor/src/distribution/harness.ts +3 −2
  4. apps/executor/src/effect-errors.ts +40 −0
  5. packages/auth/mcp-oauth/src/effect-errors.ts +13 −0
  6. packages/auth/mcp-oauth/src/index.ts +3 −6
  7. packages/dev/kitchen-sink/src/effect-errors.ts +13 −0
  8. packages/dev/kitchen-sink/src/kitchen-sink.test.ts +2 −1
  9. packages/hosts/ai-sdk/src/effect-errors.ts +13 −0
  10. packages/hosts/ai-sdk/src/index.ts +2 −1
  11. packages/kernel/core/src/effect-errors.ts +13 −0
  12. packages/kernel/core/src/tool-map.ts +5 −4
  13. packages/platform/control-plane/src/api/local/http.ts +10 −18
  14. packages/platform/control-plane/src/runtime/auth/auth-artifacts.ts +5 −8
  15. packages/platform/control-plane/src/runtime/auth/auth-leases.ts +6 −15
  16. packages/platform/control-plane/src/runtime/auth/source-auth-material.ts +2 −3
  17. packages/platform/control-plane/src/runtime/catalog/source/runtime.ts +4 −5
  18. packages/platform/control-plane/src/runtime/catalog/source/sync.ts +2 −1
  19. packages/platform/control-plane/src/runtime/control-plane-runtime.test.ts +2 −1
  20. packages/platform/control-plane/src/runtime/effect-errors.ts +13 −0
  21. packages/platform/control-plane/src/runtime/execution/ir-execution.ts +2 −1
  22. packages/platform/control-plane/src/runtime/execution/service.ts +32 −28
  23. packages/platform/control-plane/src/runtime/execution/workspace/authorization.ts +6 −9
  24. packages/platform/control-plane/src/runtime/execution/workspace/tool-invoker.ts +2 −3
  25. packages/platform/control-plane/src/runtime/local/operations.ts +14 −28
  26. packages/platform/control-plane/src/runtime/local/runtime-context.ts +4 −8
  27. packages/platform/control-plane/src/runtime/local/secret-material-providers.ts +29 −44
  28. packages/platform/control-plane/src/runtime/local/tools.ts +9 −17
  29. packages/platform/control-plane/src/runtime/policy/policies-operations.ts +4 −8
  30. packages/platform/control-plane/src/runtime/sources/executor-tools.ts +4 −9
  31. packages/platform/control-plane/src/runtime/sources/source-adapters/google-discovery.test.ts +2 −1
  32. packages/platform/control-plane/src/runtime/sources/source-adapters/graphql.test.ts +2 −1
  33. packages/platform/control-plane/src/runtime/sources/source-adapters/internal.ts +3 −4
  34. packages/platform/control-plane/src/runtime/sources/source-adapters/mcp.test.ts +4 −3
  35. packages/platform/control-plane/src/runtime/sources/source-auth-service.ts +46 −91
  36. packages/platform/control-plane/src/runtime/sources/source-definitions.ts +11 −26
  37. packages/platform/control-plane/src/runtime/sources/source-inspection.ts +3 −5
  38. packages/platform/control-plane/src/runtime/sources/source-store/deps.ts +2 −4
  39. packages/platform/control-plane/src/runtime/sources/source-store/records.ts +4 −8
  40. packages/platform/control-plane/src/runtime/sources/sources-operations.ts +1 −3
  41. packages/platform/server/src/effect-errors.ts +13 −0
  42. packages/platform/server/src/index.ts +2 −1
  43. packages/sources/core/src/effect-errors.ts +13 −0
  44. packages/sources/core/src/index.ts +1 −0
  45. packages/sources/core/src/shared.ts +13 −8
  46. packages/sources/google-discovery/src/adapter.ts +8 −15
  47. packages/sources/google-discovery/src/tools.ts +3 −4
  48. packages/sources/graphql/src/adapter.ts +3 −6
  49. packages/sources/graphql/src/graphql-tools.ts +2 −3
  50. packages/sources/mcp/src/adapter.ts +3 −4
  51. packages/sources/mcp/src/tools.ts +6 −10
  52. packages/sources/openapi/src/adapter.ts +6 −11
  53. tools/oxlint/README.md +2 −0
  54. tools/oxlint/plugin.mjs +4 −0
  55. tools/oxlint/rules/no-raw-effect-fail-errors.mjs +109 −0
  56. tools/oxlint/rules/no-yield-effect-fail.mjs +64 −0
.oxlintrc.jsonc+2−0
@@ -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
apps/executor/src/cli/main.ts+37−32
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process"; · 3 unmodified lines
4import { dirname, resolve } from "node:path";4import { dirname, resolve } from "node:path";
5import { createInterface } from "node:readline/promises";5import { createInterface } from "node:readline/promises";
6import { fileURLToPath } from "node:url";6import { fileURLToPath } from "node:url";
7import { FileSystem } from "@effect/platform";
7import { Args, Command, Options } from "@effect/cli";8import { Args, Command, Options } from "@effect/cli";
8import {9import {
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";
54import { decideInteractionHandling } from "./interaction-handling";55import { decideInteractionHandling } from "./interaction-handling";
56import {
57 executorAppEffectError,
58 type LocalServerReachabilityTimeoutError,
59 localServerReachabilityTimeoutError,
60} from "../effect-errors";
55 61
56const toError = (cause: unknown): Error =>62const 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
146const getBootstrapClient = (baseUrl: string = DEFAULT_SERVER_BASE_URL) =>150const 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
474const readServerLogTail = async (478const 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
489const 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) {498const 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
505const waitForReachability = (baseUrl: string, expected: boolean) =>515const 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
524type LocalServerStatus = {529type 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 );
apps/executor/src/distribution/harness.ts+3−2
@@ -9,6 +9,7 @@ import * as Effect from "effect/Effect"; · 8 unmodified lines
9import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
10 10
11import { buildDistributionPackage } from "./artifact";11import { buildDistributionPackage } from "./artifact";
12import { executorAppEffectError } from "../effect-errors";
12 13
13type CommandResult = {14type 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
apps/executor/src/effect-errors.ts+40−0
@@ -0,0 +1,40 @@
1import * as Data from "effect/Data";
2
3export class ExecutorAppEffectError extends Data.TaggedError(
4 "ExecutorAppEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const executorAppEffectError = (
11 module: string,
12 message: string,
13) => new ExecutorAppEffectError({ module, message });
14
15export 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
25export 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 });
packages/auth/mcp-oauth/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1import * as Data from "effect/Data";
2
3export class McpOAuthEffectError extends Data.TaggedError(
4 "McpOAuthEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const mcpOAuthEffectError = (
11 module: string,
12 message: string,
13) => new McpOAuthEffectError({ module, message });
packages/auth/mcp-oauth/src/index.ts+3−6
@@ -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";
10import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
11import { mcpOAuthEffectError } from "./effect-errors";
11 12
12type JsonValue =13type JsonValue =
13 | string14 | 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 {
packages/dev/kitchen-sink/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1import * as Data from "effect/Data";
2
3export class KitchenSinkEffectError extends Data.TaggedError(
4 "KitchenSinkEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const kitchenSinkEffectError = (
11 module: string,
12 message: string,
13) => new KitchenSinkEffectError({ module, message });
packages/dev/kitchen-sink/src/kitchen-sink.test.ts+2−1
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
11 toExecutorTool,11 toExecutorTool,
12} from "@executor/codemode-core";12} from "@executor/codemode-core";
13import { makeSesExecutor } from "@executor/runtime-ses";13import { makeSesExecutor } from "@executor/runtime-ses";
14import { kitchenSinkEffectError } from "./effect-errors";
14 15
15const numberPairInputSchema = Schema.standardSchemaV1(16const 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({
packages/hosts/ai-sdk/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1import * as Data from "effect/Data";
2
3export class AiSdkEffectError extends Data.TaggedError(
4 "AiSdkEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const aiSdkEffectError = (
11 module: string,
12 message: string,
13) => new AiSdkEffectError({ module, message });
packages/hosts/ai-sdk/src/index.ts+2−1
@@ -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";
15import { aiSdkEffectError } from "./effect-errors";
15 16
16export type AiSdkToolMap = ToolSet;17export 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.error52 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,
packages/kernel/core/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1import * as Data from "effect/Data";
2
3export class KernelCoreEffectError extends Data.TaggedError(
4 "KernelCoreEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const kernelCoreEffectError = (
11 module: string,
12 message: string,
13) => new KernelCoreEffectError({ module, message });
packages/kernel/core/src/tool-map.ts+5−4
@@ -22,6 +22,7 @@ import type { · 21 unmodified lines
22 ToolMetadata,22 ToolMetadata,
23 ToolPath,23 ToolPath,
24} from "./types";24} from "./types";
25import { kernelCoreEffectError } from "./effect-errors";
25 26
26type ResolvedTool = {27type 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({
packages/platform/control-plane/src/api/local/http.ts+10−18
@@ -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 };
packages/platform/control-plane/src/runtime/auth/auth-artifacts.ts+5−8
@@ -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";
38import { runtimeEffectError } from "../effect-errors";
38 39
39export type ResolvedSourceAuthMaterial = {40export 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) =>
packages/platform/control-plane/src/runtime/auth/auth-leases.ts+6−15
@@ -27,6 +27,7 @@ import { · 26 unmodified lines
27import { refreshOAuth2AccessToken } from "./oauth2-pkce";27import { refreshOAuth2AccessToken } from "./oauth2-pkce";
28import type { OAuth2TokenResponse } from "./oauth2-pkce";28import type { OAuth2TokenResponse } from "./oauth2-pkce";
29import { createPersistedMcpAuthProvider } from "./mcp-auth-provider";29import { createPersistedMcpAuthProvider } from "./mcp-auth-provider";
30import { runtimeEffectError } from "../effect-errors";
30 31
31const LEASE_REFRESH_SKEW_MS = 60_000;32const LEASE_REFRESH_SKEW_MS = 60_000;
32 33
@@ -92,9 +93,7 @@ const refreshRefreshableOauth2AuthorizedUserArtifact = (input: { · 59 unmodified lines
92 decoded === null93 decoded === null
93 || decoded.artifactKind !== RefreshableOAuth2AuthorizedUserAuthArtifactKind94 || 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 === null342 decoded === null
350 || decoded.artifactKind !== RefreshableOAuth2AuthorizedUserAuthArtifactKind343 || 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);
packages/platform/control-plane/src/runtime/auth/source-auth-material.ts+2−3
@@ -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";
23import { SecretMaterialResolverService } from "../local/secret-material-providers";23import { SecretMaterialResolverService } from "../local/secret-material-providers";
24import { runtimeEffectError } from "../effect-errors";
24 25
25const authForSlot = (input: {26const 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({
packages/platform/control-plane/src/runtime/catalog/source/runtime.ts+4−5
@@ -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";
49import { runtimeEffectError } from "../../effect-errors";
49 50
50type CatalogImportMetadata = CatalogSnapshotV1["import"];51type 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({
packages/platform/control-plane/src/runtime/catalog/source/sync.ts+2−1
@@ -35,6 +35,7 @@ import { snapshotFromSourceCatalogSyncResult } from "../../sources/catalog-sync- · 34 unmodified lines
35import {35import {
36 refreshSourceTypeDeclarationInBackground,36 refreshSourceTypeDeclarationInBackground,
37} from "./type-declarations";37} from "./type-declarations";
38import { runtimeEffectError } from "../../effect-errors";
38 39
39const shouldIndexSource = (source: Source): boolean =>40const shouldIndexSource = (source: Source): boolean =>
40 source.enabled41 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 );
packages/platform/control-plane/src/runtime/control-plane-runtime.test.ts+2−1
@@ -32,6 +32,7 @@ import { createSourceFromPayload } from "./sources/source-definitions"; · 31 unmodified lines
32import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions";32import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions";
33import { persistSource } from "./sources/source-store";33import { persistSource } from "./sources/source-store";
34import { withControlPlaneClient } from "./execution/test-http-client";34import { withControlPlaneClient } from "./execution/test-http-client";
35import { runtimeEffectError } from "./effect-errors";
35 36
36const makeRuntime = Effect.gen(function* () {37const 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
packages/platform/control-plane/src/runtime/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1import * as Data from "effect/Data";
2
3export class RuntimeEffectError extends Data.TaggedError(
4 "RuntimeEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const runtimeEffectError = (
11 module: string,
12 message: string,
13) => new RuntimeEffectError({ module, message });
packages/platform/control-plane/src/runtime/execution/ir-execution.ts+2−1
@@ -5,6 +5,7 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines
5import type { LoadedSourceCatalogToolIndexEntry } from "../catalog/source/runtime";5import type { LoadedSourceCatalogToolIndexEntry } from "../catalog/source/runtime";
6import type { ResolvedSourceAuthMaterial } from "../auth/source-auth-material";6import type { ResolvedSourceAuthMaterial } from "../auth/source-auth-material";
7import { getSourceAdapter } from "../sources/source-adapters";7import { getSourceAdapter } from "../sources/source-adapters";
8import { runtimeEffectError } from "../effect-errors";
8 9
9export const invocationDescriptorFromTool = (input: {10export 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 );
packages/platform/control-plane/src/runtime/execution/service.ts+32−28
@@ -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";
26import * as Data from "effect/Data";
26import * as Deferred from "effect/Deferred";27import * as Deferred from "effect/Deferred";
27import * as Effect from "effect/Effect";28import * as Effect from "effect/Effect";
28import * as Option from "effect/Option";29import * 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";
50import { RuntimeExecutionResolverService } from "./workspace/environment";51import { RuntimeExecutionResolverService } from "./workspace/environment";
52import { runtimeEffectError } from "../effect-errors";
51 53
52const executionOps = {54const 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
169class ExecutionSuspendedError extends Data.TaggedError(
170 "ExecutionSuspendedError",
171)<{
172 readonly executionId: Execution["id"];
173 readonly interactionId: string;
174 readonly message: string;
175}> {}
176
167const createExecutionSuspendedError = (input: {177const 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
175const isExecutionSuspendedValue = (value: unknown): boolean => {187const 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
389const createHybridOnElicitation = (input: {401const 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.errorText503 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 !== null944 && 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
packages/platform/control-plane/src/runtime/execution/workspace/authorization.ts+6−9
@@ -10,6 +10,7 @@ import type { WorkspaceStorageServices } from "../../local/storage"; · 9 unmodified lines
10import { invocationDescriptorFromTool } from "../ir-execution";10import { invocationDescriptorFromTool } from "../ir-execution";
11import { evaluateInvocationPolicy } from "../../policy/invocation-policy-engine";11import { evaluateInvocationPolicy } from "../../policy/invocation-policy-engine";
12import { loadRuntimeLocalWorkspacePolicies } from "../../policy/policies-operations";12import { loadRuntimeLocalWorkspacePolicies } from "../../policy/policies-operations";
13import { runtimeEffectError } from "../../effect-errors";
13 14
14const asToolPath = (value: string): ToolPath => value as ToolPath;15const 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 });
packages/platform/control-plane/src/runtime/execution/workspace/tool-invoker.ts+2−3
@@ -36,6 +36,7 @@ import { · 35 unmodified lines
36 createWorkspaceSourceCatalog,36 createWorkspaceSourceCatalog,
37 loadWorkspaceCatalogToolByPath,37 loadWorkspaceCatalogToolByPath,
38} from "./source-catalog";38} from "./source-catalog";
39import { runtimeEffectError } from "../../effect-errors";
39 40
40export const createWorkspaceToolInvoker = (input: {41export 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({
packages/platform/control-plane/src/runtime/local/operations.ts+14−28
@@ -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.workspaceId149 || decoded.workspaceId !== input.workspaceId
152 || decoded.sourceId !== input.sourceId150 || 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
packages/platform/control-plane/src/runtime/local/runtime-context.ts+4−8
@@ -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 !== undefined64 workspaceId !== undefined
67 && runtimeLocalWorkspace.installation.workspaceId !== workspaceId65 && 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;
packages/platform/control-plane/src/runtime/local/secret-material-providers.ts+29−44
@@ -22,6 +22,7 @@ import { fromConfigSecretProviderId } from "./config-secrets"; · 21 unmodified lines
22import { getRuntimeLocalWorkspaceOption } from "./runtime-context";22import { getRuntimeLocalWorkspaceOption } from "./runtime-context";
23import { ControlPlaneStore } from "../store";23import { ControlPlaneStore } from "../store";
24import type { ControlPlaneStoreShape } from "../store";24import type { ControlPlaneStoreShape } from "../store";
25import { runtimeEffectError } from "../effect-errors";
25 26
26export const ENV_SECRET_PROVIDER_ID = "env";27export const ENV_SECRET_PROVIDER_ID = "env";
27export const PARAMS_SECRET_PROVIDER_ID = "params";28export 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
310const commandAvailabilityCache = new Map<string, Promise<boolean>>();311const 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
855const createSecretMaterialProviderRuntime = (input: {844const 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 === null955 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) !== null1022 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({
packages/platform/control-plane/src/runtime/local/tools.ts+9−17
@@ -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({
packages/platform/control-plane/src/runtime/policy/policies-operations.ts+4−8
@@ -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 ?? {});
packages/platform/control-plane/src/runtime/sources/executor-tools.ts+4−9
@@ -29,6 +29,7 @@ import { · 28 unmodified lines
29 makeLocalStorageLayer,29 makeLocalStorageLayer,
30} from "../local/storage";30} from "../local/storage";
31import { provideOptionalRuntimeLocalWorkspace } from "../local/runtime-context";31import { provideOptionalRuntimeLocalWorkspace } from "../local/runtime-context";
32import { 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). */
34const runEffect = async <A>(35const 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({
packages/platform/control-plane/src/runtime/sources/source-adapters/google-discovery.test.ts+2−1
@@ -8,6 +8,7 @@ import * as Effect from "effect/Effect"; · 7 unmodified lines
8import { googleDiscoverySourceAdapter } from "./google-discovery";8import { googleDiscoverySourceAdapter } from "./google-discovery";
9import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result";9import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result";
10import { createSourceFromPayload } from "../source-definitions";10import { createSourceFromPayload } from "../source-definitions";
11import { runtimeEffectError } from "../../effect-errors";
11 12
12const fetchLiveDiscoveryDocument = async (): Promise<string> => {13const 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: [],
packages/platform/control-plane/src/runtime/sources/source-adapters/graphql.test.ts+2−1
@@ -7,6 +7,7 @@ import { vi } from "vitest"; · 6 unmodified lines
7 7
8import { createSourceFromPayload } from "../source-definitions";8import { createSourceFromPayload } from "../source-definitions";
9import { graphqlSourceAdapter } from "./graphql";9import { graphqlSourceAdapter } from "./graphql";
10import { runtimeEffectError } from "../../effect-errors";
10 11
11describe("graphql source adapter", () => {12describe("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: [],
packages/platform/control-plane/src/runtime/sources/source-adapters/internal.ts+3−4
@@ -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";
13import { runtimeEffectError } from "../../effect-errors";
13 14
14const InternalBindingConfigSchema = Schema.Struct({});15const 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};
packages/platform/control-plane/src/runtime/sources/source-adapters/mcp.test.ts+4−3
@@ -30,6 +30,7 @@ import { · 29 unmodified lines
30import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result";30import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result";
31import { createSourceFromPayload } from "../source-definitions";31import { createSourceFromPayload } from "../source-definitions";
32import { mcpSourceAdapter } from "./mcp";32import { mcpSourceAdapter } from "./mcp";
33import { runtimeEffectError } from "../../effect-errors";
33 34
34type RealMcpServer = {35type 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);
packages/platform/control-plane/src/runtime/sources/source-auth-service.ts+46−91
@@ -99,6 +99,7 @@ import { · 98 unmodified lines
99} from "./source-store";99} from "./source-store";
100import type { WorkspaceStorageServices } from "../local/storage";100import type { WorkspaceStorageServices } from "../local/storage";
101import { ControlPlaneStore, type ControlPlaneStoreShape } from "../store";101import { ControlPlaneStore, type ControlPlaneStoreShape } from "../store";
102import { runtimeEffectError } from "../effect-errors";
102 103
103const trimOrNull = (value: string | null | undefined): string | null => {104const 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.workspaceId1070 existing.value.workspaceId !== input.workspaceId
1076 || existing.value.providerKey !== input.providerKey1071 || 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 !== undefined3161 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({
packages/platform/control-plane/src/runtime/sources/source-definitions.ts+11−26
@@ -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";
35import { runtimeEffectError } from "../effect-errors";
35 36
36const trimOrNull = (value: string | null | undefined): string | null => {37const 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
273const validateSourceImportAuth = (source: Source): Effect.Effect<Source, Error, never> =>260const 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;
packages/platform/control-plane/src/runtime/sources/source-inspection.ts+3−5
@@ -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);
packages/platform/control-plane/src/runtime/sources/source-store/deps.ts+2−4
@@ -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(
packages/platform/control-plane/src/runtime/sources/source-store/records.ts+4−8
@@ -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({
packages/platform/control-plane/src/runtime/sources/sources-operations.ts+1−3
@@ -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 });
packages/platform/server/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1import * as Data from "effect/Data";
2
3export class PlatformServerEffectError extends Data.TaggedError(
4 "PlatformServerEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const platformServerEffectError = (
11 module: string,
12 message: string,
13) => new PlatformServerEffectError({ module, message });
packages/platform/server/src/index.ts+2−1
@@ -36,6 +36,7 @@ import { · 35 unmodified lines
36 createLocalTracingRuntimeFromEnv,36 createLocalTracingRuntimeFromEnv,
37 tracingSearchUrl,37 tracingSearchUrl,
38} from "./tracing";38} from "./tracing";
39import { platformServerEffectError } from "./effect-errors";
39 40
40export {41export {
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;
packages/sources/core/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1import * as Data from "effect/Data";
2
3export class SourceCoreEffectError extends Data.TaggedError(
4 "SourceCoreEffectError",
5)<{
6 readonly module: string;
7 readonly message: string;
8}> {}
9
10export const sourceCoreEffectError = (
11 module: string,
12 message: string,
13) => new SourceCoreEffectError({ module, message });
packages/sources/core/src/index.ts+1−0
@@ -2,6 +2,7 @@ export * from "./catalog"; · 1 unmodified line
2export * from "./catalog-sync-result";2export * from "./catalog-sync-result";
3export * from "./discovery";3export * from "./discovery";
4export * from "./discovery-models";4export * from "./discovery-models";
5export * from "./effect-errors";
5export * from "./registry";6export * from "./registry";
6export * from "./shared";7export * from "./shared";
7export * from "./source-models";8export * from "./source-models";
packages/sources/core/src/shared.ts+13−8
@@ -1,6 +1,8 @@
1import * as Data from "effect/Data";
1import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
3 4
5import { sourceCoreEffectError } from "./effect-errors";
4import {6import {
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
89export class SourceCredentialRequiredError extends Error {91export 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.expectedVersion198 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 )
packages/sources/google-discovery/src/adapter.ts+8−15
@@ -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(
packages/sources/google-discovery/src/tools.ts+3−4
@@ -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";
17import { sourceCoreEffectError } from "@executor/source-core";
17import * as Effect from "effect/Effect";18import * as Effect from "effect/Effect";
18import * as Layer from "effect/Layer";19import * as Layer from "effect/Layer";
19import * as Schema from "effect/Schema";20import * 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;
packages/sources/graphql/src/adapter.ts+3−6
@@ -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({
packages/sources/graphql/src/graphql-tools.ts+2−3
@@ -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";
15import { sourceCoreEffectError } from "@executor/source-core";
15import {16import {
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({
packages/sources/mcp/src/adapter.ts+3−4
@@ -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({
packages/sources/mcp/src/tools.ts+6−10
@@ -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
packages/sources/openapi/src/adapter.ts+6−11
@@ -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(
tools/oxlint/README.md+2−0
@@ -11,6 +11,8 @@ Why it lives here: · 10 unmodified lines
11Current rules:11Current 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
16To add another rule:18To add another rule:
tools/oxlint/plugin.mjs+4−0
@@ -1,6 +1,8 @@
1import { definePlugin } from "@oxlint/plugins";1import { definePlugin } from "@oxlint/plugins";
2 2
3import noCrossWorkspaceRelativeImports from "./rules/no-cross-workspace-relative-imports.mjs";3import noCrossWorkspaceRelativeImports from "./rules/no-cross-workspace-relative-imports.mjs";
4import noRawEffectFailErrors from "./rules/no-raw-effect-fail-errors.mjs";
5import noYieldEffectFail from "./rules/no-yield-effect-fail.mjs";
4import noWorkspaceSrcImports from "./rules/no-workspace-src-imports.mjs";6import noWorkspaceSrcImports from "./rules/no-workspace-src-imports.mjs";
5 7
6export default definePlugin({8export 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});
tools/oxlint/rules/no-raw-effect-fail-errors.mjs+109−0
@@ -0,0 +1,109 @@
1import { defineRule } from "@oxlint/plugins";
2
3const BUILTIN_ERROR_NAMES = new Set([
4 "AggregateError",
5 "Error",
6 "EvalError",
7 "RangeError",
8 "ReferenceError",
9 "SyntaxError",
10 "TypeError",
11 "URIError",
12]);
13
14const isIdentifierNamed = (node, name) =>
15 node?.type === "Identifier" && node.name === name;
16
17const 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
24const 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
44const readBuiltinFailedErrorName = (node) => {
45 if (!isEffectFailCall(node) || node.arguments.length === 0) {
46 return null;
47 }
48
49 return readBuiltinErrorName(node.arguments[0]);
50};
51
52export 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});
tools/oxlint/rules/no-yield-effect-fail.mjs+64−0
@@ -0,0 +1,64 @@
1import { defineRule } from "@oxlint/plugins";
2
3const isIdentifierNamed = (node, name) =>
4 node?.type === "Identifier" && node.name === name;
5
6const 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
13const 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
29export 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});