History

Use tagged errors for local runtime failures

Rhys Sullivan committed 8 months ago

Commit f858ab81 parent df6aa378 files · +235 −48Browse files

Unified · Split

8 files
  1. packages/control-plane/src/api/local-context.ts +7 −1
  2. packages/control-plane/src/runtime/local-config.ts +40 −13
  3. packages/control-plane/src/runtime/local-errors.ts +54 −0
  4. packages/control-plane/src/runtime/local-runtime-context.ts +25 −8
  5. packages/control-plane/src/runtime/local-source-artifacts.ts +23 −9
  6. packages/control-plane/src/runtime/local-workspace-state.ts +19 −8
  7. packages/control-plane/src/runtime/source-recipes-runtime.ts +6 −2
  8. packages/control-plane/src/runtime/source-store.ts +61 −7
packages/control-plane/src/api/local-context.ts+7−1
@@ -1,6 +1,9 @@
11 import type { WorkspaceId } from "#schema";
22 import * as Effect from "effect/Effect";
33
4+import {
5+ RuntimeLocalWorkspaceMismatchError,
6+} from "../runtime/local-errors";
47 import { requireRuntimeLocalWorkspace } from "../runtime/local-runtime-context";
58 import { ControlPlaneForbiddenError } from "./errors";
69
@@ -13,7 +16,10 @@ export const resolveRequestedLocalWorkspace = ( · 6 unmodified lines
1316 new ControlPlaneForbiddenError({
1417 operation,
1518 message: "Requested workspace is not the active local workspace",
16− details: cause instanceof Error ? cause.message : String(cause),
19+ details:
20+ cause instanceof RuntimeLocalWorkspaceMismatchError
21+ ? `requestedWorkspaceId=${cause.requestedWorkspaceId} activeWorkspaceId=${cause.activeWorkspaceId}`
22+ : "Runtime local workspace is unavailable",
1723 })
1824 ),
1925 );
packages/control-plane/src/runtime/local-config.ts+40−13
@@ -17,6 +17,11 @@ import { · 16 unmodified lines
1717 } from "#schema";
1818 import * as Effect from "effect/Effect";
1919 import * as Schema from "effect/Schema";
20+import {
21+ LocalExecutorConfigDecodeError,
22+ LocalFileSystemError,
23+ unknownLocalErrorDetails,
24+} from "./local-errors";
2025
2126 const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema);
2227
@@ -34,10 +39,13 @@ const provideNodeFileSystem = <A, E, R>( · 11 unmodified lines
3439 Exclude<R, FileSystem.FileSystem>
3540 >;
3641
37−const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => {
38− const message = cause instanceof Error ? cause.message : String(cause);
39− return new Error(`Failed to ${action} ${path}: ${message}`);
40−};
42+const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
43+ new LocalFileSystemError({
44+ message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
45+ action,
46+ path,
47+ details: unknownLocalErrorDetails(cause),
48+ });
4149
4250 const trimOrUndefined = (value: string | undefined | null): string | undefined => {
4351 const trimmed = value?.trim();
@@ -106,7 +114,7 @@ export const resolveHomeConfigPath = (input: { · 62 unmodified lines
106114 env?: NodeJS.ProcessEnv;
107115 platform?: NodeJS.Platform;
108116 homeDirectory?: string;
109−} = {}): Effect.Effect<string, Error> =>
117+} = {}): Effect.Effect<string, LocalFileSystemError> =>
110118 provideNodeFileSystem(Effect.gen(function* () {
111119 const fs = yield* FileSystem.FileSystem;
112120 const candidates = resolveDefaultHomeConfigCandidates(input);
@@ -152,13 +160,23 @@ const parseJsonc = (input: { path: string; content: string }): LocalExecutorConf · 39 unmodified lines
152160 allowTrailingComma: true,
153161 });
154162 if (errors.length > 0) {
155− throw new Error(formatJsoncParseErrors(input.content, errors));
163+ throw new LocalExecutorConfigDecodeError({
164+ message: `Invalid executor config at ${input.path}: ${formatJsoncParseErrors(input.content, errors)}`,
165+ path: input.path,
166+ details: formatJsoncParseErrors(input.content, errors),
167+ });
156168 }
157169
158170 return decodeLocalExecutorConfig(parsed);
159171 } catch (cause) {
160− const message = cause instanceof Error ? cause.message : String(cause);
161− throw new Error(`Invalid executor config at ${input.path}: ${message}`);
172+ if (cause instanceof LocalExecutorConfigDecodeError) {
173+ throw cause;
174+ }
175+ throw new LocalExecutorConfigDecodeError({
176+ message: `Invalid executor config at ${input.path}: ${unknownLocalErrorDetails(cause)}`,
177+ path: input.path,
178+ details: unknownLocalErrorDetails(cause),
179+ });
162180 }
163181 };
164182
@@ -320,7 +338,7 @@ export type LoadedLocalExecutorConfig = { · 155 unmodified lines
320338 export const resolveLocalWorkspaceContext = (input: {
321339 cwd?: string;
322340 workspaceRoot?: string;
323−} = {}): Effect.Effect<ResolvedLocalWorkspaceContext, Error> =>
341+} = {}): Effect.Effect<ResolvedLocalWorkspaceContext, LocalFileSystemError> =>
324342 provideNodeFileSystem(Effect.gen(function* () {
325343 const cwd = resolve(input.cwd ?? process.cwd());
326344 const workspaceRoot = resolve(
@@ -344,7 +362,7 @@ export const resolveLocalWorkspaceContext = (input: { · 17 unmodified lines
344362
345363 export const readOptionalLocalExecutorConfig = (
346364 path: string,
347−): Effect.Effect<LocalExecutorConfig | null, Error> =>
365+): Effect.Effect<LocalExecutorConfig | null, LocalFileSystemError | LocalExecutorConfigDecodeError> =>
348366 provideNodeFileSystem(Effect.gen(function* () {
349367 const fs = yield* FileSystem.FileSystem;
350368 const exists = yield* fs.exists(path).pipe(
@@ -360,13 +378,22 @@ export const readOptionalLocalExecutorConfig = ( · 9 unmodified lines
360378 return yield* Effect.try({
361379 try: () => parseJsonc({ path, content }),
362380 catch: (cause) =>
363− cause instanceof Error ? cause : new Error(String(cause)),
381+ cause instanceof LocalExecutorConfigDecodeError
382+ ? cause
383+ : new LocalExecutorConfigDecodeError({
384+ message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`,
385+ path,
386+ details: unknownLocalErrorDetails(cause),
387+ }),
364388 });
365389 }));
366390
367391 export const loadLocalExecutorConfig = (
368392 context: ResolvedLocalWorkspaceContext,
369−): Effect.Effect<LoadedLocalExecutorConfig, Error> =>
393+): Effect.Effect<
394+ LoadedLocalExecutorConfig,
395+ LocalFileSystemError | LocalExecutorConfigDecodeError
396+> =>
370397 Effect.gen(function* () {
371398 const [homeConfig, projectConfig] = yield* Effect.all([
372399 readOptionalLocalExecutorConfig(context.homeConfigPath),
@@ -388,7 +415,7 @@ export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string = · 15 unmodified lines
388415 export const writeProjectLocalExecutorConfig = (input: {
389416 context: ResolvedLocalWorkspaceContext;
390417 config: LocalExecutorConfig;
391−}): Effect.Effect<void, Error> =>
418+}): Effect.Effect<void, LocalFileSystemError> =>
392419 provideNodeFileSystem(Effect.gen(function* () {
393420 const fs = yield* FileSystem.FileSystem;
394421 yield* fs.makeDirectory(input.context.configDirectory, { recursive: true }).pipe(
packages/control-plane/src/runtime/local-errors.ts+54−0
@@ -0,0 +1,54 @@
1+import * as Data from "effect/Data";
2+
3+export const unknownLocalErrorDetails = (cause: unknown): string =>
4+ cause instanceof Error ? cause.message : String(cause);
5+
6+export class LocalFileSystemError extends Data.TaggedError("LocalFileSystemError")<{
7+ readonly message: string;
8+ readonly action: string;
9+ readonly path: string;
10+ readonly details: string;
11+}> {}
12+
13+export class LocalExecutorConfigDecodeError extends Data.TaggedError("LocalExecutorConfigDecodeError")<{
14+ readonly message: string;
15+ readonly path: string;
16+ readonly details: string;
17+}> {}
18+
19+export class LocalWorkspaceStateDecodeError extends Data.TaggedError("LocalWorkspaceStateDecodeError")<{
20+ readonly message: string;
21+ readonly path: string;
22+ readonly details: string;
23+}> {}
24+
25+export class LocalSourceArtifactDecodeError extends Data.TaggedError("LocalSourceArtifactDecodeError")<{
26+ readonly message: string;
27+ readonly path: string;
28+ readonly details: string;
29+}> {}
30+
31+export class RuntimeLocalWorkspaceUnavailableError extends Data.TaggedError("RuntimeLocalWorkspaceUnavailableError")<{
32+ readonly message: string;
33+}> {}
34+
35+export class RuntimeLocalWorkspaceMismatchError extends Data.TaggedError("RuntimeLocalWorkspaceMismatchError")<{
36+ readonly message: string;
37+ readonly requestedWorkspaceId: string;
38+ readonly activeWorkspaceId: string;
39+}> {}
40+
41+export class LocalConfiguredSourceNotFoundError extends Data.TaggedError("LocalConfiguredSourceNotFoundError")<{
42+ readonly message: string;
43+ readonly sourceId: string;
44+}> {}
45+
46+export class LocalSourceArtifactMissingError extends Data.TaggedError("LocalSourceArtifactMissingError")<{
47+ readonly message: string;
48+ readonly sourceId: string;
49+}> {}
50+
51+export class LocalUnsupportedSourceKindError extends Data.TaggedError("LocalUnsupportedSourceKindError")<{
52+ readonly message: string;
53+ readonly kind: string;
54+}> {}
packages/control-plane/src/runtime/local-runtime-context.ts+25−8
@@ -7,6 +7,10 @@ import type { · 6 unmodified lines
77 LoadedLocalExecutorConfig,
88 ResolvedLocalWorkspaceContext,
99 } from "./local-config";
10+import {
11+ RuntimeLocalWorkspaceMismatchError,
12+ RuntimeLocalWorkspaceUnavailableError,
13+} from "./local-errors";
1014
1115 export type RuntimeLocalWorkspaceState = {
1216 context: ResolvedLocalWorkspaceContext;
@@ -28,24 +32,37 @@ export const getRuntimeLocalWorkspaceOption = () => · 15 unmodified lines
2832 Effect.map((option) => (Option.isSome(option) ? option.value : null)),
2933 ) as Effect.Effect<RuntimeLocalWorkspaceState | null, never, never>;
3034
31−export const requireRuntimeLocalWorkspace = (workspaceId?: WorkspaceId) =>
32− Effect.flatMap(getRuntimeLocalWorkspaceOption(), (runtimeLocalWorkspace) => {
35+export const requireRuntimeLocalWorkspace = (
36+ workspaceId?: WorkspaceId,
37+): Effect.Effect<
38+ RuntimeLocalWorkspaceState,
39+ RuntimeLocalWorkspaceUnavailableError | RuntimeLocalWorkspaceMismatchError,
40+ never
41+> =>
42+ Effect.gen(function* () {
43+ const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
3344 if (runtimeLocalWorkspace === null) {
34− return Effect.fail(new Error("Runtime local workspace is unavailable"));
45+ return yield* Effect.fail(
46+ new RuntimeLocalWorkspaceUnavailableError({
47+ message: "Runtime local workspace is unavailable",
48+ }),
49+ );
3550 }
3651
3752 if (
3853 workspaceId !== undefined
3954 && runtimeLocalWorkspace.installation.workspaceId !== workspaceId
4055 ) {
41− return Effect.fail(
42− new Error(
43− `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`,
44− ),
56+ return yield* Effect.fail(
57+ new RuntimeLocalWorkspaceMismatchError({
58+ message: `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`,
59+ requestedWorkspaceId: workspaceId,
60+ activeWorkspaceId: runtimeLocalWorkspace.installation.workspaceId,
61+ }),
4562 );
4663 }
4764
48− return Effect.succeed(runtimeLocalWorkspace);
65+ return runtimeLocalWorkspace;
4966 });
5067
5168 export const requireRuntimeLocalAccountId = (workspaceId?: WorkspaceId) =>
packages/control-plane/src/runtime/local-source-artifacts.ts+23−9
@@ -17,6 +17,11 @@ import * as Effect from "effect/Effect"; · 16 unmodified lines
1717 import * as Schema from "effect/Schema";
1818
1919 import type { ResolvedLocalWorkspaceContext } from "./local-config";
20+import {
21+ LocalFileSystemError,
22+ LocalSourceArtifactDecodeError,
23+ unknownLocalErrorDetails,
24+} from "./local-errors";
2025 import {
2126 createSourceRecipeRevisionRecord,
2227 stableSourceRecipeId,
@@ -52,10 +57,13 @@ const provideNodeFileSystem = <A, E, R>( · 29 unmodified lines
5257 Exclude<R, FileSystem.FileSystem>
5358 >;
5459
55−const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => {
56− const message = cause instanceof Error ? cause.message : String(cause);
57− return new Error(`Failed to ${action} ${path}: ${message}`);
58−};
60+const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
61+ new LocalFileSystemError({
62+ message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
63+ action,
64+ path,
65+ details: unknownLocalErrorDetails(cause),
66+ });
5967
6068 const localSourceArtifactPath = (input: {
6169 context: ResolvedLocalWorkspaceContext;
@@ -157,7 +165,10 @@ export const buildLocalSourceArtifact = (input: { · 95 unmodified lines
157165 export const readLocalSourceArtifact = (input: {
158166 context: ResolvedLocalWorkspaceContext;
159167 sourceId: string;
160−}): Effect.Effect<LocalSourceArtifact | null, Error> =>
168+}): Effect.Effect<
169+ LocalSourceArtifact | null,
170+ LocalFileSystemError | LocalSourceArtifactDecodeError
171+> =>
161172 provideNodeFileSystem(Effect.gen(function* () {
162173 const fs = yield* FileSystem.FileSystem;
163174 const path = localSourceArtifactPath(input);
@@ -174,8 +185,11 @@ export const readLocalSourceArtifact = (input: { · 10 unmodified lines
174185 return yield* Effect.try({
175186 try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown),
176187 catch: (cause) => {
177− const message = cause instanceof Error ? cause.message : String(cause);
178− return new Error(`Invalid local source artifact at ${path}: ${message}`);
188+ return new LocalSourceArtifactDecodeError({
189+ message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`,
190+ path,
191+ details: unknownLocalErrorDetails(cause),
192+ });
179193 },
180194 });
181195 }));
@@ -184,7 +198,7 @@ export const writeLocalSourceArtifact = (input: { · 2 unmodified lines
184198 context: ResolvedLocalWorkspaceContext;
185199 sourceId: string;
186200 artifact: LocalSourceArtifact;
187−}): Effect.Effect<void, Error> =>
201+}): Effect.Effect<void, LocalFileSystemError> =>
188202 provideNodeFileSystem(Effect.gen(function* () {
189203 const fs = yield* FileSystem.FileSystem;
190204 const directory = join(input.context.artifactsDirectory, "sources");
@@ -200,7 +214,7 @@ export const writeLocalSourceArtifact = (input: { · 9 unmodified lines
200214 export const removeLocalSourceArtifact = (input: {
201215 context: ResolvedLocalWorkspaceContext;
202216 sourceId: string;
203−}): Effect.Effect<void, Error> =>
217+}): Effect.Effect<void, LocalFileSystemError> =>
204218 provideNodeFileSystem(Effect.gen(function* () {
205219 const fs = yield* FileSystem.FileSystem;
206220 const path = localSourceArtifactPath(input);
packages/control-plane/src/runtime/local-workspace-state.ts+19−8
@@ -11,6 +11,11 @@ import * as Effect from "effect/Effect"; · 10 unmodified lines
1111 import * as Schema from "effect/Schema";
1212
1313 import type { ResolvedLocalWorkspaceContext } from "./local-config";
14+import {
15+ LocalFileSystemError,
16+ LocalWorkspaceStateDecodeError,
17+ unknownLocalErrorDetails,
18+} from "./local-errors";
1419
1520 const WORKSPACE_STATE_BASENAME = "workspace-state.json";
1621
@@ -55,10 +60,13 @@ const provideNodeFileSystem = <A, E, R>( · 38 unmodified lines
5560 Exclude<R, FileSystem.FileSystem>
5661 >;
5762
58−const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => {
59− const message = cause instanceof Error ? cause.message : String(cause);
60− return new Error(`Failed to ${action} ${path}: ${message}`);
61−};
63+const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
64+ new LocalFileSystemError({
65+ message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
66+ action,
67+ path,
68+ details: unknownLocalErrorDetails(cause),
69+ });
6270
6371 const defaultLocalWorkspaceState = (): LocalWorkspaceState => ({
6472 version: 1,
@@ -72,7 +80,7 @@ export const localWorkspaceStatePath = ( · 7 unmodified lines
7280
7381 export const loadLocalWorkspaceState = (
7482 context: ResolvedLocalWorkspaceContext,
75−): Effect.Effect<LocalWorkspaceState, Error> =>
83+): Effect.Effect<LocalWorkspaceState, LocalFileSystemError | LocalWorkspaceStateDecodeError> =>
7684 provideNodeFileSystem(Effect.gen(function* () {
7785 const fs = yield* FileSystem.FileSystem;
7886 const path = localWorkspaceStatePath(context);
@@ -89,8 +97,11 @@ export const loadLocalWorkspaceState = ( · 10 unmodified lines
8997 return yield* Effect.try({
9098 try: () => decodeLocalWorkspaceState(JSON.parse(content) as unknown),
9199 catch: (cause) => {
92− const message = cause instanceof Error ? cause.message : String(cause);
93− return new Error(`Invalid local workspace state at ${path}: ${message}`);
100+ return new LocalWorkspaceStateDecodeError({
101+ message: `Invalid local workspace state at ${path}: ${unknownLocalErrorDetails(cause)}`,
102+ path,
103+ details: unknownLocalErrorDetails(cause),
104+ });
94105 },
95106 });
96107 }));
@@ -98,7 +109,7 @@ export const loadLocalWorkspaceState = ( · 1 unmodified line
98109 export const writeLocalWorkspaceState = (input: {
99110 context: ResolvedLocalWorkspaceContext;
100111 state: LocalWorkspaceState;
101−}): Effect.Effect<void, Error> =>
112+}): Effect.Effect<void, LocalFileSystemError> =>
102113 provideNodeFileSystem(Effect.gen(function* () {
103114 const fs = yield* FileSystem.FileSystem;
104115 yield* fs.makeDirectory(input.context.stateDirectory, { recursive: true }).pipe(
packages/control-plane/src/runtime/source-recipes-runtime.ts+6−2
@@ -17,6 +17,7 @@ import * as Effect from "effect/Effect"; · 16 unmodified lines
1717 import {
1818 readLocalSourceArtifact,
1919 } from "./local-source-artifacts";
20+import { LocalSourceArtifactMissingError } from "./local-errors";
2021 import {
2122 requireRuntimeLocalWorkspace,
2223 } from "./local-runtime-context";
@@ -218,7 +219,7 @@ export const loadSourceWithRecipe = (input: { · 195 unmodified lines
218219 workspaceId: WorkspaceId;
219220 sourceId: Source["id"];
220221 actorAccountId?: AccountId | null;
221−}): Effect.Effect<LoadedSourceRecipe, Error, never> =>
222+}): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never> =>
222223 Effect.gen(function* () {
223224 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
224225 const source = yield* loadSourceById(input.rows, {
@@ -232,7 +233,10 @@ export const loadSourceWithRecipe = (input: { · 7 unmodified lines
232233 });
233234 if (artifact === null) {
234235 return yield* Effect.fail(
235− new Error(`Recipe artifact missing for source ${input.sourceId}`),
236+ new LocalSourceArtifactMissingError({
237+ message: `Recipe artifact missing for source ${input.sourceId}`,
238+ sourceId: input.sourceId,
239+ }),
236240 );
237241 }
238242
packages/control-plane/src/runtime/source-store.ts+61−7
@@ -30,6 +30,15 @@ import { · 29 unmodified lines
3030 readLocalSourceArtifact,
3131 removeLocalSourceArtifact,
3232 } from "./local-source-artifacts";
33+import {
34+ LocalConfiguredSourceNotFoundError,
35+ LocalExecutorConfigDecodeError,
36+ LocalFileSystemError,
37+ LocalUnsupportedSourceKindError,
38+ LocalWorkspaceStateDecodeError,
39+ RuntimeLocalWorkspaceMismatchError,
40+ RuntimeLocalWorkspaceUnavailableError,
41+} from "./local-errors";
3342 import {
3443 requireRuntimeLocalWorkspace,
3544 } from "./local-runtime-context";
@@ -231,7 +240,21 @@ const configAuthFromSource = (input: { · 195 unmodified lines
231240 return input.existingConfigAuth;
232241 };
233242
234−const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId) =>
243+const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId): Effect.Effect<{
244+ context: ResolvedLocalWorkspaceContext;
245+ installation: {
246+ workspaceId: WorkspaceId;
247+ accountId: AccountId;
248+ };
249+ loadedConfig: LoadedLocalExecutorConfig;
250+ workspaceState: LocalWorkspaceState;
251+},
252+ | RuntimeLocalWorkspaceUnavailableError
253+ | RuntimeLocalWorkspaceMismatchError
254+ | LocalFileSystemError
255+ | LocalExecutorConfigDecodeError
256+ | LocalWorkspaceStateDecodeError,
257+never> =>
235258 Effect.gen(function* () {
236259 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);
237260 const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context);
@@ -256,11 +279,16 @@ const buildLocalSourceRecord = (input: { · 18 unmodified lines
256279 }): Effect.Effect<{
257280 source: Source;
258281 sourceId: SourceId;
259−}, Error, never> =>
282+}, LocalConfiguredSourceNotFoundError | Error, never> =>
260283 Effect.gen(function* () {
261284 const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId];
262285 if (!sourceConfig) {
263− return yield* Effect.fail(new Error(`Configured source not found for id ${input.sourceId}`));
286+ return yield* Effect.fail(
287+ new LocalConfiguredSourceNotFoundError({
288+ message: `Configured source not found for id ${input.sourceId}`,
289+ sourceId: input.sourceId,
290+ }),
291+ );
264292 }
265293
266294 const existingState = input.workspaceState.sources[input.sourceId];
@@ -356,7 +384,17 @@ export const loadSourcesInWorkspace = ( · 89 unmodified lines
356384 options: {
357385 actorAccountId?: AccountId | null;
358386 } = {},
359−) =>
387+): Effect.Effect<
388+ readonly Source[],
389+ | RuntimeLocalWorkspaceUnavailableError
390+ | RuntimeLocalWorkspaceMismatchError
391+ | LocalFileSystemError
392+ | LocalExecutorConfigDecodeError
393+ | LocalWorkspaceStateDecodeError
394+ | LocalConfiguredSourceNotFoundError
395+ | Error,
396+ never
397+> =>
360398 Effect.gen(function* () {
361399 const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId);
362400 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId);
@@ -382,13 +420,26 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 19 unmodified lines
382420 workspaceId: WorkspaceId;
383421 sourceId: Source["id"];
384422 actorAccountId?: AccountId | null;
385−}) =>
423+}): Effect.Effect<
424+ Source,
425+ | RuntimeLocalWorkspaceUnavailableError
426+ | RuntimeLocalWorkspaceMismatchError
427+ | LocalFileSystemError
428+ | LocalExecutorConfigDecodeError
429+ | LocalWorkspaceStateDecodeError
430+ | LocalConfiguredSourceNotFoundError
431+ | Error,
432+ never
433+> =>
386434 Effect.gen(function* () {
387435 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
388436 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId);
389437 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
390438 return yield* Effect.fail(
391− new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),
439+ new LocalConfiguredSourceNotFoundError({
440+ message: `Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
441+ sourceId: input.sourceId,
442+ }),
392443 );
393444 }
394445
@@ -456,7 +507,10 @@ const configSourceFromLocalSource = (input: { · 61 unmodified lines
456507 binding: cloneJson(input.source.binding) as Extract<LocalConfigSource, { kind: "mcp" }>["binding"],
457508 };
458509 default:
459− throw new Error(`Unsupported source kind for local config: ${input.source.kind}`);
510+ throw new LocalUnsupportedSourceKindError({
511+ message: `Unsupported source kind for local config: ${input.source.kind}`,
512+ kind: input.source.kind,
513+ });
460514 }
461515 };
462516