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
4040
4141 // Monorepo-specific rules.
4242 "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",
4345 "executor-monorepo/no-workspace-src-imports": "warn"
4446 },
4547
apps/executor/src/cli/main.ts+37−32
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process"; · 3 unmodified lines
44 import { dirname, resolve } from "node:path";
55 import { createInterface } from "node:readline/promises";
66 import { fileURLToPath } from "node:url";
7+import { FileSystem } from "@effect/platform";
78 import { Args, Command, Options } from "@effect/cli";
89 import {
910 NodeFileSystem,
@@ -52,6 +53,11 @@ import { · 42 unmodified lines
5253 parseInteractionPayload,
5354 } from "./pending-interaction-output";
5455 import { decideInteractionHandling } from "./interaction-handling";
56+import {
57+ executorAppEffectError,
58+ type LocalServerReachabilityTimeoutError,
59+ localServerReachabilityTimeoutError,
60+} from "../effect-errors";
5561
5662 const toError = (cause: unknown): Error =>
5763 cause instanceof Error ? cause : new Error(String(cause));
@@ -138,9 +144,7 @@ const readCode = (input: { · 80 unmodified lines
138144 }
139145 }
140146
141− return yield* Effect.fail(
142− new Error("Provide code as a positional argument, use --file, or pipe code over stdin."),
143− );
147+ return yield* executorAppEffectError("cli/main", "Provide code as a positional argument, use --file, or pipe code over stdin.");
144148 });
145149
146150 const getBootstrapClient = (baseUrl: string = DEFAULT_SERVER_BASE_URL) =>
@@ -471,36 +475,42 @@ const isPidRunning = (pid: number): boolean => { · 324 unmodified lines
471475 };
472476
473477
474−const readServerLogTail = async (
478+const readServerLogTail = (
475479 logFile: string = DEFAULT_SERVER_LOG_FILE,
476480 maxLines: number = 40,
477481 maxChars: number = 6000,
478−): Promise<string | null> => {
479− try {
480− const contents = await readFile(logFile, "utf8");
482+): Effect.Effect<string | null, never, FileSystem.FileSystem> =>
483+ Effect.gen(function* () {
484+ const fs = yield* FileSystem.FileSystem;
485+ const contents = yield* fs.readFileString(logFile, "utf8").pipe(
486+ Effect.catchAll(() => Effect.succeed<string | null>(null)),
487+ );
488+
489+ if (contents === null) {
490+ return null;
491+ }
492+
481493 const lines = contents.split(/\r?\n/u).filter((line) => line.length > 0);
482494 const tail = lines.slice(-maxLines).join("\n");
483495 return tail.length > maxChars ? tail.slice(-maxChars) : tail;
484− } catch {
485− return null;
486− }
487−};
488−
489−const buildReachabilityTimeoutError = async (
490− baseUrl: string,
491− expected: boolean,
492− logFile: string = DEFAULT_SERVER_LOG_FILE,
493−): Promise<Error> => {
494− const action = expected ? "start" : "shutdown";
495− const prefix = `Timed out waiting for local executor server ${action} at ${baseUrl}`;
496− const logTail = await readServerLogTail(logFile);
496+ });
497497
498− if (!logTail) {
499− return new Error(`${prefix}\n\nDaemon log: ${logFile}`);
500− }
498+const failReachabilityTimeout = (input: {
499+ baseUrl: string;
500+ expected: boolean;
501+ logFile?: string;
502+}): Effect.Effect<never, LocalServerReachabilityTimeoutError, FileSystem.FileSystem> =>
503+ Effect.gen(function* () {
504+ const logFile = input.logFile ?? DEFAULT_SERVER_LOG_FILE;
505+ const logTail = yield* readServerLogTail(logFile);
501506
502− return new Error(`${prefix}\n\nRecent daemon log (${logFile}):\n${logTail}`);
503−};
507+ return yield* localServerReachabilityTimeoutError({
508+ baseUrl: input.baseUrl,
509+ expected: input.expected,
510+ logFile,
511+ logTail,
512+ });
513+ });
504514
505515 const waitForReachability = (baseUrl: string, expected: boolean) =>
506516 Effect.gen(function* () {
@@ -513,12 +523,7 @@ const waitForReachability = (baseUrl: string, expected: boolean) => · 6 unmodified lines
513523 yield* sleep(SERVER_POLL_INTERVAL_MS);
514524 }
515525
516− const error = yield* Effect.tryPromise({
517− try: () => buildReachabilityTimeoutError(baseUrl, expected),
518− catch: toError,
519− });
520−
521− return yield* Effect.fail(error);
526+ return yield* failReachabilityTimeout({ baseUrl, expected });
522527 });
523528
524529 type LocalServerStatus = {
@@ -668,7 +673,7 @@ const stopServer = (baseUrl: string) => · 143 unmodified lines
668673 Effect.catchAll(() =>
669674 Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
670675 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}`))),
672677 ),
673678 ),
674679 );
apps/executor/src/distribution/harness.ts+3−2
@@ -9,6 +9,7 @@ import * as Effect from "effect/Effect"; · 8 unmodified lines
99 import * as Layer from "effect/Layer";
1010
1111 import { buildDistributionPackage } from "./artifact";
12+import { executorAppEffectError } from "../effect-errors";
1213
1314 type CommandResult = {
1415 readonly stdout: string;
@@ -96,7 +97,7 @@ const runCommand = (input: { · 81 unmodified lines
9697 return;
9798 }
9899
99− resume(Effect.fail(new Error(
100+ resume(Effect.fail(executorAppEffectError("distribution/harness",
100101 [
101102 `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`,
102103 stdout.length > 0 ? `stdout:\n${stdout.trim()}` : null,
@@ -161,7 +162,7 @@ const packPackage = (packageDir: string, outputDir: string) => · 58 unmodified lines
161162
162163 if (!tarballName) {
163164 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}`),
165166 );
166167 }
167168
apps/executor/src/effect-errors.ts+40−0
@@ -0,0 +1,40 @@
1+import * as Data from "effect/Data";
2+
3+export class ExecutorAppEffectError extends Data.TaggedError(
4+ "ExecutorAppEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const executorAppEffectError = (
11+ module: string,
12+ message: string,
13+) => new ExecutorAppEffectError({ module, message });
14+
15+export class LocalServerReachabilityTimeoutError extends Data.TaggedError(
16+ "LocalServerReachabilityTimeoutError",
17+)<{
18+ readonly baseUrl: string;
19+ readonly action: "start" | "shutdown";
20+ readonly logFile: string;
21+ readonly logTail: string | null;
22+ readonly message: string;
23+}> {}
24+
25+export const localServerReachabilityTimeoutError = (input: {
26+ baseUrl: string;
27+ expected: boolean;
28+ logFile: string;
29+ logTail: string | null;
30+}) =>
31+ new LocalServerReachabilityTimeoutError({
32+ baseUrl: input.baseUrl,
33+ action: input.expected ? "start" : "shutdown",
34+ logFile: input.logFile,
35+ logTail: input.logTail,
36+ message:
37+ input.logTail === null
38+ ? `Timed out waiting for local executor server ${input.expected ? "start" : "shutdown"} at ${input.baseUrl}\n\nDaemon log: ${input.logFile}`
39+ : `Timed out waiting for local executor server ${input.expected ? "start" : "shutdown"} at ${input.baseUrl}\n\nRecent daemon log (${input.logFile}):\n${input.logTail}`,
40+ });
packages/auth/mcp-oauth/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1+import * as Data from "effect/Data";
2+
3+export class McpOAuthEffectError extends Data.TaggedError(
4+ "McpOAuthEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const mcpOAuthEffectError = (
11+ module: string,
12+ message: string,
13+) => new McpOAuthEffectError({ module, message });
packages/auth/mcp-oauth/src/index.ts+3−6
@@ -8,6 +8,7 @@ import type { · 7 unmodified lines
88 OAuthTokens,
99 } from "@modelcontextprotocol/sdk/shared/auth.js";
1010 import * as Effect from "effect/Effect";
11+import { mcpOAuthEffectError } from "./effect-errors";
1112
1213 type JsonValue =
1314 | string
@@ -122,9 +123,7 @@ export const startMcpOAuthAuthorization = (input: { · 108 unmodified lines
122123 });
123124
124125 if (result !== "REDIRECT" || !captured.authorizationUrl || !captured.codeVerifier) {
125− return yield* Effect.fail(
126− new Error("OAuth flow did not produce an authorization redirect"),
127− );
126+ return yield* mcpOAuthEffectError("index", "OAuth flow did not produce an authorization redirect");
128127 }
129128
130129 return {
@@ -197,9 +196,7 @@ export const exchangeMcpOAuthAuthorizationCode = (input: { · 66 unmodified lines
197196 });
198197
199198 if (result !== "AUTHORIZED" || !captured.tokens) {
200− return yield* Effect.fail(
201− new Error("OAuth redirect did not complete MCP OAuth setup"),
202− );
199+ return yield* mcpOAuthEffectError("index", "OAuth redirect did not complete MCP OAuth setup");
203200 }
204201
205202 return {
packages/dev/kitchen-sink/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1+import * as Data from "effect/Data";
2+
3+export class KitchenSinkEffectError extends Data.TaggedError(
4+ "KitchenSinkEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const kitchenSinkEffectError = (
11+ module: string,
12+ message: string,
13+) => new KitchenSinkEffectError({ module, message });
packages/dev/kitchen-sink/src/kitchen-sink.test.ts+2−1
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
1111 toExecutorTool,
1212 } from "@executor/codemode-core";
1313 import { makeSesExecutor } from "@executor/runtime-ses";
14+import { kitchenSinkEffectError } from "./effect-errors";
1415
1516 const numberPairInputSchema = Schema.standardSchemaV1(
1617 Schema.Struct({
@@ -100,7 +101,7 @@ describe("kitchen-sink", () => { · 83 unmodified lines
100101 }).execute;
101102
102103 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");
104105 }
105106
106107 const output = yield* Effect.tryPromise({
packages/hosts/ai-sdk/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1+import * as Data from "effect/Data";
2+
3+export class AiSdkEffectError extends Data.TaggedError(
4+ "AiSdkEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const aiSdkEffectError = (
11+ module: string,
12+ message: string,
13+) => new AiSdkEffectError({ module, message });
packages/hosts/ai-sdk/src/index.ts+2−1
@@ -12,6 +12,7 @@ import { · 11 unmodified lines
1212 type ToolMap,
1313 type ToolInvoker,
1414 } from "@executor/codemode-core";
15+import { aiSdkEffectError } from "./effect-errors";
1516
1617 export type AiSdkToolMap = ToolSet;
1718
@@ -49,7 +50,7 @@ export function createCodeTool(input: { · 31 unmodified lines
4950 input.executor.execute(code, input.toolInvoker).pipe(
5051 Effect.flatMap((result: ExecuteResult) =>
5152 result.error
52− ? Effect.fail(new Error(result.error))
53+ ? Effect.fail(aiSdkEffectError("index", result.error))
5354 : Effect.succeed({
5455 code,
5556 result: result.result,
packages/kernel/core/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1+import * as Data from "effect/Data";
2+
3+export class KernelCoreEffectError extends Data.TaggedError(
4+ "KernelCoreEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const kernelCoreEffectError = (
11+ module: string,
12+ message: string,
13+) => new KernelCoreEffectError({ module, message });
packages/kernel/core/src/tool-map.ts+5−4
@@ -22,6 +22,7 @@ import type { · 21 unmodified lines
2222 ToolMetadata,
2323 ToolPath,
2424 } from "./types";
25+import { kernelCoreEffectError } from "./effect-errors";
2526
2627 type ResolvedTool = {
2728 path: ToolPath;
@@ -88,7 +89,7 @@ const parseInput = (input: { · 60 unmodified lines
8889 const validate = getSchemaValidator(input.schema);
8990 if (!validate) {
9091 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`),
9293 );
9394 }
9495
@@ -99,7 +100,7 @@ const parseInput = (input: { · 4 unmodified lines
99100 Effect.flatMap((result) => {
100101 if ("issues" in result && result.issues) {
101102 return Effect.fail(
102− new Error(
103+ kernelCoreEffectError("tool-map",
103104 `Input validation failed for ${input.path}: ${formatIssues(result.issues)}`,
104105 ),
105106 );
@@ -213,7 +214,7 @@ const mergeAcceptedElicitationContent = (input: { · 107 unmodified lines
213214
214215 if (!isRecord(input.args)) {
215216 return Effect.fail(
216− new Error(
217+ kernelCoreEffectError("tool-map",
217218 `Tool ${input.path} cannot merge elicitation content into non-object arguments`,
218219 ),
219220 );
@@ -492,7 +493,7 @@ export const makeToolInvokerFromTools = (input: { · 272 unmodified lines
492493 Effect.gen(function* () {
493494 const entry = byPath.get(path);
494495 if (!entry) {
495− return yield* Effect.fail(new Error(`Unknown tool path: ${path}`));
496+ return yield* kernelCoreEffectError("tool-map", `Unknown tool path: ${path}`);
496497 }
497498
498499 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
127127 : parseSecretStoreProviderId(payload.providerId);
128128
129129 if (name.length === 0) {
130− return yield* Effect.fail(
131− new ControlPlaneBadRequestError({
130+ return yield* new ControlPlaneBadRequestError({
132131 operation: "secrets.create",
133132 message: "Secret name is required.",
134133 details: "Secret name is required.",
135− }),
136− );
134+ });
137135 }
138136 if (payload.providerId !== undefined && requestedProviderId === null) {
139− return yield* Effect.fail(
140− new ControlPlaneBadRequestError({
137+ return yield* new ControlPlaneBadRequestError({
141138 operation: "secrets.create",
142139 message: `Unsupported secret provider: ${payload.providerId}`,
143140 details: `Unsupported secret provider: ${payload.providerId}`,
144− }),
145− );
141+ });
146142 }
147143
148144 const store = yield* ControlPlaneStore;
@@ -165,7 +161,7 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 16 unmodified lines
165161 );
166162
167163 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}`);
169165 }
170166
171167 return {
@@ -188,13 +184,11 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 16 unmodified lines
188184 );
189185
190186 if (Option.isNone(existing)) {
191− return yield* Effect.fail(
192− new ControlPlaneNotFoundError({
187+ return yield* new ControlPlaneNotFoundError({
193188 operation: "secrets.update",
194189 message: `Secret not found: ${path.secretId}`,
195190 details: `Secret not found: ${path.secretId}`,
196− }),
197− );
191+ });
198192 }
199193
200194 const update: { name?: string | null; value?: string } = {};
@@ -234,13 +228,11 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 33 unmodified lines
234228 );
235229
236230 if (Option.isNone(existing)) {
237− return yield* Effect.fail(
238− new ControlPlaneNotFoundError({
231+ return yield* new ControlPlaneNotFoundError({
239232 operation: "secrets.delete",
240233 message: `Secret not found: ${path.secretId}`,
241234 details: `Secret not found: ${path.secretId}`,
242− }),
243− );
235+ });
244236 }
245237
246238 const deleteSecretMaterial = createDefaultSecretMaterialDeleter({
@@ -254,7 +246,7 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 7 unmodified lines
254246 );
255247
256248 if (!removed) {
257− return yield* Effect.fail(storageError(`Failed removing secret: ${path.secretId}`));
249+ return yield* storageError(`Failed removing secret: ${path.secretId}`);
258250 }
259251
260252 return { removed: true };
packages/platform/control-plane/src/runtime/auth/auth-artifacts.ts+5−8
@@ -35,6 +35,7 @@ import type { · 34 unmodified lines
3535 ResolveSecretMaterial,
3636 SecretMaterialResolveContext,
3737 } from "../local/secret-material-providers";
38+import { runtimeEffectError } from "../effect-errors";
3839
3940 export type ResolvedSourceAuthMaterial = {
4041 placements: ReadonlyArray<RequestPlacement>;
@@ -456,9 +457,7 @@ export const resolveAuthArtifactMaterial = (input: { · 415 unmodified lines
456457 const providerGrantRefConfig = decodeProviderGrantRefAuthArtifactConfig(input.artifact);
457458 if (providerGrantRefConfig !== null) {
458459 if (input.lease === null || input.lease === undefined) {
459− return yield* Effect.fail(
460− new Error(`Provider grant auth artifact ${input.artifact.id} requires a lease`),
461− );
460+ return yield* runtimeEffectError("auth/auth-artifacts", `Provider grant auth artifact ${input.artifact.id} requires a lease`);
462461 }
463462 }
464463
@@ -470,18 +469,16 @@ export const resolveAuthArtifactMaterial = (input: { · 5 unmodified lines
470469 }
471470
472471 if (input.lease === null || input.lease === undefined) {
473− return yield* Effect.fail(
474− new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`),
475− );
472+ return yield* runtimeEffectError("auth/auth-artifacts", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`);
476473 }
477474
478475 const templatesEither = decodeRequestPlacementTemplates(
479476 input.lease.placementsTemplateJson,
480477 );
481478 if (templatesEither._tag === "Left") {
482− return yield* Effect.fail(new Error(
479+ return yield* runtimeEffectError("auth/auth-artifacts",
483480 `Invalid auth lease placements for artifact ${input.artifact.id}`,
484− ));
481+ );
485482 }
486483
487484 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
2727 import { refreshOAuth2AccessToken } from "./oauth2-pkce";
2828 import type { OAuth2TokenResponse } from "./oauth2-pkce";
2929 import { createPersistedMcpAuthProvider } from "./mcp-auth-provider";
30+import { runtimeEffectError } from "../effect-errors";
3031
3132 const LEASE_REFRESH_SKEW_MS = 60_000;
3233
@@ -92,9 +93,7 @@ const refreshRefreshableOauth2AuthorizedUserArtifact = (input: { · 59 unmodified lines
9293 decoded === null
9394 || decoded.artifactKind !== RefreshableOAuth2AuthorizedUserAuthArtifactKind
9495 ) {
95− return yield* Effect.fail(
96− new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`),
97− );
96+ return yield* runtimeEffectError("auth/auth-leases", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`);
9897 }
9998
10099 const refreshToken = yield* input.resolveSecretMaterial({
@@ -204,24 +203,18 @@ const refreshProviderGrantRefArtifact = (input: { · 103 unmodified lines
204203 Effect.gen(function* () {
205204 const config = decodeProviderGrantRefAuthArtifactConfig(input.artifact);
206205 if (config === null) {
207− return yield* Effect.fail(
208− new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`),
209− );
206+ return yield* runtimeEffectError("auth/auth-leases", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`);
210207 }
211208
212209 const grantOption = yield* input.rows.providerAuthGrants.getById(config.grantId);
213210 if (Option.isNone(grantOption)) {
214− return yield* Effect.fail(
215− new Error(`Provider auth grant not found: ${config.grantId}`),
216− );
211+ return yield* runtimeEffectError("auth/auth-leases", `Provider auth grant not found: ${config.grantId}`);
217212 }
218213 const grant = grantOption.value;
219214
220215 const oauthClientOption = yield* input.rows.workspaceOauthClients.getById(grant.oauthClientId);
221216 if (Option.isNone(oauthClientOption)) {
222− return yield* Effect.fail(
223− new Error(`Workspace OAuth client not found: ${grant.oauthClientId}`),
224− );
217+ return yield* runtimeEffectError("auth/auth-leases", `Workspace OAuth client not found: ${grant.oauthClientId}`);
225218 }
226219 const oauthClient = oauthClientOption.value;
227220
@@ -349,9 +342,7 @@ export const upsertOauth2AuthorizedUserLeaseFromTokenResponse = (input: { · 121 unmodified lines
349342 decoded === null
350343 || decoded.artifactKind !== RefreshableOAuth2AuthorizedUserAuthArtifactKind
351344 ) {
352− return yield* Effect.fail(
353− new Error(`Unsupported auth artifact kind: ${input.artifact.artifactKind}`),
354− );
345+ return yield* runtimeEffectError("auth/auth-leases", `Unsupported auth artifact kind: ${input.artifact.artifactKind}`);
355346 }
356347
357348 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
2121 SecretMaterialResolveContext,
2222 } from "../local/secret-material-providers";
2323 import { SecretMaterialResolverService } from "../local/secret-material-providers";
24+import { runtimeEffectError } from "../effect-errors";
2425
2526 const authForSlot = (input: {
2627 source: Source;
@@ -114,9 +115,7 @@ export const resolveSourceAuthMaterialWithDeps = (input: { · 87 unmodified lines
114115 || artifact?.artifactKind === "provider_grant_ref"
115116 || artifact?.artifactKind === "mcp_oauth"
116117 ) {
117− return yield* Effect.fail(
118− new Error("Dynamic auth artifacts require persistence-backed lease resolution"),
119− );
118+ return yield* runtimeEffectError("auth/source-auth-material", "Dynamic auth artifacts require persistence-backed lease resolution");
120119 }
121120
122121 return yield* resolveAuthArtifactMaterial({
packages/platform/control-plane/src/runtime/catalog/source/runtime.ts+4−5
@@ -46,6 +46,7 @@ import { · 45 unmodified lines
4646 RuntimeSourceStoreService,
4747 type RuntimeSourceStore,
4848 } from "../../sources/source-store";
49+import { runtimeEffectError } from "../../effect-errors";
4950
5051 type CatalogImportMetadata = CatalogSnapshotV1["import"];
5152
@@ -672,7 +673,7 @@ const ensureRuntimeCatalogWorkspace = ( · 620 unmodified lines
672673 ) => {
673674 if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
674675 return Effect.fail(
675− new Error(
676+ runtimeEffectError("catalog/source/runtime",
676677 `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
677678 ),
678679 );
@@ -762,12 +763,10 @@ const loadSourceWithCatalogWithDeps = (deps: RuntimeSourceCatalogStoreDeps, inpu · 83 unmodified lines
762763 sourceId: source.id,
763764 });
764765 if (artifact === null) {
765− return yield* Effect.fail(
766− new LocalSourceArtifactMissingError({
766+ return yield* new LocalSourceArtifactMissingError({
767767 message: `Catalog artifact missing for source ${input.sourceId}`,
768768 sourceId: input.sourceId,
769− }),
770− );
769+ });
771770 }
772771
773772 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
3535 import {
3636 refreshSourceTypeDeclarationInBackground,
3737 } from "./type-declarations";
38+import { runtimeEffectError } from "../../effect-errors";
3839
3940 const shouldIndexSource = (source: Source): boolean =>
4041 source.enabled
@@ -77,7 +78,7 @@ const ensureRuntimeCatalogSyncWorkspace = ( · 36 unmodified lines
7778 ) => {
7879 if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
7980 return Effect.fail(
80− new Error(
81+ runtimeEffectError("catalog/source/sync",
8182 `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
8283 ),
8384 );
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
3232 import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions";
3333 import { persistSource } from "./sources/source-store";
3434 import { withControlPlaneClient } from "./execution/test-http-client";
35+import { runtimeEffectError } from "./effect-errors";
3536
3637 const makeRuntime = Effect.gen(function* () {
3738 const fs = yield* FileSystem.FileSystem;
@@ -369,7 +370,7 @@ const expectLeft = <A, E>(effect: Effect.Effect<A, E, never>) => · 331 unmodified lines
369370 Effect.flatMap((result) =>
370371 result._tag === "Left"
371372 ? 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")),
373374 ),
374375 );
375376
packages/platform/control-plane/src/runtime/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1+import * as Data from "effect/Data";
2+
3+export class RuntimeEffectError extends Data.TaggedError(
4+ "RuntimeEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const runtimeEffectError = (
11+ module: string,
12+ message: string,
13+) => new RuntimeEffectError({ module, message });
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
55 import type { LoadedSourceCatalogToolIndexEntry } from "../catalog/source/runtime";
66 import type { ResolvedSourceAuthMaterial } from "../auth/source-auth-material";
77 import { getSourceAdapter } from "../sources/source-adapters";
8+import { runtimeEffectError } from "../effect-errors";
89
910 export const invocationDescriptorFromTool = (input: {
1011 tool: LoadedSourceCatalogToolIndexEntry;
@@ -49,7 +50,7 @@ export const invokeIrTool = (input: { · 38 unmodified lines
4950 const adapter = getSourceAdapter(input.tool.executable.adapterKey);
5051 if (adapter.key !== input.tool.source.kind) {
5152 return Effect.fail(
52− new Error(
53+ runtimeEffectError("execution/ir-execution",
5354 `Executable ${input.tool.executable.id} expects adapter ${adapter.key}, but source ${input.tool.source.id} is ${input.tool.source.kind}`,
5455 ),
5556 );
packages/platform/control-plane/src/runtime/execution/service.ts+32−28
@@ -23,6 +23,7 @@ import { · 22 unmodified lines
2323 type ExecutionInteraction,
2424 type WorkspaceId,
2525 } from "#schema";
26+import * as Data from "effect/Data";
2627 import * as Deferred from "effect/Deferred";
2728 import * as Effect from "effect/Effect";
2829 import * as Option from "effect/Option";
@@ -48,6 +49,7 @@ import { · 19 unmodified lines
4849 type ControlPlaneStoreShape,
4950 } from "../store";
5051 import { RuntimeExecutionResolverService } from "./workspace/environment";
52+import { runtimeEffectError } from "../effect-errors";
5153
5254 const executionOps = {
5355 create: operationErrors("executions.create"),
@@ -164,15 +166,29 @@ const resolveInteractionMode = ( · 110 unmodified lines
164166 ): InteractionMode =>
165167 value === "live" || value === "live_form" ? value : DEFAULT_INTERACTION_MODE;
166168
169+class ExecutionSuspendedError extends Data.TaggedError(
170+ "ExecutionSuspendedError",
171+)<{
172+ readonly executionId: Execution["id"];
173+ readonly interactionId: string;
174+ readonly message: string;
175+}> {}
176+
167177 const createExecutionSuspendedError = (input: {
168178 executionId: Execution["id"];
169179 interactionId: string;
170−}): Error =>
171− new Error(
172− `${EXECUTION_SUSPENDED_SENTINEL}:${input.executionId}:${input.interactionId}`,
173− );
180+}): ExecutionSuspendedError =>
181+ new ExecutionSuspendedError({
182+ executionId: input.executionId,
183+ interactionId: input.interactionId,
184+ message: `${EXECUTION_SUSPENDED_SENTINEL}:${input.executionId}:${input.interactionId}`,
185+ });
174186
175187 const isExecutionSuspendedValue = (value: unknown): boolean => {
188+ if (value instanceof ExecutionSuspendedError) {
189+ return true;
190+ }
191+
176192 if (value instanceof Error) {
177193 return value.message.includes(EXECUTION_SUSPENDED_SENTINEL);
178194 }
@@ -242,12 +258,10 @@ const fetchExecution = ( · 63 unmodified lines
242258 );
243259
244260 if (Option.isNone(existing)) {
245− return yield* Effect.fail(
246− errors.notFound(
261+ return yield* errors.notFound(
247262 "Execution not found",
248263 `workspaceId=${input.workspaceId} executionId=${input.executionId}`,
249− ),
250− );
264+ );
251265 }
252266
253267 return existing.value;
@@ -378,12 +392,10 @@ const suspendExecutionForInteraction = (input: { · 124 unmodified lines
378392 state: "waiting_for_interaction",
379393 });
380394
381− return yield* Effect.fail(
382− createExecutionSuspendedError({
395+ return yield* createExecutionSuspendedError({
383396 executionId: input.executionId,
384397 interactionId: input.interactionId,
385− }),
386− );
398+ });
387399 });
388400
389401 const createHybridOnElicitation = (input: {
@@ -487,12 +499,10 @@ const createReplayToolInvoker = (input: { · 97 unmodified lines
487499 }
488500
489501 if (existing.value.status === "failed") {
490− return yield* Effect.fail(
491− new Error(
502+ return yield* runtimeEffectError("execution/service",
492503 existing.value.errorText
493504 ?? `Stored tool step ${String(stepSequence)} failed`,
494− ),
495− );
505+ );
496506 }
497507 } else {
498508 const now = Date.now();
@@ -834,12 +844,10 @@ const createExecutionWithDependencies = ( · 335 unmodified lines
834844 );
835845
836846 if (Option.isNone(running)) {
837− return yield* Effect.fail(
838− executionOps.create.notFound(
847+ return yield* executionOps.create.notFound(
839848 "Execution not found after insert",
840849 `executionId=${execution.id}`,
841− ),
842− );
850+ );
843851 }
844852
845853 const nextState = yield* liveExecutionManager.registerStateWaiter(execution.id);
@@ -936,12 +944,10 @@ export const resumeExecution = (input: { · 90 unmodified lines
936944 && existing.pendingInteraction !== null
937945 )
938946 ) {
939− return yield* Effect.fail(
940− executionOps.resume.badRequest(
947+ return yield* executionOps.resume.badRequest(
941948 "Execution is not waiting for interaction",
942949 `executionId=${input.executionId} status=${existing.execution.status}`,
943− ),
944− );
950+ );
945951 }
946952
947953 const responseJson = input.payload.responseJson;
@@ -988,12 +994,10 @@ export const resumeExecution = (input: { · 40 unmodified lines
988994 );
989995
990996 if (!resumed) {
991− return yield* Effect.fail(
992− executionOps.resume.badRequest(
997+ return yield* executionOps.resume.badRequest(
993998 "Resume is unavailable for this execution",
994999 `executionId=${input.executionId}`,
995− ),
996− );
1000+ );
9971001 }
9981002 }
9991003
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
1010 import { invocationDescriptorFromTool } from "../ir-execution";
1111 import { evaluateInvocationPolicy } from "../../policy/invocation-policy-engine";
1212 import { loadRuntimeLocalWorkspacePolicies } from "../../policy/policies-operations";
13+import { runtimeEffectError } from "../../effect-errors";
1314
1415 const asToolPath = (value: string): ToolPath => value as ToolPath;
1516
@@ -98,15 +99,13 @@ export const authorizePersistedToolInvocation = (input: { · 82 unmodified lines
9899 }
99100
100101 if (decision.kind === "deny") {
101− return yield* Effect.fail(new Error(decision.reason));
102+ return yield* runtimeEffectError("execution/workspace/authorization", decision.reason);
102103 }
103104
104105 if (!input.onElicitation) {
105− return yield* Effect.fail(
106− new Error(
106+ return yield* runtimeEffectError("execution/workspace/authorization",
107107 `Approval required for ${descriptor.toolPath}, but no elicitation-capable host is available`,
108− ),
109− );
108+ );
110109 }
111110
112111 const interactionId =
@@ -145,10 +144,8 @@ export const authorizePersistedToolInvocation = (input: { · 32 unmodified lines
145144 );
146145
147146 if (response.action !== "accept") {
148− return yield* Effect.fail(
149− new Error(
147+ return yield* runtimeEffectError("execution/workspace/authorization",
150148 `Tool invocation not approved for ${descriptor.toolPath}`,
151− ),
152− );
149+ );
153150 }
154151 });
packages/platform/control-plane/src/runtime/execution/workspace/tool-invoker.ts+2−3
@@ -36,6 +36,7 @@ import { · 35 unmodified lines
3636 createWorkspaceSourceCatalog,
3737 loadWorkspaceCatalogToolByPath,
3838 } from "./source-catalog";
39+import { runtimeEffectError } from "../../effect-errors";
3940
4041 export const createWorkspaceToolInvoker = (input: {
4142 workspaceId: Source["workspaceId"];
@@ -120,9 +121,7 @@ export const createWorkspaceToolInvoker = (input: { · 78 unmodified lines
120121 includeSchemas: false,
121122 });
122123 if (!catalogTool) {
123− return yield* Effect.fail(
124− new Error(`Unknown tool path: ${invocation.path}`),
125− );
124+ return yield* runtimeEffectError("execution/workspace/tool-invoker", `Unknown tool path: ${invocation.path}`);
126125 }
127126
128127 yield* authorizePersistedToolInvocation({
packages/platform/control-plane/src/runtime/local/operations.ts+14−28
@@ -137,12 +137,10 @@ const loadSourceCredentialInteraction = (input: { · 136 unmodified lines
137137 );
138138
139139 if (Option.isNone(stored)) {
140− return yield* Effect.fail(
141− input.operation.notFound(
140+ return yield* input.operation.notFound(
142141 "Source credential request not found",
143142 `interactionId=${input.interactionId}`,
144− ),
145− );
143+ );
146144 }
147145
148146 const decoded = decodeSourceCredentialInteraction(stored.value);
@@ -151,12 +149,10 @@ const loadSourceCredentialInteraction = (input: { · 2 unmodified lines
151149 || decoded.workspaceId !== input.workspaceId
152150 || decoded.sourceId !== input.sourceId
153151 ) {
154− return yield* Effect.fail(
155− input.operation.notFound(
152+ return yield* input.operation.notFound(
156153 "Source credential request not found",
157154 `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`,
158− ),
159− );
155+ );
160156 }
161157
162158 const source = yield* sourceAuthService.getSourceById({
@@ -226,12 +222,10 @@ export const submitSourceCredentialInteraction = (input: { · 63 unmodified lines
226222 });
227223
228224 if (interaction.status !== "pending") {
229− return yield* Effect.fail(
230− localOps.sourceCredentialSubmit.badRequest(
225+ return yield* localOps.sourceCredentialSubmit.badRequest(
231226 "Source credential request is no longer active",
232227 `interactionId=${interaction.interactionId} status=${interaction.status}`,
233− ),
234− );
228+ );
235229 }
236230
237231 const liveExecutionManager = yield* LiveExecutionManagerService;
@@ -260,12 +254,10 @@ export const submitSourceCredentialInteraction = (input: { · 22 unmodified lines
260254 );
261255
262256 if (!persisted) {
263− return yield* Effect.fail(
264− localOps.sourceCredentialSubmit.badRequest(
257+ return yield* localOps.sourceCredentialSubmit.badRequest(
265258 "Source credential request is no longer resumable",
266259 `interactionId=${interaction.interactionId}`,
267− ),
268− );
260+ );
269261 }
270262 }
271263
@@ -302,12 +294,10 @@ export const submitSourceCredentialInteraction = (input: { · 30 unmodified lines
302294 );
303295
304296 if (!persisted) {
305− return yield* Effect.fail(
306− localOps.sourceCredentialSubmit.badRequest(
297+ return yield* localOps.sourceCredentialSubmit.badRequest(
307298 "Source credential request is no longer resumable",
308299 `interactionId=${interaction.interactionId}`,
309− ),
310− );
300+ );
311301 }
312302 }
313303
@@ -320,12 +310,10 @@ export const submitSourceCredentialInteraction = (input: { · 6 unmodified lines
320310
321311 const token = trimOrNull(input.token);
322312 if (token === null) {
323− return yield* Effect.fail(
324− localOps.sourceCredentialSubmit.badRequest(
313+ return yield* localOps.sourceCredentialSubmit.badRequest(
325314 "Missing token",
326315 `interactionId=${interaction.interactionId}`,
327− ),
328− );
316+ );
329317 }
330318
331319 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
@@ -365,12 +353,10 @@ export const submitSourceCredentialInteraction = (input: { · 33 unmodified lines
365353 );
366354
367355 if (!persisted) {
368− return yield* Effect.fail(
369− localOps.sourceCredentialSubmit.badRequest(
356+ return yield* localOps.sourceCredentialSubmit.badRequest(
370357 "Source credential request is no longer resumable",
371358 `interactionId=${interaction.interactionId}`,
372− ),
373− );
359+ );
374360 }
375361 }
376362
packages/platform/control-plane/src/runtime/local/runtime-context.ts+4−8
@@ -55,24 +55,20 @@ export const requireRuntimeLocalWorkspace = ( · 54 unmodified lines
5555 Effect.gen(function* () {
5656 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
5757 if (runtimeLocalWorkspace === null) {
58− return yield* Effect.fail(
59− new RuntimeLocalWorkspaceUnavailableError({
58+ return yield* new RuntimeLocalWorkspaceUnavailableError({
6059 message: "Runtime local workspace is unavailable",
61− }),
62− );
60+ });
6361 }
6462
6563 if (
6664 workspaceId !== undefined
6765 && runtimeLocalWorkspace.installation.workspaceId !== workspaceId
6866 ) {
69− return yield* Effect.fail(
70− new RuntimeLocalWorkspaceMismatchError({
67+ return yield* new RuntimeLocalWorkspaceMismatchError({
7168 message: `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`,
7269 requestedWorkspaceId: workspaceId,
7370 activeWorkspaceId: runtimeLocalWorkspace.installation.workspaceId,
74− }),
75− );
71+ });
7672 }
7773
7874 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
2222 import { getRuntimeLocalWorkspaceOption } from "./runtime-context";
2323 import { ControlPlaneStore } from "../store";
2424 import type { ControlPlaneStoreShape } from "../store";
25+import { runtimeEffectError } from "../effect-errors";
2526
2627 export const ENV_SECRET_PROVIDER_ID = "env";
2728 export const PARAMS_SECRET_PROVIDER_ID = "params";
@@ -304,7 +305,7 @@ const ensureCommandSuccess = (input: { · 276 unmodified lines
304305 ?? ensureNonEmptyString(input.result.stdout)
305306 ?? "command returned non-zero exit code";
306307
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}`));
308309 };
309310
310311 const commandAvailabilityCache = new Map<string, Promise<boolean>>();
@@ -389,9 +390,7 @@ const loadStoredSecretMaterial = (input: { · 78 unmodified lines
389390 const materialId = SecretMaterialIdSchema.make(input.id);
390391 const stored = yield* input.runtime.rows.secretMaterials.getById(materialId);
391392 if (Option.isNone(stored)) {
392− return yield* Effect.fail(
393− new Error(`${input.operation}: secret material not found: ${input.id}`),
394− );
393+ return yield* runtimeEffectError("local/secret-material-providers", `${input.operation}: secret material not found: ${input.id}`);
395394 }
396395
397396 return stored.value;
@@ -437,11 +436,9 @@ const loadManagedKeychainRef = (input: { · 39 unmodified lines
437436 operation: input.operation,
438437 });
439438 if (material.providerId !== KEYCHAIN_SECRET_PROVIDER_ID) {
440− return yield* Effect.fail(
441− new Error(
439+ return yield* runtimeEffectError("local/secret-material-providers",
442440 `${input.operation}: secret ${material.id} is stored in provider '${material.providerId}', not '${KEYCHAIN_SECRET_PROVIDER_ID}'`,
443− ),
444− );
441+ );
445442 }
446443
447444 return {
@@ -502,7 +499,7 @@ const readKeychainSecretValue = (input: { · 54 unmodified lines
502499 );
503500 default:
504501 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}'`),
506503 );
507504 }
508505 };
@@ -567,7 +564,7 @@ const writeKeychainSecretValue = (input: { · 58 unmodified lines
567564 );
568565 default:
569566 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}'`),
571568 );
572569 }
573570 };
@@ -609,7 +606,7 @@ const deleteKeychainSecretValue = (input: { · 35 unmodified lines
609606 );
610607 default:
611608 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}'`),
613610 );
614611 }
615612 };
@@ -618,7 +615,7 @@ const createParamsSecretMaterialProvider = (): SecretMaterialProvider => ({ · 2 unmodified lines
618615 resolve: ({ ref, context }) => {
619616 const value = ensureNonEmptyString(context.params?.[ref.handle]);
620617 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`));
622619 }
623620
624621 return Effect.succeed(value);
@@ -631,7 +628,7 @@ const createEnvSecretMaterialProvider = (): SecretMaterialProvider => ({ · 6 unmodified lines
631628 resolve: ({ ref, runtime }) => {
632629 if (!runtime.dangerouslyAllowEnvSecrets) {
633630 return Effect.fail(
634− new Error(
631+ runtimeEffectError("local/secret-material-providers",
635632 `Env-backed secrets are disabled. Set ${DANGEROUSLY_ALLOW_ENV_SECRETS_ENV}=true to allow provider '${ENV_SECRET_PROVIDER_ID}'.`,
636633 ),
637634 );
@@ -639,7 +636,7 @@ const createEnvSecretMaterialProvider = (): SecretMaterialProvider => ({ · 1 unmodified line
639636
640637 const value = ensureNonEmptyString(runtime.env[ref.handle]);
641638 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`));
643640 }
644641
645642 return Effect.succeed(value);
@@ -657,16 +654,12 @@ const createLocalSecretMaterialProvider = (): SecretMaterialProvider => ({ · 11 unmodified lines
657654 operation: "local.get",
658655 });
659656 if (stored.providerId !== LOCAL_SECRET_PROVIDER_ID) {
660− return yield* Effect.fail(
661− new Error(
657+ return yield* runtimeEffectError("local/secret-material-providers",
662658 `local.get: secret ${stored.id} is stored in provider '${stored.providerId}', not '${LOCAL_SECRET_PROVIDER_ID}'`,
663− ),
664− );
659+ );
665660 }
666661 if (stored.value === null) {
667− return yield* Effect.fail(
668− new Error(`local.get: secret ${stored.id} does not have a local value`),
669− );
662+ return yield* runtimeEffectError("local/secret-material-providers", `local.get: secret ${stored.id} does not have a local value`);
670663 }
671664
672665 return stored.value;
@@ -690,11 +683,9 @@ const createLocalSecretMaterialProvider = (): SecretMaterialProvider => ({ · 17 unmodified lines
690683 operation: "local.update",
691684 });
692685 if (stored.providerId !== LOCAL_SECRET_PROVIDER_ID) {
693− return yield* Effect.fail(
694− new Error(
686+ return yield* runtimeEffectError("local/secret-material-providers",
695687 `local.update: secret ${stored.id} is stored in provider '${stored.providerId}', not '${LOCAL_SECRET_PROVIDER_ID}'`,
696− ),
697− );
688+ );
698689 }
699690
700691 if (name === undefined && value === undefined) {
@@ -709,7 +700,7 @@ const createLocalSecretMaterialProvider = (): SecretMaterialProvider => ({ · 8 unmodified lines
709700 },
710701 );
711702 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}`);
713704 }
714705
715706 return {
@@ -797,9 +788,7 @@ const createKeychainSecretMaterialProvider = (): SecretMaterialProvider => ({ · 81 unmodified lines
797788 },
798789 );
799790 if (Option.isNone(updated)) {
800− return yield* Effect.fail(
801− new Error(`keychain.update: secret material not found: ${loaded.material.id}`),
802− );
791+ return yield* runtimeEffectError("local/secret-material-providers", `keychain.update: secret material not found: ${loaded.material.id}`);
803792 }
804793
805794 return {
@@ -849,7 +838,7 @@ const getSecretMaterialProvider = (input: { · 43 unmodified lines
849838 return Effect.succeed(provider);
850839 }
851840
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}`));
853842 };
854843
855844 const createSecretMaterialProviderRuntime = (input: {
@@ -946,25 +935,25 @@ const resolveConfiguredSecretProvider = (input: { · 90 unmodified lines
946935 }): Effect.Effect<string, Error, never> => {
947936 const providerAlias = fromConfigSecretProviderId(input.ref.providerId);
948937 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}`));
950939 }
951940
952941 const provider = input.runtime.localConfig?.secrets?.providers?.[providerAlias];
953942 if (!provider) {
954943 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`),
956945 );
957946 }
958947 if (input.runtime.workspaceRoot === null) {
959948 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`),
961950 );
962951 }
963952
964953 if (provider.source === "env") {
965954 const value = ensureNonEmptyString(input.runtime.env[input.ref.handle]);
966955 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`))
968957 : Effect.succeed(value);
969958 }
970959
@@ -979,17 +968,17 @@ const resolveConfiguredSecretProvider = (input: { · 8 unmodified lines
979968 const command = provider.command.trim();
980969 if (!isAbsolute(command)) {
981970 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}`),
983972 );
984973 }
985974 if (!isRegularFilePath(command, provider.allowSymlinkCommand ?? false)) {
986975 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}`),
988977 );
989978 }
990979 if (!ensureTrustedDir(command, provider.trustedDirs)) {
991980 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}`),
993982 );
994983 }
995984
@@ -1032,7 +1021,7 @@ export const createDefaultSecretMaterialResolver = (input: { · 36 unmodified lines
10321021 Effect.catchAll(() =>
10331022 fromConfigSecretProviderId(ref.providerId) !== null
10341023 ? 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}`)),
10361025 ),
10371026 );
10381027
@@ -1072,9 +1061,7 @@ export const createDefaultSecretMaterialStorer = (input: { · 33 unmodified lines
10721061 });
10731062
10741063 if (!provider.store) {
1075− return yield* Effect.fail(
1076− new Error(`Secret provider ${defaultStoreProviderId} does not support storing secret material`),
1077− );
1064+ return yield* runtimeEffectError("local/secret-material-providers", `Secret provider ${defaultStoreProviderId} does not support storing secret material`);
10781065 }
10791066
10801067 return yield* provider.store({
@@ -1102,9 +1089,7 @@ export const createDefaultSecretMaterialUpdater = (input: { · 21 unmodified lines
11021089 });
11031090
11041091 if (!provider.update) {
1105− return yield* Effect.fail(
1106− new Error(`Secret provider ${ref.providerId} does not support updating secret material`),
1107− );
1092+ return yield* runtimeEffectError("local/secret-material-providers", `Secret provider ${ref.providerId} does not support updating secret material`);
11081093 }
11091094
11101095 return yield* provider.update({
packages/platform/control-plane/src/runtime/local/tools.ts+9−17
@@ -136,25 +136,21 @@ const toToolPath = ( · 135 unmodified lines
136136 rawSegments.at(-1) === "index" ? rawSegments.slice(0, -1) : rawSegments;
137137
138138 if (segments.length === 0) {
139− return yield* Effect.fail(
140− new LocalToolDefinitionError({
139+ return yield* new LocalToolDefinitionError({
141140 message: `Invalid local tool path ${relativePath}: root index files are not supported`,
142141 path: relativePath,
143142 details:
144143 "Root index files are not supported. Use a named file such as hello.ts or a nested index.ts.",
145− }),
146− );
144+ });
147145 }
148146
149147 for (const segment of segments) {
150148 if (!LOCAL_TOOL_SEGMENT_PATTERN.test(segment)) {
151− return yield* Effect.fail(
152− new LocalToolDefinitionError({
149+ return yield* new LocalToolDefinitionError({
153150 message: `Invalid local tool path ${relativePath}: segment ${segment} contains unsupported characters`,
154151 path: relativePath,
155152 details: `Tool path segments may only contain letters, numbers, underscores, and hyphens. Invalid segment: ${segment}`,
156− }),
157− );
153+ });
158154 }
159155 }
160156
@@ -329,7 +325,7 @@ const transpileSourceFile = (input: { · 168 unmodified lines
329325 transpiled.diagnostics,
330326 );
331327 if (diagnosticError) {
332− return yield* Effect.fail(diagnosticError);
328+ return yield* diagnosticError;
333329 }
334330
335331 return rewriteRelativeImportSpecifiers(transpiled.outputText);
@@ -608,26 +604,22 @@ export const loadLocalToolRuntime = ( · 272 unmodified lines
608604 const toolPath = yield* toToolPath(relativePath);
609605 const existing = seenToolPaths.get(toolPath);
610606 if (existing) {
611− return yield* Effect.fail(
612− new LocalToolPathConflictError({
607+ return yield* new LocalToolPathConflictError({
613608 message: `Local tool path conflict for ${toolPath}`,
614609 path: sourcePath,
615610 otherPath: existing,
616611 toolPath,
617− }),
618− );
612+ });
619613 }
620614
621615 const artifactPath = artifactPaths.get(sourcePath);
622616 if (!artifactPath) {
623− return yield* Effect.fail(
624− new LocalToolImportError({
617+ return yield* new LocalToolImportError({
625618 message: `Missing compiled artifact for local tool ${sourcePath}`,
626619 path: sourcePath,
627620 details:
628621 "Expected a compiled local tool artifact, but none was produced.",
629− }),
630− );
622+ });
631623 }
632624
633625 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
248248 );
249249 const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
250250 if (policy === null) {
251− return yield* Effect.fail(
252− policyOps.get.notFound(
251+ return yield* policyOps.get.notFound(
253252 "Policy not found",
254253 `workspaceId=${input.workspaceId} policyId=${input.policyId}`,
255− ),
256− );
254+ );
257255 }
258256 return policy;
259257 });
@@ -277,12 +275,10 @@ export const updatePolicy = (input: { · 17 unmodified lines
277275 );
278276 const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
279277 if (existing === null) {
280− return yield* Effect.fail(
281− policyOps.update.notFound(
278+ return yield* policyOps.update.notFound(
282279 "Policy not found",
283280 `workspaceId=${input.workspaceId} policyId=${input.policyId}`,
284− ),
285− );
281+ );
286282 }
287283
288284 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
2929 makeLocalStorageLayer,
3030 } from "../local/storage";
3131 import { provideOptionalRuntimeLocalWorkspace } from "../local/runtime-context";
32+import { runtimeEffectError } from "../effect-errors";
3233
3334 /** Run an Effect as a Promise, preserving the original error (not FiberFailure). */
3435 const runEffect = async <A>(
@@ -251,15 +252,11 @@ const promptForSourceCredentialSelection = (input: { · 216 unmodified lines
251252 }) =>
252253 Effect.gen(function* () {
253254 if (!input.onElicitation) {
254− return yield* Effect.fail(
255− new Error("executor.sources.add requires an elicitation-capable host"),
256− );
255+ return yield* runtimeEffectError("sources/executor-tools", "executor.sources.add requires an elicitation-capable host");
257256 }
258257
259258 if (input.localServerBaseUrl === null) {
260− return yield* Effect.fail(
261− new Error("executor.sources.add requires a local server base URL for credential capture"),
262− );
259+ return yield* runtimeEffectError("sources/executor-tools", "executor.sources.add requires a local server base URL for credential capture");
263260 }
264261
265262 const response: ElicitationResponse = yield* input.onElicitation({
@@ -287,9 +284,7 @@ const promptForSourceCredentialSelection = (input: { · 21 unmodified lines
287284 }).pipe(Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause))));
288285
289286 if (response.action !== "accept") {
290− return yield* Effect.fail(
291− new Error(`Source credential setup was not completed for ${input.source.name}`),
292− );
287+ return yield* runtimeEffectError("sources/executor-tools", `Source credential setup was not completed for ${input.source.name}`);
293288 }
294289
295290 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
88 import { googleDiscoverySourceAdapter } from "./google-discovery";
99 import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result";
1010 import { createSourceFromPayload } from "../source-definitions";
11+import { runtimeEffectError } from "../../effect-errors";
1112
1213 const fetchLiveDiscoveryDocument = async (): Promise<string> => {
1314 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
9091 const syncResult = await Effect.runPromise(
9192 googleDiscoverySourceAdapter.syncCatalog({
9293 source,
93− resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")),
94+ resolveSecretMaterial: () => Effect.fail(runtimeEffectError("sources/source-adapters/google-discovery.test", "unexpected secret lookup")),
9495 resolveAuthMaterialForSlot: () =>
9596 Effect.succeed({
9697 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
77
88 import { createSourceFromPayload } from "../source-definitions";
99 import { graphqlSourceAdapter } from "./graphql";
10+import { runtimeEffectError } from "../../effect-errors";
1011
1112 describe("graphql source adapter", () => {
1213 it("fails sync when introspection never responds", async () => {
@@ -71,7 +72,7 @@ describe("graphql source adapter", () => { · 58 unmodified lines
7172 Effect.runPromise(
7273 graphqlSourceAdapter.syncCatalog({
7374 source,
74− resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")),
75+ resolveSecretMaterial: () => Effect.fail(runtimeEffectError("sources/source-adapters/graphql.test", "unexpected secret lookup")),
7576 resolveAuthMaterialForSlot: () =>
7677 Effect.succeed({
7778 placements: [],
packages/platform/control-plane/src/runtime/sources/source-adapters/internal.ts+3−4
@@ -10,6 +10,7 @@ import { · 9 unmodified lines
1010 type Source,
1111 type SourceAdapter,
1212 } from "@executor/source-core";
13+import { runtimeEffectError } from "../../effect-errors";
1314
1415 const InternalBindingConfigSchema = Schema.Struct({});
1516
@@ -37,9 +38,7 @@ const internalBindingConfigFromSource = ( · 21 unmodified lines
3738 "headers",
3839 ])
3940 ) {
40− return yield* Effect.fail(
41− new Error("internal sources cannot define HTTP source settings"),
42− );
41+ return yield* runtimeEffectError("sources/source-adapters/internal", "internal sources cannot define HTTP source settings");
4342 }
4443
4544 return yield* decodeSourceBindingPayload({
@@ -123,6 +122,6 @@ export const internalSourceAdapter: SourceAdapter = { · 77 unmodified lines
123122 ),
124123 invoke: () =>
125124 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"),
127126 ),
128127 };
packages/platform/control-plane/src/runtime/sources/source-adapters/mcp.test.ts+4−3
@@ -30,6 +30,7 @@ import { · 29 unmodified lines
3030 import { snapshotFromSourceCatalogSyncResult } from "../catalog-sync-result";
3131 import { createSourceFromPayload } from "../source-definitions";
3232 import { mcpSourceAdapter } from "./mcp";
33+import { runtimeEffectError } from "../../effect-errors";
3334
3435 type RealMcpServer = {
3536 endpoint: string;
@@ -390,7 +391,7 @@ describe("mcp source adapter", () => { · 354 unmodified lines
390391 const syncResult = yield* mcpSourceAdapter.syncCatalog({
391392 source,
392393 resolveSecretMaterial: () =>
393− Effect.fail(new Error("unexpected secret lookup")),
394+ Effect.fail(runtimeEffectError("sources/source-adapters/mcp.test", "unexpected secret lookup")),
394395 resolveAuthMaterialForSlot: () =>
395396 Effect.succeed({
396397 placements: [],
@@ -478,7 +479,7 @@ describe("mcp source adapter", () => { · 81 unmodified lines
478479 const syncResult = yield* mcpSourceAdapter.syncCatalog({
479480 source,
480481 resolveSecretMaterial: () =>
481− Effect.fail(new Error("unexpected secret lookup")),
482+ Effect.fail(runtimeEffectError("sources/source-adapters/mcp.test", "unexpected secret lookup")),
482483 resolveAuthMaterialForSlot: () =>
483484 Effect.succeed({
484485 placements: [],
@@ -576,7 +577,7 @@ describe("mcp source adapter", () => { · 91 unmodified lines
576577 const syncResult = yield* mcpSourceAdapter.syncCatalog({
577578 source,
578579 resolveSecretMaterial: () =>
579− Effect.fail(new Error("unexpected secret lookup")),
580+ Effect.fail(runtimeEffectError("sources/source-adapters/mcp.test", "unexpected secret lookup")),
580581 resolveAuthMaterialForSlot: () => Effect.succeed(resolvedAuth),
581582 });
582583 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
9999 } from "./source-store";
100100 import type { WorkspaceStorageServices } from "../local/storage";
101101 import { ControlPlaneStore, type ControlPlaneStoreShape } from "../store";
102+import { runtimeEffectError } from "../effect-errors";
102103
103104 const trimOrNull = (value: string | null | undefined): string | null => {
104105 if (value === null || value === undefined) {
@@ -214,7 +215,7 @@ const probeMcpSourceWithoutAuth = ( · 109 unmodified lines
214215 ) =>
215216 Effect.gen(function* () {
216217 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}`);
218219 }
219220 const bindingState = yield* sourceBindingStateFromSource(source);
220221
@@ -739,9 +740,7 @@ const materializeExecutorHttpAuth = (input: { · 518 unmodified lines
739740 storeSecretMaterial: input.storeSecretMaterial,
740741 });
741742 if (tokenRef === null) {
742− return yield* Effect.fail(
743− new Error("Bearer auth requires token or tokenRef"),
744− );
743+ return yield* runtimeEffectError("sources/source-auth-service", "Bearer auth requires token or tokenRef");
745744 }
746745
747746 return {
@@ -770,9 +769,7 @@ const materializeExecutorHttpAuth = (input: { · 22 unmodified lines
770769 storeSecretMaterial: input.storeSecretMaterial,
771770 });
772771 if (accessTokenRef === null) {
773− return yield* Effect.fail(
774− new Error("OAuth2 auth requires accessToken or accessTokenRef"),
775− );
772+ return yield* runtimeEffectError("sources/source-auth-service", "OAuth2 auth requires accessToken or accessTokenRef");
776773 }
777774
778775 const refreshTokenRef = yield* materializeSecretRefInput({
@@ -908,9 +905,7 @@ const upsertSourceOauthClient = (input: { · 129 unmodified lines
908905 })
909906 : null;
910907 if (setupConfig === null) {
911− return yield* Effect.fail(
912− new Error(`Source ${input.source.id} does not support OAuth client configuration`),
913− );
908+ return yield* runtimeEffectError("sources/source-auth-service", `Source ${input.source.id} does not support OAuth client configuration`);
914909 }
915910
916911 const existing = yield* input.rows.sourceOauthClients.getByWorkspaceSourceAndProvider({
@@ -1075,9 +1070,7 @@ const resolveWorkspaceOauthClientById = (input: { · 158 unmodified lines
10751070 existing.value.workspaceId !== input.workspaceId
10761071 || existing.value.providerKey !== input.providerKey
10771072 ) {
1078− return yield* Effect.fail(
1079− new Error(`Workspace OAuth client ${input.oauthClientId} is not valid for ${input.providerKey}`),
1080− );
1073+ return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client ${input.oauthClientId} is not valid for ${input.providerKey}`);
10811074 }
10821075
10831076 return {
@@ -1152,9 +1145,7 @@ const upsertProviderAuthGrant = (input: { · 68 unmodified lines
11521145 }
11531146
11541147 if (refreshTokenRef === null) {
1155− return yield* Effect.fail(
1156− new Error(`Provider auth grant for ${input.providerKey} is missing a refresh token`),
1157− );
1148+ return yield* runtimeEffectError("sources/source-auth-service", `Provider auth grant for ${input.providerKey} is missing a refresh token`);
11581149 }
11591150
11601151 const now = Date.now();
@@ -1340,7 +1331,7 @@ const startProviderOauthBatchCredentialSetup = (input: { · 179 unmodified lines
13401331 }, Error, WorkspaceStorageServices> =>
13411332 Effect.gen(function* () {
13421333 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");
13441335 }
13451336
13461337 const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`);
@@ -1580,7 +1571,7 @@ const connectMcpSourceInternal = (input: { · 233 unmodified lines
15801571 Effect.flatMap((source) =>
15811572 sourceAdapterRequiresInteractiveConnect(source.kind)
15821573 ? 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}`)),
15841575 ),
15851576 )
15861577 : input.sourceStore.loadSourcesInWorkspace(input.workspaceId, {
@@ -1712,9 +1703,7 @@ const connectMcpSourceInternal = (input: { · 125 unmodified lines
17121703
17131704 const localServerBaseUrl = trimOrNull(input.baseUrl) ?? input.getLocalServerBaseUrl?.() ?? null;
17141705 if (!localServerBaseUrl) {
1715− return yield* Effect.fail(
1716− new Error("Local executor server base URL is unavailable for source credential setup"),
1717− );
1706+ return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for source credential setup");
17181707 }
17191708
17201709 const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`);
@@ -2145,9 +2134,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 424 unmodified lines
21452134 }
21462135
21472136 if (workspaceOauthClient === null) {
2148− return yield* Effect.fail(
2149− new Error("Google shared auth requires a workspace OAuth client"),
2150− );
2137+ return yield* runtimeEffectError("sources/source-auth-service", "Google shared auth requires a workspace OAuth client");
21512138 }
21522139
21532140 const requiredScopes = normalizeScopes(googleSetupConfig.scopes);
@@ -2184,9 +2171,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 30 unmodified lines
21842171 const requestBaseUrl = trimOrNull(input.baseUrl);
21852172 const baseUrl = requestBaseUrl ?? input.getLocalServerBaseUrl?.() ?? null;
21862173 if (baseUrl === null) {
2187− return yield* Effect.fail(
2188− new Error("Local executor server base URL is unavailable for Google OAuth setup"),
2189− );
2174+ return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for Google OAuth setup");
21902175 }
21912176
21922177 const oauthRequired = yield* startProviderOauthBatchCredentialSetup({
@@ -2318,7 +2303,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 125 unmodified lines
23182303 }): Effect.Effect<ConnectGoogleDiscoveryBatchResult, Error, WorkspaceStorageServices> =>
23192304 Effect.gen(function* () {
23202305 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");
23222307 }
23232308
23242309 const existingSources = yield* input.sourceStore.loadSourcesInWorkspace(
@@ -2426,9 +2411,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 101 unmodified lines
24262411 })
24272412 : null;
24282413 if (setupConfig === null) {
2429− return yield* Effect.fail(
2430− new Error(`Source ${persistedDraft.id} does not support Google shared auth`),
2431− );
2414+ return yield* runtimeEffectError("sources/source-auth-service", `Source ${persistedDraft.id} does not support Google shared auth`);
24322415 }
24332416
24342417 return {
@@ -2451,9 +2434,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 16 unmodified lines
24512434 providerKey: baseSetupConfig.providerKey,
24522435 });
24532436 if (workspaceOauthClient === null) {
2454− return yield* Effect.fail(
2455− new Error(`Workspace OAuth client not found: ${input.sourceInput.workspaceOauthClientId}`),
2456− );
2437+ return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client not found: ${input.sourceInput.workspaceOauthClientId}`);
24572438 }
24582439
24592440 const reusableGrant = yield* findReusableProviderGrant({
@@ -2491,9 +2472,7 @@ const connectGoogleDiscoveryBatchInternal = (input: { · 31 unmodified lines
24912472
24922473 const baseUrl = trimOrNull(input.sourceInput.baseUrl) ?? input.getLocalServerBaseUrl?.() ?? null;
24932474 if (baseUrl === null) {
2494− return yield* Effect.fail(
2495− new Error("Local executor server base URL is unavailable for Google OAuth setup"),
2496− );
2475+ return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for Google OAuth setup");
24972476 }
24982477
24992478 const oauthRequired = yield* startProviderOauthBatchCredentialSetup({
@@ -2706,7 +2685,7 @@ const createRuntimeSourceConnectionService = ( · 206 unmodified lines
27062685 baseUrl: options?.baseUrl,
27072686 resolveSecretMaterial: input.resolveSecretMaterial,
27082687 })
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(
27102689 Effect.flatMap(mirrorLocalSourceResult),
27112690 ),
27122691 ),
@@ -2772,9 +2751,7 @@ const createRuntimeSourceConnectionService = ( · 59 unmodified lines
27722751 });
27732752 const stored = yield* input.rows.workspaceOauthClients.getById(created.id);
27742753 if (Option.isNone(stored)) {
2775− return yield* Effect.fail(
2776− new Error(`Workspace OAuth client ${created.id} was not persisted`),
2777− );
2754+ return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client ${created.id} was not persisted`);
27782755 }
27792756
27802757 return stored.value;
@@ -2792,11 +2769,9 @@ const createRuntimeSourceConnectionService = ( · 11 unmodified lines
27922769 const grants = yield* input.rows.providerAuthGrants.listByWorkspaceId(workspaceId);
27932770 const dependentGrant = grants.find((grant) => grant.oauthClientId === oauthClientId);
27942771 if (dependentGrant) {
2795− return yield* Effect.fail(
2796− new Error(
2772+ return yield* runtimeEffectError("sources/source-auth-service",
27972773 `Workspace OAuth client ${oauthClientId} is still referenced by provider grant ${dependentGrant.id}`,
2798− ),
2799− );
2774+ );
28002775 }
28012776
28022777 const secretRef = sourceOauthClientSecretRef(oauthClient.value);
@@ -2835,9 +2810,7 @@ const createRuntimeSourceOAuthSessionService = ( · 32 unmodified lines
28352810 Effect.gen(function* () {
28362811 const resolvedBaseUrl = trimOrNull(oauthInput.baseUrl) ?? input.getLocalServerBaseUrl?.() ?? null;
28372812 if (!resolvedBaseUrl) {
2838− return yield* Effect.fail(
2839− new Error("Local executor server base URL is unavailable for OAuth setup"),
2840− );
2813+ return yield* runtimeEffectError("sources/source-auth-service", "Local executor server base URL is unavailable for OAuth setup");
28412814 }
28422815
28432816 const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`);
@@ -2895,17 +2868,17 @@ const createRuntimeSourceOAuthSessionService = ( · 51 unmodified lines
28952868 provideLocalWorkspace(Effect.gen(function* () {
28962869 const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state);
28972870 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}`);
28992872 }
29002873
29012874 const session = sessionOption.value;
29022875 const sessionData = decodeMcpSourceAuthSessionData(session);
29032876 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`);
29052878 }
29062879
29072880 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`);
29092882 }
29102883
29112884 if (trimOrNull(error) !== null) {
@@ -2922,20 +2895,20 @@ const createRuntimeSourceOAuthSessionService = ( · 10 unmodified lines
29222895 }),
29232896 );
29242897
2925− return yield* Effect.fail(new Error(reason));
2898+ return yield* runtimeEffectError("sources/source-auth-service", reason);
29262899 }
29272900
29282901 const authorizationCode = trimOrNull(code);
29292902 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");
29312904 }
29322905
29332906 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");
29352908 }
29362909
29372910 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}`);
29392912 }
29402913
29412914 const exchanged = yield* exchangeMcpOAuthAuthorizationCode({
@@ -3007,24 +2980,18 @@ const createRuntimeSourceOAuthSessionService = ( · 65 unmodified lines
30072980 provideLocalWorkspace(Effect.gen(function* () {
30082981 const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state);
30092982 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}`);
30112984 }
30122985
30132986 const session = sessionOption.value;
30142987 if (session.workspaceId !== workspaceId) {
3015− return yield* Effect.fail(
3016− new Error(`Source auth session ${session.id} does not match workspaceId=${workspaceId}`),
3017− );
2988+ return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} does not match workspaceId=${workspaceId}`);
30182989 }
30192990 if ((session.actorAccountId ?? null) !== (actorAccountId ?? null)) {
3020− return yield* Effect.fail(
3021− new Error(`Source auth session ${session.id} does not match the active account`),
3022− );
2991+ return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} does not match the active account`);
30232992 }
30242993 if (session.providerKind !== "oauth2_provider_batch") {
3025− return yield* Effect.fail(
3026− new Error(`Source auth session ${session.id} is not a provider batch OAuth session`),
3027− );
2994+ return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is not a provider batch OAuth session`);
30282995 }
30292996 if (session.status === "completed") {
30302997 const sessionData = decodeProviderOauthBatchSourceAuthSessionData(session);
@@ -3044,9 +3011,7 @@ const createRuntimeSourceOAuthSessionService = ( · 13 unmodified lines
30443011 } satisfies CompleteProviderOauthCallbackResult;
30453012 }
30463013 if (session.status !== "pending") {
3047− return yield* Effect.fail(
3048− new Error(`Source auth session ${session.id} is not pending`),
3049− );
3014+ return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is not pending`);
30503015 }
30513016
30523017 const sessionData = decodeProviderOauthBatchSourceAuthSessionData(session);
@@ -3061,15 +3026,15 @@ const createRuntimeSourceOAuthSessionService = ( · 8 unmodified lines
30613026 errorText: reason,
30623027 }),
30633028 );
3064− return yield* Effect.fail(new Error(reason));
3029+ return yield* runtimeEffectError("sources/source-auth-service", reason);
30653030 }
30663031
30673032 const authorizationCode = trimOrNull(code);
30683033 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");
30703035 }
30713036 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");
30733038 }
30743039
30753040 const oauthClient = yield* resolveWorkspaceOauthClientById({
@@ -3079,9 +3044,7 @@ const createRuntimeSourceOAuthSessionService = ( · 3 unmodified lines
30793044 providerKey: sessionData.providerKey,
30803045 });
30813046 if (oauthClient === null) {
3082− return yield* Effect.fail(
3083− new Error(`Workspace OAuth client not found: ${sessionData.oauthClientId}`),
3084− );
3047+ return yield* runtimeEffectError("sources/source-auth-service", `Workspace OAuth client not found: ${sessionData.oauthClientId}`);
30853048 }
30863049
30873050 let clientSecret: string | null = null;
@@ -3185,24 +3148,20 @@ const createRuntimeSourceOAuthSessionService = ( · 97 unmodified lines
31853148 provideLocalWorkspace(Effect.gen(function* () {
31863149 const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state);
31873150 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}`);
31893152 }
31903153
31913154 const session = sessionOption.value;
31923155 if (session.workspaceId !== workspaceId || session.sourceId !== sourceId) {
3193− return yield* Effect.fail(
3194− new Error(
3156+ return yield* runtimeEffectError("sources/source-auth-service",
31953157 `Source auth session ${session.id} does not match workspaceId=${workspaceId} sourceId=${sourceId}`,
3196− ),
3197− );
3158+ );
31983159 }
31993160 if (
32003161 actorAccountId !== undefined
32013162 && (session.actorAccountId ?? null) !== (actorAccountId ?? null)
32023163 ) {
3203− return yield* Effect.fail(
3204− new Error(`Source auth session ${session.id} does not match the active account`),
3205− );
3164+ return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} does not match the active account`);
32063165 }
32073166
32083167 const source = yield* input.sourceStore.loadSourceById({
@@ -3219,9 +3178,7 @@ const createRuntimeSourceOAuthSessionService = ( · 10 unmodified lines
32193178 }
32203179
32213180 if (session.status !== "pending") {
3222− return yield* Effect.fail(
3223− new Error(`Source auth session ${session.id} is not pending`),
3224− );
3181+ return yield* runtimeEffectError("sources/source-auth-service", `Source auth session ${session.id} is not pending`);
32253182 }
32263183
32273184 const sessionData = session.providerKind === "oauth2_pkce"
@@ -3260,16 +3217,16 @@ const createRuntimeSourceOAuthSessionService = ( · 32 unmodified lines
32603217 },
32613218 });
32623219
3263− return yield* Effect.fail(new Error(reason));
3220+ return yield* runtimeEffectError("sources/source-auth-service", reason);
32643221 }
32653222
32663223 const authorizationCode = trimOrNull(code);
32673224 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");
32693226 }
32703227
32713228 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");
32733230 }
32743231
32753232 const now = Date.now();
@@ -3294,9 +3251,7 @@ const createRuntimeSourceOAuthSessionService = ( · 18 unmodified lines
32943251 });
32953252 const refreshToken = trimOrNull(exchanged.refresh_token);
32963253 if (refreshToken === null) {
3297− return yield* Effect.fail(
3298− new Error("OAuth authorization did not return a refresh token"),
3299− );
3254+ return yield* runtimeEffectError("sources/source-auth-service", "OAuth authorization did not return a refresh token");
33003255 }
33013256
33023257 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
3232 authArtifactFromSourceAuth,
3333 sourceAuthFromAuthArtifact,
3434 } from "../auth/auth-artifacts";
35+import { runtimeEffectError } from "../effect-errors";
3536
3637 const trimOrNull = (value: string | null | undefined): string | null => {
3738 if (value === null || value === undefined) {
@@ -96,7 +97,7 @@ const normalizeAuth = ( · 58 unmodified lines
9697 const providerId = trimOrNull(auth.token.providerId);
9798 const handle = trimOrNull(auth.token.handle);
9899 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");
100101 }
101102
102103 return {
@@ -116,9 +117,7 @@ const normalizeAuth = ( · 13 unmodified lines
116117 const refreshProviderId = trimOrNull(auth.refreshToken.providerId);
117118 const refreshHandle = trimOrNull(auth.refreshToken.handle);
118119 if (refreshProviderId === null || refreshHandle === null) {
119− return yield* Effect.fail(
120− new Error("OAuth2 authorized-user auth requires a refresh token secret ref"),
121− );
120+ return yield* runtimeEffectError("sources/source-definitions", "OAuth2 authorized-user auth requires a refresh token secret ref");
122121 }
123122
124123 let clientSecret: { providerId: string; handle: string } | null = null;
@@ -126,9 +125,7 @@ const normalizeAuth = ( · 1 unmodified line
126125 const clientSecretProviderId = trimOrNull(auth.clientSecret.providerId);
127126 const clientSecretHandle = trimOrNull(auth.clientSecret.handle);
128127 if (clientSecretProviderId === null || clientSecretHandle === null) {
129− return yield* Effect.fail(
130− new Error("OAuth2 authorized-user client secret ref must include providerId and handle"),
131− );
128+ return yield* runtimeEffectError("sources/source-definitions", "OAuth2 authorized-user client secret ref must include providerId and handle");
132129 }
133130 clientSecret = {
134131 providerId: clientSecretProviderId,
@@ -139,9 +136,7 @@ const normalizeAuth = ( · 4 unmodified lines
139136 const tokenEndpoint = trimOrNull(auth.tokenEndpoint);
140137 const clientId = trimOrNull(auth.clientId);
141138 if (tokenEndpoint === null || clientId === null) {
142− return yield* Effect.fail(
143− new Error("OAuth2 authorized-user auth requires tokenEndpoint and clientId"),
144− );
139+ return yield* runtimeEffectError("sources/source-definitions", "OAuth2 authorized-user auth requires tokenEndpoint and clientId");
145140 }
146141
147142 return {
@@ -165,9 +160,7 @@ const normalizeAuth = ( · 17 unmodified lines
165160 const prefix = auth.prefix ?? "Bearer ";
166161 const grantId = trimOrNull(auth.grantId);
167162 if (grantId === null) {
168− return yield* Effect.fail(
169− new Error("Provider grant auth requires a grantId"),
170− );
163+ return yield* runtimeEffectError("sources/source-definitions", "Provider grant auth requires a grantId");
171164 }
172165
173166 return {
@@ -187,9 +180,7 @@ const normalizeAuth = ( · 13 unmodified lines
187180 const accessProviderId = trimOrNull(auth.accessToken.providerId);
188181 const accessHandle = trimOrNull(auth.accessToken.handle);
189182 if (redirectUri === null || accessProviderId === null || accessHandle === null) {
190− return yield* Effect.fail(
191− new Error("MCP OAuth auth requires redirectUri and access token secret ref"),
192− );
183+ return yield* runtimeEffectError("sources/source-definitions", "MCP OAuth auth requires redirectUri and access token secret ref");
193184 }
194185
195186 let refreshToken: { providerId: string; handle: string } | null = null;
@@ -197,9 +188,7 @@ const normalizeAuth = ( · 1 unmodified line
197188 const refreshProviderId = trimOrNull(auth.refreshToken.providerId);
198189 const refreshHandle = trimOrNull(auth.refreshToken.handle);
199190 if (refreshProviderId === null || refreshHandle === null) {
200− return yield* Effect.fail(
201− new Error("MCP OAuth refresh token ref must include providerId and handle"),
202− );
191+ return yield* runtimeEffectError("sources/source-definitions", "MCP OAuth refresh token ref must include providerId and handle");
203192 }
204193
205194 refreshToken = {
@@ -234,7 +223,7 @@ const normalizeAuth = ( · 28 unmodified lines
234223 const accessProviderId = trimOrNull(auth.accessToken.providerId);
235224 const accessHandle = trimOrNull(auth.accessToken.handle);
236225 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");
238227 }
239228
240229 let refreshToken: { providerId: string; handle: string } | null = null;
@@ -242,9 +231,7 @@ const normalizeAuth = ( · 1 unmodified line
242231 const refreshProviderId = trimOrNull(auth.refreshToken.providerId);
243232 const refreshHandle = trimOrNull(auth.refreshToken.handle);
244233 if (refreshProviderId === null || refreshHandle === null) {
245− return yield* Effect.fail(
246− new Error("OAuth2 refresh token ref must include providerId and handle"),
247− );
234+ return yield* runtimeEffectError("sources/source-definitions", "OAuth2 refresh token ref must include providerId and handle");
248235 }
249236
250237 refreshToken = {
@@ -273,9 +260,7 @@ const normalizeImportAuthPolicy = ( · 22 unmodified lines
273260 const validateSourceImportAuth = (source: Source): Effect.Effect<Source, Error, never> =>
274261 Effect.gen(function* () {
275262 if (source.importAuthPolicy !== "separate" && source.importAuth.kind !== "none") {
276− return yield* Effect.fail(
277− new Error("importAuth must be none unless importAuthPolicy is separate"),
278− );
263+ return yield* runtimeEffectError("sources/source-definitions", "importAuth must be none unless importAuthPolicy is separate");
279264 }
280265
281266 return source;
packages/platform/control-plane/src/runtime/sources/source-inspection.ts+3−5
@@ -57,7 +57,7 @@ const loadSourceForMissingCatalog = (input: { · 56 unmodified lines
5757 });
5858
5959 if (!canInspectSourceWithoutCatalog(source)) {
60− return yield* Effect.fail(input.cause);
60+ return yield* input.cause;
6161 }
6262
6363 return source;
@@ -414,12 +414,10 @@ export const getSourceInspectionToolDetail = (input: { · 350 unmodified lines
414414 const tool = inspection.tool;
415415
416416 if (!tool) {
417− return yield* Effect.fail(
418− sourceInspectOps.tool.notFound(
417+ return yield* sourceInspectOps.tool.notFound(
419418 "Tool not found",
420419 `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`,
421− ),
422− );
420+ );
423421 }
424422
425423 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
6666 > =>
6767 Effect.gen(function* () {
6868 if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
69− return yield* Effect.fail(
70− new RuntimeLocalWorkspaceMismatchError({
69+ return yield* new RuntimeLocalWorkspaceMismatchError({
7170 message: `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
7271 requestedWorkspaceId: workspaceId,
7372 activeWorkspaceId: deps.runtimeLocalWorkspace.installation.workspaceId,
74− }),
75− );
73+ });
7674 }
7775
7876 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
5252 Effect.gen(function* () {
5353 const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId];
5454 if (!sourceConfig) {
55− return yield* Effect.fail(
56− new LocalConfiguredSourceNotFoundError({
55+ return yield* new LocalConfiguredSourceNotFoundError({
5756 message: `Configured source not found for id ${input.sourceId}`,
5857 sourceId: input.sourceId,
59− }),
60− );
58+ });
6159 }
6260
6361 const existingState = input.workspaceState.sources[input.sourceId];
@@ -313,12 +311,10 @@ export const loadSourceByIdWithDeps = ( · 249 unmodified lines
313311 input.workspaceId,
314312 );
315313 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
316− return yield* Effect.fail(
317− new LocalConfiguredSourceNotFoundError({
314+ return yield* new LocalConfiguredSourceNotFoundError({
318315 message: `Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
319316 sourceId: input.sourceId,
320− }),
321− );
317+ });
322318 }
323319
324320 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
121121 );
122122 }
123123
124− return yield* Effect.fail(
125− input.operation.unknownStorage(error, "Failed syncing source tools"),
126− );
124+ return yield* input.operation.unknownStorage(error, "Failed syncing source tools");
127125 }),
128126 });
129127 });
packages/platform/server/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1+import * as Data from "effect/Data";
2+
3+export class PlatformServerEffectError extends Data.TaggedError(
4+ "PlatformServerEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const platformServerEffectError = (
11+ module: string,
12+ message: string,
13+) => new PlatformServerEffectError({ module, message });
packages/platform/server/src/index.ts+2−1
@@ -36,6 +36,7 @@ import { · 35 unmodified lines
3636 createLocalTracingRuntimeFromEnv,
3737 tracingSearchUrl,
3838 } from "./tracing";
39+import { platformServerEffectError } from "./effect-errors";
3940
4041 export {
4142 DEFAULT_EXECUTOR_DATA_DIR,
@@ -403,7 +404,7 @@ export const createLocalExecutorServer = ( · 361 unmodified lines
403404
404405 const address = server.address();
405406 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");
407408 }
408409
409410 const resolvedAddress = address as AddressInfo;
packages/sources/core/src/effect-errors.ts+13−0
@@ -0,0 +1,13 @@
1+import * as Data from "effect/Data";
2+
3+export class SourceCoreEffectError extends Data.TaggedError(
4+ "SourceCoreEffectError",
5+)<{
6+ readonly module: string;
7+ readonly message: string;
8+}> {}
9+
10+export const sourceCoreEffectError = (
11+ module: string,
12+ message: string,
13+) => new SourceCoreEffectError({ module, message });
packages/sources/core/src/index.ts+1−0
@@ -2,6 +2,7 @@ export * from "./catalog"; · 1 unmodified line
22 export * from "./catalog-sync-result";
33 export * from "./discovery";
44 export * from "./discovery-models";
5+export * from "./effect-errors";
56 export * from "./registry";
67 export * from "./shared";
78 export * from "./source-models";
packages/sources/core/src/shared.ts+13−8
@@ -1,6 +1,8 @@
1+import * as Data from "effect/Data";
12 import * as Effect from "effect/Effect";
23 import * as Schema from "effect/Schema";
34
5+import { sourceCoreEffectError } from "./effect-errors";
46 import {
57 SecretRefSchema,
68 SourceBindingVersionSchema,
@@ -86,14 +88,17 @@ export const parseJsonValue = <T>(input: { · 79 unmodified lines
8688 : new Error(`Invalid ${input.label}: ${String(cause)}`),
8789 });
8890
89−export class SourceCredentialRequiredError extends Error {
90− readonly _tag = "SourceCredentialRequiredError";
91−
91+export class SourceCredentialRequiredError extends Data.TaggedError(
92+ "SourceCredentialRequiredError",
93+)<{
94+ readonly slot: CredentialSlot;
95+ readonly message: string;
96+}> {
9297 constructor(
93− readonly slot: CredentialSlot,
98+ slot: CredentialSlot,
9499 message: string,
95100 ) {
96− super(message);
101+ super({ slot, message });
97102 }
98103 }
99104
@@ -150,7 +155,7 @@ export const decodeBindingConfig = <A>(input: { · 50 unmodified lines
150155 }): Effect.Effect<SourceBinding & { payload: A }, Error, never> => {
151156 if (input.value === null) {
152157 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}`),
154159 );
155160 }
156161
@@ -173,7 +178,7 @@ export const decodeBindingConfig = <A>(input: { · 16 unmodified lines
173178 payload: decoded.payload,
174179 } as SourceBinding & { payload: A })
175180 : Effect.fail(
176− new Error(
181+ sourceCoreEffectError("core/shared",
177182 `Unsupported ${input.label} binding config version ${decoded.version} for ${input.sourceId}; expected ${input.version}`,
178183 ),
179184 ),
@@ -192,7 +197,7 @@ export const decodeSourceBindingPayload = <A>(input: { · 12 unmodified lines
192197 }): Effect.Effect<A, Error, never> =>
193198 input.version !== input.expectedVersion
194199 ? Effect.fail(
195− new Error(
200+ sourceCoreEffectError("core/shared",
196201 `Unsupported ${input.label} binding version ${input.version} for ${input.sourceId}; expected ${input.expectedVersion}`,
197202 ),
198203 )
packages/sources/google-discovery/src/adapter.ts+8−15
@@ -39,6 +39,7 @@ import { · 38 unmodified lines
3939 WorkspaceOauthClientIdSchema,
4040 createCatalogImportMetadata,
4141 EXECUTABLE_BINDING_VERSION,
42+ sourceCoreEffectError,
4243 type Source,
4344 type SourceAdapter,
4445 } from "@executor/source-core";
@@ -116,11 +117,9 @@ const googleDiscoveryBindingConfigFromSource = ( · 71 unmodified lines
116117 "specUrl",
117118 ])
118119 ) {
119− return yield* Effect.fail(
120− new Error(
120+ return yield* sourceCoreEffectError("google-discovery/adapter",
121121 "Google Discovery sources cannot define MCP or OpenAPI binding fields",
122− ),
123− );
122+ );
124123 }
125124
126125 const bindingConfig = yield* decodeSourceBindingPayload({
@@ -142,9 +141,7 @@ const googleDiscoveryBindingConfigFromSource = ( · 15 unmodified lines
142141 const service = bindingConfig.service.trim();
143142 const version = bindingConfig.version.trim();
144143 if (service.length === 0 || version.length === 0) {
145− return yield* Effect.fail(
146− new Error("Google Discovery sources require service and version"),
147− );
144+ return yield* sourceCoreEffectError("google-discovery/adapter", "Google Discovery sources require service and version");
148145 }
149146
150147 const explicitDiscoveryUrl =
@@ -194,19 +191,15 @@ const fetchGoogleDiscoveryDocumentWithHeaders = (input: { · 43 unmodified lines
194191 );
195192
196193 if (response.status === 401 || response.status === 403) {
197− return yield* Effect.fail(
198− new SourceCredentialRequiredError(
194+ return yield* new SourceCredentialRequiredError(
199195 "import",
200196 `Google Discovery fetch requires credentials (status ${response.status})`,
201− ),
202− );
197+ );
203198 }
204199 if (response.status < 200 || response.status >= 300) {
205− return yield* Effect.fail(
206− new Error(
200+ return yield* sourceCoreEffectError("google-discovery/adapter",
207201 `Google Discovery fetch failed with status ${response.status}`,
208− ),
209− );
202+ );
210203 }
211204
212205 return yield* response.text.pipe(
packages/sources/google-discovery/src/tools.ts+3−4
@@ -14,6 +14,7 @@ import { · 13 unmodified lines
1414 type HttpRequestPlacements,
1515 type ToolMetadata,
1616 } from "@executor/codemode-core";
17+import { sourceCoreEffectError } from "@executor/source-core";
1718 import * as Effect from "effect/Effect";
1819 import * as Layer from "effect/Layer";
1920 import * as Schema from "effect/Schema";
@@ -503,11 +504,9 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 483 unmodified lines
503504 const body = yield* decodeResponseBody(response);
504505
505506 if (response.status < 200 || response.status >= 300) {
506− return yield* Effect.fail(
507− new Error(
507+ return yield* sourceCoreEffectError("google-discovery/tools",
508508 `Google Discovery request failed with HTTP ${response.status}: ${typeof body === "string" ? body : JSON.stringify(body)}`,
509− ),
510− );
509+ );
511510 }
512511
513512 return body;
packages/sources/graphql/src/adapter.ts+3−6
@@ -33,6 +33,7 @@ import { · 32 unmodified lines
3333 StringMapSchema,
3434 createCatalogImportMetadata,
3535 EXECUTABLE_BINDING_VERSION,
36+ sourceCoreEffectError,
3637 type Source,
3738 type SourceAdapter,
3839 } from "@executor/source-core";
@@ -94,14 +95,10 @@ const graphqlBindingConfigFromSource = ( · 55 unmodified lines
9495 "headers",
9596 ])
9697 ) {
97− return yield* Effect.fail(
98− new Error("GraphQL sources cannot define MCP transport settings"),
99− );
98+ return yield* sourceCoreEffectError("graphql/adapter", "GraphQL sources cannot define MCP transport settings");
10099 }
101100 if (bindingHasAnyField(source.binding, ["specUrl"])) {
102− return yield* Effect.fail(
103− new Error("GraphQL sources cannot define specUrl"),
104− );
101+ return yield* sourceCoreEffectError("graphql/adapter", "GraphQL sources cannot define specUrl");
105102 }
106103
107104 const bindingConfig = yield* decodeSourceBindingPayload({
packages/sources/graphql/src/graphql-tools.ts+2−3
@@ -12,6 +12,7 @@ import { · 11 unmodified lines
1212 type ToolPath,
1313 typeSignatureFromSchema,
1414 } from "@executor/codemode-core";
15+import { sourceCoreEffectError } from "@executor/source-core";
1516 import {
1617 GraphqlToolProviderDataSchema,
1718 type GraphqlToolProviderData,
@@ -1612,9 +1613,7 @@ const invokeRawGraphqlTool = (input: { · 1594 unmodified lines
16121613 const record = asRecord(input.args);
16131614 const query = asString(record.query);
16141615 if (query === null) {
1615− return yield* Effect.fail(
1616− new Error(`GraphQL query must be a non-empty string for ${input.path}`),
1617− );
1616+ return yield* sourceCoreEffectError("graphql/graphql-tools", `GraphQL query must be a non-empty string for ${input.path}`);
16181617 }
16191618
16201619 return yield* invokeGraphqlHttpRequest({
packages/sources/mcp/src/adapter.ts+3−4
@@ -31,6 +31,7 @@ import { · 30 unmodified lines
3131 StringMapSchema,
3232 createCatalogImportMetadata,
3333 EXECUTABLE_BINDING_VERSION,
34+ sourceCoreEffectError,
3435 type Source,
3536 type SourceAdapter,
3637 type SourceCatalogSyncResult,
@@ -131,12 +132,10 @@ const mcpBindingConfigFromSource = ( · 94 unmodified lines
131132 ): Effect.Effect<McpBindingConfig, Error, never> =>
132133 Effect.gen(function* () {
133134 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");
135136 }
136137 if (bindingHasAnyField(source.binding, ["defaultHeaders"])) {
137− return yield* Effect.fail(
138− new Error("MCP sources cannot define HTTP source settings"),
139− );
138+ return yield* sourceCoreEffectError("mcp/adapter", "MCP sources cannot define HTTP source settings");
140139 }
141140
142141 const bindingConfig = yield* decodeSourceBindingPayload({
packages/sources/mcp/src/tools.ts+6−10
@@ -349,7 +349,7 @@ const runMcpListToolsEffect = (input: { · 348 unmodified lines
349349 }
350350
351351 if (isExecutionSuspendedCause(attempt.left)) {
352− return yield* Effect.fail(attempt.left as McpToolsError);
352+ return yield* (attempt.left as McpToolsError);
353353 }
354354
355355 if (
@@ -370,13 +370,11 @@ const runMcpListToolsEffect = (input: { · 14 unmodified lines
370370 continue;
371371 }
372372
373− return yield* Effect.fail(
374− new McpToolsError({
373+ return yield* new McpToolsError({
375374 stage: "list_tools",
376375 message: "Failed listing MCP tools",
377376 details: toDetails(attempt.left),
378− }),
379− );
377+ });
380378 }
381379 });
382380
@@ -417,7 +415,7 @@ const runMcpToolCallEffect = (input: { · 34 unmodified lines
417415 }
418416
419417 if (isExecutionSuspendedCause(attempt.left)) {
420− return yield* Effect.fail(attempt.left as McpToolsError);
418+ return yield* (attempt.left as McpToolsError);
421419 }
422420
423421 if (
@@ -438,13 +436,11 @@ const runMcpToolCallEffect = (input: { · 14 unmodified lines
438436 continue;
439437 }
440438
441− return yield* Effect.fail(
442− new McpToolsError({
439+ return yield* new McpToolsError({
443440 stage: "call_tool",
444441 message: `Failed invoking MCP tool: ${input.toolName}`,
445442 details: toDetails(attempt.left),
446− }),
447− );
443+ });
448444 }
449445 });
450446
packages/sources/openapi/src/adapter.ts+6−11
@@ -42,6 +42,7 @@ import { · 41 unmodified lines
4242 StringMapSchema,
4343 createCatalogImportMetadata,
4444 EXECUTABLE_BINDING_VERSION,
45+ sourceCoreEffectError,
4546 type Source,
4647 type SourceAdapter,
4748 } from "@executor/source-core";
@@ -109,9 +110,7 @@ const openApiBindingConfigFromSource = ( · 61 unmodified lines
109110 "headers",
110111 ])
111112 ) {
112− return yield* Effect.fail(
113− new Error("OpenAPI sources cannot define MCP transport settings"),
114− );
113+ return yield* sourceCoreEffectError("openapi/adapter", "OpenAPI sources cannot define MCP transport settings");
115114 }
116115
117116 const bindingConfig = yield* decodeSourceBindingPayload({
@@ -129,7 +128,7 @@ const openApiBindingConfigFromSource = ( · 11 unmodified lines
129128 ? bindingConfig.specUrl.trim()
130129 : "";
131130 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");
133132 }
134133
135134 return {
@@ -168,17 +167,13 @@ const fetchOpenApiDocumentWithHeaders = (input: { · 32 unmodified lines
168167 );
169168
170169 if (response.status === 401 || response.status === 403) {
171− return yield* Effect.fail(
172− new SourceCredentialRequiredError(
170+ return yield* new SourceCredentialRequiredError(
173171 "import",
174172 `OpenAPI spec fetch requires credentials (status ${response.status})`,
175− ),
176− );
173+ );
177174 }
178175 if (response.status < 200 || response.status >= 300) {
179− return yield* Effect.fail(
180− new Error(`OpenAPI spec fetch failed with status ${response.status}`),
181− );
176+ return yield* sourceCoreEffectError("openapi/adapter", `OpenAPI spec fetch failed with status ${response.status}`);
182177 }
183178
184179 return yield* response.text.pipe(
tools/oxlint/README.md+2−0
@@ -11,6 +11,8 @@ Why it lives here: · 10 unmodified lines
1111 Current rules:
1212
1313 - `executor-monorepo/no-cross-workspace-relative-imports`
14+- `executor-monorepo/no-raw-effect-fail-errors`
15+- `executor-monorepo/no-yield-effect-fail`
1416 - `executor-monorepo/no-workspace-src-imports`
1517
1618 To add another rule:
tools/oxlint/plugin.mjs+4−0
@@ -1,6 +1,8 @@
11 import { definePlugin } from "@oxlint/plugins";
22
33 import noCrossWorkspaceRelativeImports from "./rules/no-cross-workspace-relative-imports.mjs";
4+import noRawEffectFailErrors from "./rules/no-raw-effect-fail-errors.mjs";
5+import noYieldEffectFail from "./rules/no-yield-effect-fail.mjs";
46 import noWorkspaceSrcImports from "./rules/no-workspace-src-imports.mjs";
57
68 export default definePlugin({
@@ -9,6 +11,8 @@ export default definePlugin({ · 2 unmodified lines
911 },
1012 rules: {
1113 "no-cross-workspace-relative-imports": noCrossWorkspaceRelativeImports,
14+ "no-raw-effect-fail-errors": noRawEffectFailErrors,
15+ "no-yield-effect-fail": noYieldEffectFail,
1216 "no-workspace-src-imports": noWorkspaceSrcImports,
1317 },
1418 });
tools/oxlint/rules/no-raw-effect-fail-errors.mjs+109−0
@@ -0,0 +1,109 @@
1+import { defineRule } from "@oxlint/plugins";
2+
3+const BUILTIN_ERROR_NAMES = new Set([
4+ "AggregateError",
5+ "Error",
6+ "EvalError",
7+ "RangeError",
8+ "ReferenceError",
9+ "SyntaxError",
10+ "TypeError",
11+ "URIError",
12+]);
13+
14+const isIdentifierNamed = (node, name) =>
15+ node?.type === "Identifier" && node.name === name;
16+
17+const isEffectFailCall = (node) =>
18+ node?.type === "CallExpression"
19+ && node.callee?.type === "MemberExpression"
20+ && node.callee.computed === false
21+ && isIdentifierNamed(node.callee.object, "Effect")
22+ && isIdentifierNamed(node.callee.property, "fail");
23+
24+const readBuiltinErrorName = (node) => {
25+ if (
26+ node?.type === "NewExpression"
27+ && node.callee?.type === "Identifier"
28+ && BUILTIN_ERROR_NAMES.has(node.callee.name)
29+ ) {
30+ return node.callee.name;
31+ }
32+
33+ if (
34+ node?.type === "CallExpression"
35+ && node.callee?.type === "Identifier"
36+ && BUILTIN_ERROR_NAMES.has(node.callee.name)
37+ ) {
38+ return node.callee.name;
39+ }
40+
41+ return null;
42+};
43+
44+const readBuiltinFailedErrorName = (node) => {
45+ if (!isEffectFailCall(node) || node.arguments.length === 0) {
46+ return null;
47+ }
48+
49+ return readBuiltinErrorName(node.arguments[0]);
50+};
51+
52+export default defineRule({
53+ meta: {
54+ type: "problem",
55+ docs: {
56+ description:
57+ "Disallow failing Effects with builtin Error constructors instead of tagged Effect errors.",
58+ recommended: true,
59+ },
60+ messages: {
61+ noRawEffectFailErrors:
62+ "Do not pass builtin {{errorName}} to Effect.fail. Use a Data.TaggedError or Schema.TaggedError instead. If no suitable tagged error exists, define one near this module with structured fields and fail with that tagged error rather than interpolating context into a message string.",
63+ noRawYieldedEffectFailErrors:
64+ "Do not `yield* Effect.fail` with builtin {{errorName}}. In Effect.gen, prefer yielding the tagged error directly, for example `yield* new MyTaggedError({ ... })`. If no suitable Data.TaggedError or Schema.TaggedError exists, define one near this module with structured fields instead of interpolating context into a message string.",
65+ },
66+ },
67+ create(context) {
68+ return {
69+ CallExpression(node) {
70+ const errorName = readBuiltinFailedErrorName(node);
71+
72+ if (!errorName) {
73+ return;
74+ }
75+
76+ if (
77+ node.parent?.type === "YieldExpression"
78+ && node.parent.argument === node
79+ ) {
80+ return;
81+ }
82+
83+ context.report({
84+ node: node.arguments[0],
85+ messageId: "noRawEffectFailErrors",
86+ data: {
87+ errorName,
88+ },
89+ });
90+ },
91+ YieldExpression(node) {
92+ const failedCall = node.argument;
93+ const errorName = readBuiltinFailedErrorName(failedCall);
94+
95+ if (!errorName) {
96+ return;
97+ }
98+
99+ context.report({
100+ node: failedCall.arguments[0],
101+ messageId: "noRawYieldedEffectFailErrors",
102+ data: {
103+ errorName,
104+ },
105+ });
106+ },
107+ };
108+ },
109+});
tools/oxlint/rules/no-yield-effect-fail.mjs+64−0
@@ -0,0 +1,64 @@
1+import { defineRule } from "@oxlint/plugins";
2+
3+const isIdentifierNamed = (node, name) =>
4+ node?.type === "Identifier" && node.name === name;
5+
6+const isEffectFailCall = (node) =>
7+ node?.type === "CallExpression"
8+ && node.callee?.type === "MemberExpression"
9+ && node.callee.computed === false
10+ && isIdentifierNamed(node.callee.object, "Effect")
11+ && isIdentifierNamed(node.callee.property, "fail");
12+
13+const isDirectYieldCandidate = (node) => {
14+ if (!node) {
15+ return false;
16+ }
17+
18+ if (node.type === "CallExpression" || node.type === "NewExpression") {
19+ return true;
20+ }
21+
22+ if (node.type === "ParenthesizedExpression") {
23+ return isDirectYieldCandidate(node.expression);
24+ }
25+
26+ return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
27+};
28+
29+export default defineRule({
30+ meta: {
31+ type: "suggestion",
32+ docs: {
33+ description:
34+ "Disallow `yield* Effect.fail(...)` when the yielded failure value can be yielded directly in Effect.gen.",
35+ recommended: true,
36+ },
37+ messages: {
38+ noYieldEffectFail:
39+ "Avoid `yield* Effect.fail(...)` here. In Effect.gen, yield the tagged or yieldable error expression directly instead, for example `yield* new MyTaggedError({ ... })` or `yield* myTaggedError(...)`.",
40+ },
41+ },
42+ create(context) {
43+ return {
44+ YieldExpression(node) {
45+ if (!node.delegate) {
46+ return;
47+ }
48+
49+ if (!isEffectFailCall(node.argument) || node.argument.arguments.length === 0) {
50+ return;
51+ }
52+
53+ if (!isDirectYieldCandidate(node.argument.arguments[0])) {
54+ return;
55+ }
56+
57+ context.report({
58+ node,
59+ messageId: "noYieldEffectFail",
60+ });
61+ },
62+ };
63+ },
64+});