History

Ban node:fs in Effect modules

Rhys Sullivan committed 8 months ago

Commit 13fd4041 parent 820797928 files · +968 −496Browse files

Unified · Split

28 files
  1. .oxlintrc.jsonc +1 −0
  2. apps/executor/src/cli/main.ts +99 −79
  3. apps/executor/src/distribution/harness.ts +35 −28
  4. bun.lock +2 −1
  5. packages/clients/react/src/index.test.tsx +9 −6
  6. packages/kernel/runtime-deno-subprocess/src/index.ts +5 −2
  7. packages/platform/control-plane/src/runtime/catalog/source/type-declarations.test.ts +60 −26
  8. packages/platform/control-plane/src/runtime/control-plane-runtime.test.ts +2 −3
  9. packages/platform/control-plane/src/runtime/execution/http.test.ts +9 −3
  10. packages/platform/control-plane/src/runtime/execution/mcp-resume.test.ts +8 −3
  11. packages/platform/control-plane/src/runtime/execution/runtime-config.test.ts +25 −17
  12. packages/platform/control-plane/src/runtime/index.ts +11 −2
  13. packages/platform/control-plane/src/runtime/local/config.test.ts +21 −19
  14. packages/platform/control-plane/src/runtime/local/control-plane-store.test.ts +31 −20
  15. packages/platform/control-plane/src/runtime/local/control-plane-store.ts +68 −41
  16. packages/platform/control-plane/src/runtime/local/installation.test.ts +32 −19
  17. packages/platform/control-plane/src/runtime/local/secret-material-providers.ts +167 −124
  18. packages/platform/control-plane/src/runtime/local/source-artifacts.test.ts +32 −21
  19. packages/platform/control-plane/src/runtime/local/tools.ts +7 −7
  20. packages/platform/control-plane/src/runtime/sources/source-adapter-fixture-matrix.test.ts +19 −9
  21. packages/platform/control-plane/src/runtime/sources/source-auth-service.ts +156 −15
  22. packages/platform/server/package.json +1 −0
  23. packages/platform/server/src/index.ts +63 −37
  24. packages/sources/openapi/src/real-spec-coverage.test.ts +15 −6
  25. packages/sources/openapi/src/tool-presentation.test.ts +17 −8
  26. tools/oxlint/README.md +1 −0
  27. tools/oxlint/plugin.mjs +2 −0
  28. tools/oxlint/rules/no-node-fs-with-effect-imports.mjs +70 −0
.oxlintrc.jsonc+1−0
@@ -40,6 +40,7 @@ · 39 unmodified lines
4040
4141 // Monorepo-specific rules.
4242 "executor-monorepo/no-cross-workspace-relative-imports": "error",
43+ "executor-monorepo/no-node-fs-with-effect-imports": "error",
4344 "executor-monorepo/no-raw-effect-fail-errors": "error",
4445 "executor-monorepo/no-yield-effect-fail": "error",
4546 "executor-monorepo/no-workspace-src-imports": "warn"
apps/executor/src/cli/main.ts+99−79
@@ -1,9 +1,7 @@
1−import { existsSync, readFileSync } from "node:fs";
2−import { mkdir, open, readFile, rm } from "node:fs/promises";
31 import { spawn } from "node:child_process";
4−import { dirname, resolve } from "node:path";
2+import { createRequire } from "node:module";
3+import { dirname } from "node:path";
54 import { createInterface } from "node:readline/promises";
6−import { fileURLToPath } from "node:url";
75 import { FileSystem } from "@effect/platform";
86 import { Args, Command, Options } from "@effect/cli";
97 import {
@@ -120,17 +118,17 @@ const readCode = (input: { · 110 unmodified lines
120118 code?: string;
121119 file?: string;
122120 stdin?: boolean;
123−}): Effect.Effect<string, Error, never> =>
121+}): Effect.Effect<string, Error, FileSystem.FileSystem> =>
124122 Effect.gen(function* () {
125123 if (input.code && input.code.trim().length > 0) {
126124 return input.code;
127125 }
128126
129127 if (input.file && input.file.trim().length > 0) {
130− const contents = yield* Effect.tryPromise({
131− try: () => readFile(input.file!, "utf8"),
132− catch: toError,
133− });
128+ const fs = yield* FileSystem.FileSystem;
129+ const contents = yield* fs.readFileString(input.file!, "utf8").pipe(
130+ Effect.mapError(toError),
131+ );
134132 if (contents.trim().length > 0) {
135133 return contents;
136134 }
@@ -151,22 +149,17 @@ const getBootstrapClient = (baseUrl: string = DEFAULT_SERVER_BASE_URL) => · 14 unmodified lines
151149 createControlPlaneClient({ baseUrl });
152150
153151 const decodeExecutionId = Schema.decodeUnknown(ExecutionIdSchema);
154−const cliSourceDir = dirname(fileURLToPath(import.meta.url));
152+const require = createRequire(import.meta.url);
155153 const CLI_NAME = "executor";
156154 const CLI_VERSION = (() => {
157155 const candidatePaths = [
158− resolve(cliSourceDir, "../package.json"),
159− resolve(cliSourceDir, "../../package.json"),
156+ "../package.json",
157+ "../../package.json",
160158 ];
161159
162160 for (const candidatePath of candidatePaths) {
163− if (!existsSync(candidatePath)) {
164− continue;
165− }
166−
167161 try {
168− const contents = readFileSync(candidatePath, "utf8");
169− const metadata = JSON.parse(contents) as { version?: string };
162+ const metadata = require(candidatePath) as { version?: string };
170163 if (metadata.version && metadata.version.trim().length > 0) {
171164 return metadata.version;
172165 }
@@ -428,24 +421,30 @@ const getDefaultServerOptions = (port: number = DEFAULT_SERVER_PORT) => { · 255 unmodified lines
428421 };
429422
430423 const startServerInBackground = (port: number) =>
431− Effect.tryPromise({
432− try: async () => {
424+ Effect.scoped(
425+ Effect.gen(function* () {
426+ const fs = yield* FileSystem.FileSystem;
433427 const command = resolveSelfCommand(["__local-server", "--port", String(port)]);
434− await mkdir(dirname(DEFAULT_SERVER_LOG_FILE), { recursive: true });
435− const logHandle = await open(DEFAULT_SERVER_LOG_FILE, "a");
436−
437− try {
438− const child = spawn(command[0]!, command.slice(1), {
439− detached: true,
440− stdio: ["ignore", logHandle.fd, logHandle.fd],
441− });
442− child.unref();
443− } finally {
444− await logHandle.close();
445− }
446− },
447− catch: toError,
448− });
428+ yield* fs.makeDirectory(dirname(DEFAULT_SERVER_LOG_FILE), {
429+ recursive: true,
430+ }).pipe(Effect.mapError(toError));
431+ const logHandle = yield* fs.open(DEFAULT_SERVER_LOG_FILE, {
432+ flag: "a",
433+ }).pipe(Effect.mapError(toError));
434+
435+ yield* Effect.try({
436+ try: () => {
437+ const fd = Number(logHandle.fd);
438+ const child = spawn(command[0]!, command.slice(1), {
439+ detached: true,
440+ stdio: ["ignore", fd, fd],
441+ });
442+ child.unref();
443+ },
444+ catch: toError,
445+ });
446+ }),
447+ );
449448
450449 type LocalServerPidRecord = {
451450 pid?: number;
@@ -456,13 +455,21 @@ type LocalServerPidRecord = { · 4 unmodified lines
456455 logFile?: string;
457456 };
458457
459−const readPidRecord = (): Effect.Effect<LocalServerPidRecord | null, never, never> =>
460− Effect.tryPromise({
461− try: async () => {
462− const contents = await readFile(DEFAULT_SERVER_PID_FILE, "utf8");
463− return JSON.parse(contents) as LocalServerPidRecord;
464− },
465− catch: () => null,
458+const readPidRecord = (): Effect.Effect<
459+ LocalServerPidRecord | null,
460+ never,
461+ FileSystem.FileSystem
462+> =>
463+ Effect.gen(function* () {
464+ const fs = yield* FileSystem.FileSystem;
465+ const contents = yield* fs.readFileString(DEFAULT_SERVER_PID_FILE, "utf8").pipe(
466+ Effect.catchAll(() => Effect.succeed<string | null>(null)),
467+ );
468+ if (contents === null) {
469+ return null;
470+ }
471+
472+ return JSON.parse(contents) as LocalServerPidRecord;
466473 }).pipe(Effect.catchAll(() => Effect.succeed(null)));
467474
468475 const isPidRunning = (pid: number): boolean => {
@@ -547,7 +554,9 @@ const renderDenoSandboxDetail = (denoVersion: string | null): string => · 78 unmodified lines
547554 ? `deno ${denoVersion}`
548555 : "deno not found (run `executor sandbox` to install)";
549556
550−const getServerStatus = (baseUrl: string): Effect.Effect<LocalServerStatus, Error, never> =>
557+const getServerStatus = (
558+ baseUrl: string,
559+): Effect.Effect<LocalServerStatus, Error, FileSystem.FileSystem> =>
551560 Effect.gen(function* () {
552561 const pidRecord = yield* readPidRecord();
553562 const reachable = yield* isServerReachable(baseUrl);
@@ -647,20 +656,20 @@ const printText = (value: string) => · 93 unmodified lines
647656
648657 const stopServer = (baseUrl: string) =>
649658 Effect.gen(function* () {
659+ const fs = yield* FileSystem.FileSystem;
660+ const removePidFile = fs.remove(DEFAULT_SERVER_PID_FILE, {
661+ force: true,
662+ }).pipe(Effect.ignore);
650663 const pidRecord = yield* readPidRecord();
651664 const pid = typeof pidRecord?.pid === "number" ? pidRecord.pid : null;
652665
653666 if (pid === null) {
654− yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
655− Effect.ignore,
656− );
667+ yield* removePidFile;
657668 return false;
658669 }
659670
660671 if (!isPidRunning(pid)) {
661− yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
662− Effect.ignore,
663− );
672+ yield* removePidFile;
664673 return false;
665674 }
666675
@@ -671,7 +680,7 @@ const stopServer = (baseUrl: string) => · 4 unmodified lines
671680
672681 yield* waitForReachability(baseUrl, false).pipe(
673682 Effect.catchAll(() =>
674− Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
683+ removePidFile.pipe(
675684 Effect.ignore,
676685 Effect.zipRight(Effect.fail(executorAppEffectError("cli/main", `Timed out stopping local executor server pid ${pid}`))),
677686 ),
@@ -1178,38 +1187,49 @@ const doctorCommand = Command.make( · 500 unmodified lines
11781187 ),
11791188 ).pipe(Command.withDescription("Check local executor install and daemon health"));
11801189
1181−const getDenoVersion = (): Effect.Effect<string | null, never, never> =>
1182− Effect.tryPromise({
1183− try: () =>
1184− new Promise<string | null>((resolve) => {
1185− const denoExecutable = process.env.DENO_BIN?.trim()
1186− || (process.env.HOME && existsSync(`${process.env.HOME}/.deno/bin/deno`)
1187− ? `${process.env.HOME}/.deno/bin/deno`
1188− : "deno");
1189−
1190− const child = spawn(denoExecutable, ["--version"], {
1191− stdio: ["ignore", "pipe", "ignore"],
1192− timeout: 5000,
1193− });
1190+const getDenoVersion = (): Effect.Effect<string | null, never, FileSystem.FileSystem> =>
1191+ Effect.gen(function* () {
1192+ const fs = yield* FileSystem.FileSystem;
1193+ const configuredDenoExecutable = process.env.DENO_BIN?.trim();
1194+ const bundledDenoExecutable = process.env.HOME?.trim()
1195+ ? `${process.env.HOME.trim()}/.deno/bin/deno`
1196+ : null;
1197+ const bundledDenoExists = bundledDenoExecutable === null
1198+ ? false
1199+ : yield* fs.exists(bundledDenoExecutable).pipe(
1200+ Effect.catchAll(() => Effect.succeed(false)),
1201+ );
1202+ const denoExecutable = configuredDenoExecutable
1203+ || (bundledDenoExists ? bundledDenoExecutable : null)
1204+ || "deno";
1205+
1206+ return yield* Effect.tryPromise({
1207+ try: () =>
1208+ new Promise<string | null>((resolveVersion) => {
1209+ const child = spawn(denoExecutable, ["--version"], {
1210+ stdio: ["ignore", "pipe", "ignore"],
1211+ timeout: 5000,
1212+ });
11941213
1195− let stdout = "";
1196− child.stdout?.setEncoding("utf8");
1197− child.stdout?.on("data", (chunk: string) => {
1198− stdout += chunk;
1199− });
1214+ let stdout = "";
1215+ child.stdout?.setEncoding("utf8");
1216+ child.stdout?.on("data", (chunk: string) => {
1217+ stdout += chunk;
1218+ });
12001219
1201− child.once("error", () => resolve(null));
1202− child.once("close", (code) => {
1203− if (code !== 0) {
1204− resolve(null);
1205− return;
1206− }
1220+ child.once("error", () => resolveVersion(null));
1221+ child.once("close", (code) => {
1222+ if (code !== 0) {
1223+ resolveVersion(null);
1224+ return;
1225+ }
12071226
1208− const match = /deno\s+(\S+)/i.exec(stdout);
1209− resolve(match ? match[1] : null);
1210− });
1211− }),
1212− catch: () => null,
1227+ const match = /deno\s+(\S+)/i.exec(stdout);
1228+ resolveVersion(match ? match[1] : null);
1229+ });
1230+ }),
1231+ catch: () => null,
1232+ }).pipe(Effect.catchAll(() => Effect.succeed(null)));
12131233 }).pipe(Effect.catchAll(() => Effect.succeed(null)));
12141234
12151235 const sandboxCommand = Command.make(
apps/executor/src/distribution/harness.ts+35−28
@@ -1,9 +1,10 @@
11 import { spawn } from "node:child_process";
2−import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
32 import { createServer } from "node:net";
43 import { tmpdir } from "node:os";
54 import { dirname, join } from "node:path";
65
6+import { FileSystem } from "@effect/platform";
7+import { NodeFileSystem } from "@effect/platform-node";
78 import * as Context from "effect/Context";
89 import * as Effect from "effect/Effect";
910 import * as Layer from "effect/Layer";
@@ -11,6 +12,9 @@ import * as Layer from "effect/Layer"; · 1 unmodified line
1112 import { buildDistributionPackage } from "./artifact";
1213 import { executorAppEffectError } from "../effect-errors";
1314
15+const toError = (cause: unknown): Error =>
16+ cause instanceof Error ? cause : new Error(String(cause));
17+
1418 type CommandResult = {
1519 readonly stdout: string;
1620 readonly stderr: string;
@@ -173,16 +177,17 @@ const packPackage = (packageDir: string, outputDir: string) => · 156 unmodified lines
173177 export const LocalDistributionHarnessLive = Layer.scoped(
174178 DistributionHarness,
175179 Effect.gen(function* () {
180+ const fs = yield* FileSystem.FileSystem;
176181 const tempRoot = yield* Effect.acquireRelease(
177− Effect.tryPromise({
178− try: () => mkdtemp(join(tmpdir(), "executor-distribution-")),
179− catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
180− }),
181− (path) => Effect.tryPromise({
182− try: () => rm(path, { recursive: true, force: true }),
183− catch: (cause) =>
184− cause instanceof Error ? cause : new Error(String(cause ?? "temp dir cleanup failed")),
185− }).pipe(Effect.orDie),
182+ fs.makeTempDirectory({
183+ directory: tmpdir(),
184+ prefix: "executor-distribution-",
185+ }).pipe(Effect.mapError(toError)),
186+ (path) =>
187+ fs.remove(path, { recursive: true, force: true }).pipe(
188+ Effect.mapError(toError),
189+ Effect.orDie,
190+ ),
186191 );
187192
188193 const packageDir = join(tempRoot, "package");
@@ -193,14 +198,11 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 4 unmodified lines
193198 const installedWorkspaceRoot = tempRoot;
194199 const baseUrl = `http://127.0.0.1:${yield* allocatePort()}`;
195200
196− yield* Effect.tryPromise({
197− try: async () => {
198− await mkdir(prefixDir, { recursive: true });
199− await mkdir(homeDir, { recursive: true });
200− await mkdir(executorHome, { recursive: true });
201− },
202− catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
203− });
201+ yield* Effect.all([
202+ fs.makeDirectory(prefixDir, { recursive: true }),
203+ fs.makeDirectory(homeDir, { recursive: true }),
204+ fs.makeDirectory(executorHome, { recursive: true }),
205+ ]).pipe(Effect.mapError(toError));
204206
205207 const artifact = yield* buildPackage(packageDir);
206208 const tarballPath = yield* packPackage(packageDir, tempRoot);
@@ -263,16 +265,21 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 56 unmodified lines
263265 });
264266
265267 const writeProjectConfig = (contents: string) =>
266− Effect.tryPromise({
267− try: async () => {
268− for (const workspaceRoot of [stagedWorkspaceRoot, installedWorkspaceRoot]) {
268+ Effect.forEach(
269+ [stagedWorkspaceRoot, installedWorkspaceRoot],
270+ (workspaceRoot) =>
271+ Effect.gen(function* () {
269272 const configDir = join(workspaceRoot, ".executor");
270− await mkdir(configDir, { recursive: true });
271− await writeFile(join(configDir, "executor.jsonc"), contents, "utf8");
272− }
273− },
274− catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
275− });
273+ yield* fs.makeDirectory(configDir, { recursive: true }).pipe(
274+ Effect.mapError(toError),
275+ );
276+ yield* fs.writeFileString(
277+ join(configDir, "executor.jsonc"),
278+ contents,
279+ ).pipe(Effect.mapError(toError));
280+ }),
281+ { discard: true },
282+ );
276283
277284 return DistributionHarness.of({
278285 packageDir,
@@ -285,5 +292,5 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 6 unmodified lines
285292 runInstalled,
286293 fetchText,
287294 });
288− }),
295+ }).pipe(Effect.provide(NodeFileSystem.layer)),
289296 );
bun.lock+2−1
@@ -19,7 +19,7 @@ · 18 unmodified lines
1919 },
2020 "apps/executor": {
2121 "name": "executor",
22− "version": "1.2.2",
22+ "version": "1.2.3",
2323 "bin": {
2424 "executor": "bin/executor",
2525 },
@@ -299,6 +299,7 @@ · 273 unmodified lines
299299 "dependencies": {
300300 "@effect/opentelemetry": "^0.61.0",
301301 "@effect/platform": "catalog:",
302+ "@effect/platform-node": "catalog:",
302303 "@executor/control-plane": "workspace:*",
303304 "@executor/executor-mcp": "workspace:*",
304305 "@opentelemetry/api": "^1.9.0",
packages/clients/react/src/index.test.tsx+9−6
@@ -1,7 +1,7 @@
11 import { createServer } from "node:http";
22
3−import { HttpApiBuilder, HttpServer } from "@effect/platform";
4−import { NodeHttpServer } from "@effect/platform-node";
3+import { FileSystem, HttpApiBuilder, HttpServer } from "@effect/platform";
4+import { NodeFileSystem, NodeHttpServer } from "@effect/platform-node";
55 import { describe, expect, it } from "@effect/vitest";
66 import {
77 createControlPlaneApiLayer,
@@ -92,9 +92,15 @@ const closeScope = (scope: Scope.CloseableScope) => · 84 unmodified lines
9292 Scope.close(scope, Exit.void).pipe(Effect.orDie);
9393
9494 const startControlPlaneServer = async (): Promise<ApiServer> => {
95+ const workspaceRoot = await Effect.runPromise(
96+ FileSystem.FileSystem.pipe(
97+ Effect.flatMap((fs) => fs.makeTempDirectory({ prefix: "executor-react-test-" })),
98+ Effect.provide(NodeFileSystem.layer),
99+ ),
100+ );
95101 const runtime = await Effect.runPromise(
96102 createControlPlaneRuntime({
97− workspaceRoot: mkdtempSync(join(tmpdir(), "executor-react-test-")),
103+ workspaceRoot,
98104 }),
99105 );
100106 const scope = await Effect.runPromise(Scope.make());
@@ -747,6 +753,3 @@ describe("executor-react source hooks", () => { · 646 unmodified lines
747753 60_000,
748754 );
749755 });
750−import { mkdtempSync } from "node:fs";
751−import { tmpdir } from "node:os";
752−import { join } from "node:path";
packages/kernel/runtime-deno-subprocess/src/index.ts+5−2
@@ -1,5 +1,4 @@
11 import { spawnSync } from "node:child_process";
2−import { existsSync } from "node:fs";
32 import { fileURLToPath } from "node:url";
43
54 import type {
@@ -84,7 +83,11 @@ const defaultDenoExecutable = (): string => { · 78 unmodified lines
8483 const home = process.env.HOME?.trim();
8584 if (home) {
8685 const installedPath = `${home}/.deno/bin/deno`;
87− if (existsSync(installedPath)) {
86+ const installedResult = spawnSync(installedPath, ["--version"], {
87+ stdio: "ignore",
88+ timeout: 5000,
89+ });
90+ if (installedResult.error === undefined && installedResult.status === 0) {
8891 return installedPath;
8992 }
9093 }
packages/platform/control-plane/src/runtime/catalog/source/type-declarations.test.ts+60−26
@@ -1,6 +1,7 @@
1−import { mkdtempSync, readFileSync } from "node:fs";
21 import { tmpdir } from "node:os";
32 import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
45 import { describe, expect, it } from "@effect/vitest";
56 import * as Effect from "effect/Effect";
67
@@ -325,9 +326,34 @@ const makeSource = (input: { · 318 unmodified lines
325326 updatedAt: 0,
326327 });
327328
329+const makeTempDirectory = (prefix: string) =>
330+ FileSystem.FileSystem.pipe(
331+ Effect.flatMap((fs) =>
332+ fs.makeTempDirectory({
333+ directory: tmpdir(),
334+ prefix,
335+ })
336+ ),
337+ Effect.provide(NodeFileSystem.layer),
338+ );
339+
340+const readTextFile = (path: string) =>
341+ FileSystem.FileSystem.pipe(
342+ Effect.flatMap((fs) => fs.readFileString(path, "utf8")),
343+ Effect.provide(NodeFileSystem.layer),
344+ );
345+
346+const fileExists = (path: string) =>
347+ FileSystem.FileSystem.pipe(
348+ Effect.flatMap((fs) => fs.exists(path)),
349+ Effect.provide(NodeFileSystem.layer),
350+ );
351+
328352 describe("source-type-declarations", () => {
329353 it("writes per-source and aggregate declaration files", async () => {
330− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-types-"));
354+ const workspaceRoot = await Effect.runPromise(
355+ makeTempDirectory("executor-types-"),
356+ );
331357 const context = {
332358 cwd: workspaceRoot,
333359 workspaceRoot,
@@ -355,13 +381,13 @@ describe("source-type-declarations", () => { · 21 unmodified lines
355381 ],
356382 }));
357383
358− const sourceDeclaration = readFileSync(
359− join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
360− "utf8",
384+ const sourceDeclaration = await Effect.runPromise(
385+ readTextFile(
386+ join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
387+ ),
361388 );
362− const aggregateDeclaration = readFileSync(
363− join(workspaceRoot, ".executor", "types", "index.d.ts"),
364− "utf8",
389+ const aggregateDeclaration = await Effect.runPromise(
390+ readTextFile(join(workspaceRoot, ".executor", "types", "index.d.ts")),
365391 );
366392
367393 expect(sourceDeclaration).toContain("export interface SourceTools_src_linear");
@@ -381,7 +407,9 @@ describe("source-type-declarations", () => { · 13 unmodified lines
381407 });
382408
383409 it("removes stale source declarations when the source disappears", async () => {
384− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-types-stale-"));
410+ const workspaceRoot = await Effect.runPromise(
411+ makeTempDirectory("executor-types-stale-"),
412+ );
385413 const context = {
386414 cwd: workspaceRoot,
387415 workspaceRoot,
@@ -404,22 +432,24 @@ describe("source-type-declarations", () => { · 16 unmodified lines
404432 entries: [],
405433 }));
406434
407− const aggregateDeclaration = readFileSync(
408− join(workspaceRoot, ".executor", "types", "index.d.ts"),
409− "utf8",
435+ const aggregateDeclaration = await Effect.runPromise(
436+ readTextFile(join(workspaceRoot, ".executor", "types", "index.d.ts")),
410437 );
411438
412439 expect(aggregateDeclaration).toContain("export type ExecutorSourceTools = {};");
413− expect(() =>
414− readFileSync(
415− join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
416− "utf8",
417− )
418− ).toThrow();
440+ expect(
441+ await Effect.runPromise(
442+ fileExists(
443+ join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
444+ ),
445+ ),
446+ ).toBe(false);
419447 });
420448
421449 it("falls back to unknown for unsupported declaration-only shape nodes", async () => {
422− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-types-unsupported-"));
450+ const workspaceRoot = await Effect.runPromise(
451+ makeTempDirectory("executor-types-unsupported-"),
452+ );
423453 const context = {
424454 cwd: workspaceRoot,
425455 workspaceRoot,
@@ -525,9 +555,10 @@ describe("source-type-declarations", () => { · 99 unmodified lines
525555 entries: [{ source: makeSource({ id: "src_linear" }), snapshot }],
526556 }));
527557
528− const sourceDeclaration = readFileSync(
529− join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
530− "utf8",
558+ const sourceDeclaration = await Effect.runPromise(
559+ readTextFile(
560+ join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
561+ ),
531562 );
532563
533564 expect(sourceDeclaration).toContain("unsupportedNot?: unknown;");
@@ -535,7 +566,9 @@ describe("source-type-declarations", () => { · 1 unmodified line
535566 });
536567
537568 it("renders object unions as discriminated unions with shared fields", async () => {
538− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-types-union-"));
569+ const workspaceRoot = await Effect.runPromise(
570+ makeTempDirectory("executor-types-union-"),
571+ );
539572 const context = {
540573 cwd: workspaceRoot,
541574 workspaceRoot,
@@ -692,9 +725,10 @@ describe("source-type-declarations", () => { · 150 unmodified lines
692725 entries: [{ source: makeSource({ id: "src_linear" }), snapshot }],
693726 }));
694727
695− const sourceDeclaration = readFileSync(
696− join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
697− "utf8",
728+ const sourceDeclaration = await Effect.runPromise(
729+ readTextFile(
730+ join(workspaceRoot, ".executor", "types", "sources", "src_linear.d.ts"),
731+ ),
698732 );
699733
700734 expect(sourceDeclaration).toContain("teams: (args?: LinearTeamsCall) => Promise<{");
packages/platform/control-plane/src/runtime/control-plane-runtime.test.ts+2−3
@@ -1,5 +1,4 @@
11 import { createServer } from "node:http";
2−import { readFileSync } from "node:fs";
32 import { join } from "node:path";
43
54 import { FileSystem } from "@effect/platform";
@@ -414,7 +413,7 @@ describe("control-plane-runtime", () => { · 408 unmodified lines
414413 );
415414
416415 const configPath = join(workspaceRoot, ".executor", "executor.jsonc");
417− const createdConfig = JSON.parse(readFileSync(configPath, "utf8")) as {
416+ const createdConfig = JSON.parse(yield* fs.readFileString(configPath, "utf8")) as {
418417 sources?: Record<string, { kind: string; connection: { endpoint: string } }>;
419418 };
420419 expect(createdConfig.sources?.github?.kind).toBe("openapi");
@@ -432,7 +431,7 @@ describe("control-plane-runtime", () => { · 11 unmodified lines
432431 );
433432 expect(removed.removed).toBe(true);
434433
435− const removedConfig = JSON.parse(readFileSync(configPath, "utf8")) as {
434+ const removedConfig = JSON.parse(yield* fs.readFileString(configPath, "utf8")) as {
436435 sources?: Record<string, unknown>;
437436 };
438437 expect(removedConfig.sources?.github).toBeUndefined();
packages/platform/control-plane/src/runtime/execution/http.test.ts+9−3
@@ -1,6 +1,7 @@
1−import { mkdtempSync } from "node:fs";
21 import { tmpdir } from "node:os";
32 import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
45 import { describe, expect, it } from "@effect/vitest";
56 import * as Effect from "effect/Effect";
67 import * as Schema from "effect/Schema";
@@ -40,8 +41,12 @@ const makeExecutionResolver = () => { · 33 unmodified lines
4041 };
4142
4243 const makeRuntime = Effect.acquireRelease(
43− Effect.sync(() => {
44− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-execution-http-"));
44+ Effect.gen(function* () {
45+ const fs = yield* FileSystem.FileSystem;
46+ const workspaceRoot = yield* fs.makeTempDirectory({
47+ directory: tmpdir(),
48+ prefix: "executor-execution-http-",
49+ });
4550
4651 return {
4752 workspaceRoot,
@@ -49,6 +54,7 @@ const makeRuntime = Effect.acquireRelease( · 1 unmodified line
4954 homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
5055 };
5156 }).pipe(
57+ Effect.provide(NodeFileSystem.layer),
5258 Effect.flatMap(({ workspaceRoot, homeConfigPath, homeStateDirectory }) =>
5359 createControlPlaneRuntime({
5460 localDataDir: ":memory:",
packages/platform/control-plane/src/runtime/execution/mcp-resume.test.ts+8−3
@@ -5,9 +5,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; · 4 unmodified lines
55 import { Client } from "@modelcontextprotocol/sdk/client/index.js";
66 import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
77 import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8−import { mkdtempSync } from "node:fs";
98 import { tmpdir } from "node:os";
109 import { join } from "node:path";
10+import { FileSystem } from "@effect/platform";
11+import { NodeFileSystem } from "@effect/platform-node";
1112 import { describe, expect, it } from "@effect/vitest";
1213 import { assertTrue } from "@effect/vitest/utils";
1314 import * as Effect from "effect/Effect";
@@ -283,7 +284,11 @@ describe("execution-mcp-resume", () => { · 269 unmodified lines
283284 it.scoped("keeps live form elicitation resumable without replaying the run", () =>
284285 Effect.gen(function* () {
285286 const mcpServer = yield* makeFormElicitationServer;
286− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-execution-mcp-resume-"));
287+ const fs = yield* FileSystem.FileSystem;
288+ const workspaceRoot = yield* fs.makeTempDirectory({
289+ directory: tmpdir(),
290+ prefix: "executor-execution-mcp-resume-",
291+ });
287292 const runtime = yield* Effect.acquireRelease(
288293 createControlPlaneRuntime({
289294 localDataDir: ":memory:",
@@ -357,7 +362,7 @@ describe("execution-mcp-resume", () => { · 67 unmodified lines
357362 expect(resumed.pendingInteraction).toBeNull();
358363 expect(resumed.execution.resultJson).toContain("approved:from-control-plane");
359364
360− }),
365+ }).pipe(Effect.provide(NodeFileSystem.layer)),
361366 60_000,
362367 );
363368 });
packages/platform/control-plane/src/runtime/execution/runtime-config.test.ts+25−17
@@ -1,6 +1,7 @@
1−import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
21 import { tmpdir } from "node:os";
32 import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
45 import { describe, expect, it } from "@effect/vitest";
56 import * as Effect from "effect/Effect";
67
@@ -12,26 +13,33 @@ import { withControlPlaneClient } from "./test-http-client"; · 5 unmodified lines
1213 const writeProjectConfig = (
1314 workspaceRoot: string,
1415 config: Record<string, unknown>,
15−) => {
16+) =>
17+ Effect.gen(function* () {
18+ const fs = yield* FileSystem.FileSystem;
1619 const configDirectory = join(workspaceRoot, ".executor");
17− mkdirSync(configDirectory, { recursive: true });
18− writeFileSync(
19− join(configDirectory, "executor.jsonc"),
20− `${JSON.stringify(config, null, 2)}\n`,
21− "utf8",
22− );
23−};
20+ yield* fs.makeDirectory(configDirectory, { recursive: true });
21+ yield* fs.writeFileString(
22+ join(configDirectory, "executor.jsonc"),
23+ `${JSON.stringify(config, null, 2)}\n`,
24+ );
25+ });
2426
2527 const makeRuntime = (config: Record<string, unknown>) => {
26− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-runtime-config-"));
27− writeProjectConfig(workspaceRoot, config);
28−
2928 return Effect.acquireRelease(
30− createControlPlaneRuntime({
31− workspaceRoot,
32− homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
33− homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
34− }),
29+ Effect.gen(function* () {
30+ const fs = yield* FileSystem.FileSystem;
31+ const workspaceRoot = yield* fs.makeTempDirectory({
32+ directory: tmpdir(),
33+ prefix: "executor-runtime-config-",
34+ });
35+ yield* writeProjectConfig(workspaceRoot, config);
36+
37+ return yield* createControlPlaneRuntime({
38+ workspaceRoot,
39+ homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
40+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
41+ });
42+ }).pipe(Effect.provide(NodeFileSystem.layer)),
3543 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
3644 );
3745 };
packages/platform/control-plane/src/runtime/index.ts+11−2
@@ -1,9 +1,11 @@
1+import { FileSystem } from "@effect/platform";
12 import * as Effect from "effect/Effect";
23 import * as Exit from "effect/Exit";
34 import * as Layer from "effect/Layer";
45 import * as ManagedRuntime from "effect/ManagedRuntime";
56 import * as Scope from "effect/Scope";
67 import { NodeFileSystem } from "@effect/platform-node";
8+import { clearAllMcpConnectionPools } from "@executor/source-mcp";
79
810 import { type ControlPlaneApiRuntimeContext } from "#api";
911 import type { LocalInstallation } from "#schema";
@@ -217,6 +219,7 @@ export const createControlPlaneRuntime = ( · 207 unmodified lines
217219
218220 const installationStore = LocalInstallationStore;
219221 const workspaceConfigStore = LocalWorkspaceConfigStore;
222+ const fileSystem = yield* FileSystem.FileSystem;
220223
221224 const localInstallation = yield* installationStore.getOrProvision({
222225 context: localWorkspaceContext,
@@ -227,7 +230,10 @@ export const createControlPlaneRuntime = ( · 4 unmodified lines
227230 ),
228231 );
229232
230− const persistence = createLocalControlPlanePersistence(localWorkspaceContext);
233+ const persistence = createLocalControlPlanePersistence(
234+ localWorkspaceContext,
235+ fileSystem,
236+ );
231237 const rows = persistence.rows;
232238
233239 const loadedLocalConfig = yield* workspaceConfigStore.load(
@@ -276,6 +282,9 @@ export const createControlPlaneRuntime = ( · 42 unmodified lines
276282 localInstallation,
277283 managedRuntime,
278284 runtimeLayer: concreteRuntimeLayer as RuntimeControlPlaneLayer,
279− close: () => managedRuntime.dispose().catch(() => {}),
285+ close: async () => {
286+ await clearAllMcpConnectionPools().catch(() => undefined);
287+ await managedRuntime.dispose().catch(() => undefined);
288+ },
280289 };
281290 }).pipe(Effect.provide(NodeFileSystem.layer));
packages/platform/control-plane/src/runtime/local/config.test.ts+21−19
@@ -1,7 +1,6 @@
1−import { mkdtempSync } from "node:fs";
2−import { mkdir, writeFile } from "node:fs/promises";
31 import { tmpdir } from "node:os";
42 import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
54 import { NodeFileSystem } from "@effect/platform-node";
65 import { describe, expect, it } from "@effect/vitest";
76 import * as Effect from "effect/Effect";
@@ -15,18 +14,25 @@ import { · 7 unmodified lines
1514 } from "./config";
1615
1716 const makeWorkspaceRoot = () =>
18− mkdtempSync(join(tmpdir(), "executor-local-config-"));
17+ FileSystem.FileSystem.pipe(
18+ Effect.flatMap((fs) =>
19+ fs.makeTempDirectory({
20+ directory: tmpdir(),
21+ prefix: "executor-local-config-",
22+ })
23+ ),
24+ );
1925
2026 describe("local-config", () => {
2127 it.effect("parses jsonc project config with comments and trailing commas", () =>
2228 Effect.gen(function* () {
23− const workspaceRoot = makeWorkspaceRoot();
29+ const fs = yield* FileSystem.FileSystem;
30+ const workspaceRoot = yield* makeWorkspaceRoot();
2431 const configDirectory = join(workspaceRoot, ".executor");
25− yield* Effect.promise(() => mkdir(configDirectory, { recursive: true }));
26− yield* Effect.promise(() =>
27− writeFile(
28− join(configDirectory, "executor.jsonc"),
29− `{
32+ yield* fs.makeDirectory(configDirectory, { recursive: true });
33+ yield* fs.writeFileString(
34+ join(configDirectory, "executor.jsonc"),
35+ `{
3036 "runtime": "ses",
3137 // local workspace config
3238 "sources": {
@@ -42,8 +48,6 @@ describe("local-config", () => { · 9 unmodified lines
4248 },
4349 }
4450 `,
45− "utf8",
46− ),
4751 );
4852
4953 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });
@@ -60,13 +64,13 @@ describe("local-config", () => { · 10 unmodified lines
6064
6165 it.effect("reports jsonc syntax errors with line and column details", () =>
6266 Effect.gen(function* () {
63− const workspaceRoot = makeWorkspaceRoot();
67+ const fs = yield* FileSystem.FileSystem;
68+ const workspaceRoot = yield* makeWorkspaceRoot();
6469 const configDirectory = join(workspaceRoot, ".executor");
65− yield* Effect.promise(() => mkdir(configDirectory, { recursive: true }));
66− yield* Effect.promise(() =>
67− writeFile(
68− join(configDirectory, "executor.jsonc"),
69− `{
70+ yield* fs.makeDirectory(configDirectory, { recursive: true });
71+ yield* fs.writeFileString(
72+ join(configDirectory, "executor.jsonc"),
73+ `{
7074 "sources": {
7175 "github": {
7276 "kind": "openapi"
@@ -77,8 +81,6 @@ describe("local-config", () => { · 4 unmodified lines
7781 }
7882 }
7983 `,
80− "utf8",
81− ),
8284 );
8385
8486 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });
packages/platform/control-plane/src/runtime/local/control-plane-store.test.ts+31−20
@@ -1,6 +1,7 @@
1−import { existsSync, mkdtempSync, statSync } from "node:fs";
21 import { tmpdir } from "node:os";
32 import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
45 import { describe, expect, it } from "@effect/vitest";
56 import * as Effect from "effect/Effect";
67
@@ -11,26 +12,36 @@ import { · 4 unmodified lines
1112 writeLocalControlPlaneState,
1213 } from "./control-plane-store";
1314
14−const makeContext = (): ResolvedLocalWorkspaceContext => {
15− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-control-plane-store-"));
15+const makeContext = (): Effect.Effect<
16+ ResolvedLocalWorkspaceContext,
17+ never,
18+ FileSystem.FileSystem
19+> =>
20+ Effect.gen(function* () {
21+ const fs = yield* FileSystem.FileSystem;
22+ const workspaceRoot = yield* fs.makeTempDirectory({
23+ directory: tmpdir(),
24+ prefix: "executor-control-plane-store-",
25+ }).pipe(Effect.orDie);
1626
17− return {
18− cwd: workspaceRoot,
19− workspaceRoot,
20− workspaceName: "executor-control-plane-store",
21− configDirectory: join(workspaceRoot, ".executor"),
22− projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
23− homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
24− homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
25− artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
26− stateDirectory: join(workspaceRoot, ".executor", "state"),
27− };
28−};
27+ return {
28+ cwd: workspaceRoot,
29+ workspaceRoot,
30+ workspaceName: "executor-control-plane-store",
31+ configDirectory: join(workspaceRoot, ".executor"),
32+ projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
33+ homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
34+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
35+ artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
36+ stateDirectory: join(workspaceRoot, ".executor", "state"),
37+ };
38+ });
2939
3040 describe("local-control-plane-store", () => {
3141 it.effect("stores secret-bearing control-plane state outside the workspace", () =>
3242 Effect.gen(function* () {
33− const context = makeContext();
43+ const fs = yield* FileSystem.FileSystem;
44+ const context = yield* makeContext();
3445 const expectedPath = localControlPlaneStatePath(context);
3546 const workspacePath = join(context.stateDirectory, "control-plane-state.json");
3647
@@ -52,16 +63,16 @@ describe("local-control-plane-store", () => { · 15 unmodified lines
5263 });
5364
5465 expect(expectedPath.startsWith(context.homeStateDirectory)).toBe(true);
55− expect(existsSync(expectedPath)).toBe(true);
56− expect(existsSync(workspacePath)).toBe(false);
66+ expect(yield* fs.exists(expectedPath)).toBe(true);
67+ expect(yield* fs.exists(workspacePath)).toBe(false);
5768
5869 const loaded = yield* loadLocalControlPlaneState(context);
5970 expect(loaded.version).toBe(1);
6071 expect(loaded.secretMaterials).toEqual([]);
6172
6273 if (process.platform !== "win32") {
63− expect(statSync(expectedPath).mode & 0o777).toBe(0o600);
74+ expect((yield* fs.stat(expectedPath)).mode & 0o777).toBe(0o600);
6475 }
65− }),
76+ }).pipe(Effect.provide(NodeFileSystem.layer)),
6677 );
6778 });
packages/platform/control-plane/src/runtime/local/control-plane-store.ts+68−41
@@ -1,7 +1,7 @@
11 import { randomUUID } from "node:crypto";
2−import { existsSync } from "node:fs";
3−import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
42 import { dirname, join } from "node:path";
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
55
66 import {
77 AuthArtifactSchema,
@@ -60,7 +60,7 @@ export type LocalControlPlanePersistence = { · 52 unmodified lines
6060 close: () => Promise<void>;
6161 };
6262
63−const decodeLocalControlPlaneState = Schema.decodeUnknownSync(
63+const decodeLocalControlPlaneState = Schema.decodeUnknown(
6464 LocalControlPlaneStateSchema,
6565 );
6666
@@ -118,54 +118,74 @@ const localControlPlaneStatePath = ( · 51 unmodified lines
118118 LOCAL_CONTROL_PLANE_STATE_BASENAME,
119119 );
120120
121−const readStateFromDisk = async (
121+const bindFileSystem = <A, E>(
122+ fileSystem: FileSystem.FileSystem,
123+ effect: Effect.Effect<A, E, FileSystem.FileSystem>,
124+): Effect.Effect<A, E, never> =>
125+ effect.pipe(Effect.provideService(FileSystem.FileSystem, fileSystem));
126+
127+const bindNodeFileSystem = <A, E>(
128+ effect: Effect.Effect<A, E, FileSystem.FileSystem>,
129+): Effect.Effect<A, E, never> =>
130+ effect.pipe(Effect.provide(NodeFileSystem.layer));
131+
132+const readStateFromDisk = (
122133 context: ResolvedLocalWorkspaceContext,
123−): Promise<LocalControlPlaneState> => {
124− const path = localControlPlaneStatePath(context);
125− if (!existsSync(path)) {
126− return defaultLocalControlPlaneState();
127− }
134+): Effect.Effect<LocalControlPlaneState, LocalFileSystemError, FileSystem.FileSystem> =>
135+ Effect.gen(function* () {
136+ const fs = yield* FileSystem.FileSystem;
137+ const path = localControlPlaneStatePath(context);
138+ const exists = yield* fs.exists(path).pipe(
139+ Effect.mapError(mapFileSystemError(path, "check control plane state path")),
140+ );
141+ if (!exists) {
142+ return defaultLocalControlPlaneState();
143+ }
128144
129− const content = await readFile(path, "utf8");
130− return decodeLocalControlPlaneState(JSON.parse(content) as unknown);
131−};
145+ const content = yield* fs.readFileString(path, "utf8").pipe(
146+ Effect.mapError(mapFileSystemError(path, "read control plane state")),
147+ );
148+ const parsed = yield* Effect.try({
149+ try: () => JSON.parse(content) as unknown,
150+ catch: mapFileSystemError(path, "parse control plane state"),
151+ });
152+ return yield* decodeLocalControlPlaneState(parsed).pipe(
153+ Effect.mapError(mapFileSystemError(path, "decode control plane state")),
154+ );
155+ });
132156
133−const writeStateToDisk = async (
157+const writeStateToDisk = (
134158 context: ResolvedLocalWorkspaceContext,
135159 state: LocalControlPlaneState,
136−): Promise<void> => {
137− const path = localControlPlaneStatePath(context);
138− await mkdir(dirname(path), { recursive: true });
139− const tempPath = `${path}.${randomUUID()}.tmp`;
140− await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, {
141− encoding: "utf8",
142− mode: 0o600,
160+): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>
161+ Effect.gen(function* () {
162+ const fs = yield* FileSystem.FileSystem;
163+ const path = localControlPlaneStatePath(context);
164+ const tempPath = `${path}.${randomUUID()}.tmp`;
165+
166+ yield* fs.makeDirectory(dirname(path), { recursive: true }).pipe(
167+ Effect.mapError(mapFileSystemError(dirname(path), "create control plane state directory")),
168+ );
169+ yield* fs.writeFileString(tempPath, `${JSON.stringify(state, null, 2)}\n`, {
170+ mode: 0o600,
171+ }).pipe(
172+ Effect.mapError(mapFileSystemError(tempPath, "write control plane state")),
173+ );
174+ yield* fs.rename(tempPath, path).pipe(
175+ Effect.mapError(mapFileSystemError(path, "replace control plane state")),
176+ );
143177 });
144− await rename(tempPath, path);
145−};
146178
147179 export const loadLocalControlPlaneState = (
148180 context: ResolvedLocalWorkspaceContext,
149181 ): Effect.Effect<LocalControlPlaneState, LocalFileSystemError> =>
150− Effect.tryPromise({
151− try: () => readStateFromDisk(context),
152− catch: mapFileSystemError(
153− localControlPlaneStatePath(context),
154− "read control plane state",
155− ),
156− });
182+ bindNodeFileSystem(readStateFromDisk(context));
157183
158184 export const writeLocalControlPlaneState = (input: {
159185 context: ResolvedLocalWorkspaceContext;
160186 state: LocalControlPlaneState;
161187 }): Effect.Effect<void, LocalFileSystemError> =>
162− Effect.tryPromise({
163− try: () => writeStateToDisk(input.context, input.state),
164− catch: mapFileSystemError(
165− localControlPlaneStatePath(input.context),
166− "write control plane state",
167− ),
168− });
188+ bindNodeFileSystem(writeStateToDisk(input.context, input.state));
169189
170190 const mergeById = <T extends { id: string }>(
171191 current: readonly T[],
@@ -338,16 +358,21 @@ type StateMutationResult<A> = { · 166 unmodified lines
338358 value: A;
339359 };
340360
341−const createStateManager = (context: ResolvedLocalWorkspaceContext) => {
361+const createStateManager = (
362+ context: ResolvedLocalWorkspaceContext,
363+ fileSystem: FileSystem.FileSystem,
364+) => {
342365 let cache: LocalControlPlaneState | null = null;
343366 let mutationQueue: Promise<void> = Promise.resolve();
367+ const run = <A, E>(effect: Effect.Effect<A, E, FileSystem.FileSystem>) =>
368+ Effect.runPromise(bindFileSystem(fileSystem, effect));
344369
345370 const ensureLoaded = async (): Promise<LocalControlPlaneState> => {
346371 if (cache !== null) {
347372 return cache;
348373 }
349374
350− cache = await readStateFromDisk(context);
375+ cache = await run(readStateFromDisk(context));
351376 return cache;
352377 };
353378
@@ -381,7 +406,7 @@ const createStateManager = (context: ResolvedLocalWorkspaceContext) => { · 27 unmodified lines
381406 const result = await operation(current);
382407 cache = result.state;
383408 value = result.value;
384− await writeStateToDisk(context, cache);
409+ await run(writeStateToDisk(context, cache));
385410 } catch (cause) {
386411 failure = cause;
387412 }
@@ -409,8 +434,9 @@ const createStateManager = (context: ResolvedLocalWorkspaceContext) => { · 21 unmodified lines
409434
410435 export const createLocalControlPlaneStore = (
411436 context: ResolvedLocalWorkspaceContext,
437+ fileSystem: FileSystem.FileSystem,
412438 ) => {
413− const stateManager = createStateManager(context);
439+ const stateManager = createStateManager(context, fileSystem);
414440
415441 return {
416442 authArtifacts: {
@@ -1189,8 +1215,9 @@ export type LocalControlPlaneStore = ReturnType<typeof createLocalControlPlaneSt · 772 unmodified lines
11891215
11901216 export const createLocalControlPlanePersistence = (
11911217 context: ResolvedLocalWorkspaceContext,
1218+ fileSystem: FileSystem.FileSystem,
11921219 ): LocalControlPlanePersistence => ({
1193− rows: createLocalControlPlaneStore(context),
1220+ rows: createLocalControlPlaneStore(context, fileSystem),
11941221 close: async () => {},
11951222 });
11961223
packages/platform/control-plane/src/runtime/local/installation.test.ts+32−19
@@ -1,6 +1,6 @@
1−import { existsSync, mkdtempSync } from "node:fs";
21 import { tmpdir } from "node:os";
32 import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
44 import { NodeFileSystem } from "@effect/platform-node";
55 import { describe, expect, it } from "@effect/vitest";
66 import * as Effect from "effect/Effect";
@@ -9,40 +9,53 @@ import { createControlPlaneRuntime } from "../index"; · 2 unmodified lines
99 import { getOrProvisionLocalInstallation } from "./installation";
1010 import { resolveLocalWorkspaceContext } from "./config";
1111
12−const TEST_WORKSPACE_ROOT = mkdtempSync(join(tmpdir(), "executor-local-installation-"));
13−const TEST_HOME_CONFIG_PATH = join(TEST_WORKSPACE_ROOT, ".executor-home.jsonc");
14−const TEST_HOME_STATE_DIRECTORY = join(TEST_WORKSPACE_ROOT, ".executor-home-state");
15−
1612 const makeRuntime = Effect.acquireRelease(
17− createControlPlaneRuntime({
18− localDataDir: ":memory:",
19− workspaceRoot: TEST_WORKSPACE_ROOT,
20− homeConfigPath: TEST_HOME_CONFIG_PATH,
21− homeStateDirectory: TEST_HOME_STATE_DIRECTORY,
22− }),
23− (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
13+ Effect.gen(function* () {
14+ const fs = yield* FileSystem.FileSystem;
15+ const workspaceRoot = yield* fs.makeTempDirectory({
16+ directory: tmpdir(),
17+ prefix: "executor-local-installation-",
18+ });
19+ const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc");
20+ const homeStateDirectory = join(workspaceRoot, ".executor-home-state");
21+ const runtime = yield* createControlPlaneRuntime({
22+ localDataDir: ":memory:",
23+ workspaceRoot,
24+ homeConfigPath,
25+ homeStateDirectory,
26+ });
27+
28+ return {
29+ runtime,
30+ workspaceRoot,
31+ homeConfigPath,
32+ homeStateDirectory,
33+ };
34+ }).pipe(Effect.provide(NodeFileSystem.layer)),
35+ ({ runtime }) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
2436 );
2537
2638 describe("local-installation", () => {
2739 it.scoped("derives a stable local identity on first boot", () =>
2840 Effect.gen(function* () {
29− const runtime = yield* makeRuntime;
41+ const { runtime, workspaceRoot } = yield* makeRuntime;
3042 const installation = runtime.localInstallation;
43+ const fs = yield* FileSystem.FileSystem;
3144
3245 expect(installation.accountId).toBe("acc_local_default");
3346 expect(installation.workspaceId.startsWith("ws_local_")).toBe(true);
34− expect(existsSync(join(TEST_WORKSPACE_ROOT, ".executor", "executor.jsonc"))).toBe(false);
35− }),
47+ expect(yield* fs.exists(join(workspaceRoot, ".executor", "executor.jsonc"))).toBe(false);
48+ }).pipe(Effect.provide(NodeFileSystem.layer)),
3649 60_000,
3750 );
3851
3952 it.scoped("is idempotent when loading the default local installation", () =>
4053 Effect.gen(function* () {
41− const runtime = yield* makeRuntime;
54+ const { runtime, workspaceRoot, homeConfigPath, homeStateDirectory } = yield* makeRuntime;
4255 const context = yield* resolveLocalWorkspaceContext({
43− workspaceRoot: TEST_WORKSPACE_ROOT,
44− homeConfigPath: TEST_HOME_CONFIG_PATH,
45− homeStateDirectory: TEST_HOME_STATE_DIRECTORY,
56+ workspaceRoot,
57+ homeConfigPath,
58+ homeStateDirectory,
4659 });
4760
4861 const first = runtime.localInstallation;
packages/platform/control-plane/src/runtime/local/secret-material-providers.ts+167−124
@@ -1,8 +1,8 @@
11 import { randomUUID } from "node:crypto";
22 import { spawn } from "node:child_process";
3−import { promises as fs } from "node:fs";
4−import { lstatSync, realpathSync } from "node:fs";
53 import { isAbsolute } from "node:path";
4+import { FileSystem } from "@effect/platform";
5+import { NodeFileSystem } from "@effect/platform-node";
66
77 import {
88 type LocalConfigSecretProvider,
@@ -29,6 +29,9 @@ export const PARAMS_SECRET_PROVIDER_ID = "params"; · 20 unmodified lines
2929 export const KEYCHAIN_SECRET_PROVIDER_ID = "keychain";
3030 export const LOCAL_SECRET_PROVIDER_ID = "local";
3131
32+const toError = (cause: unknown): Error =>
33+ cause instanceof Error ? cause : new Error(String(cause));
34+
3235 export type SecretStoreProviderId =
3336 | typeof KEYCHAIN_SECRET_PROVIDER_ID
3437 | typeof LOCAL_SECRET_PROVIDER_ID;
@@ -856,151 +859,191 @@ const createSecretMaterialProviderRuntime = (input: { · 821 unmodified lines
856859 workspaceRoot: input.workspaceRoot ?? null,
857860 });
858861
859−const isRegularFilePath = (path: string, allowSymlink: boolean): boolean => {
860− const stat = lstatSync(path);
861− if (stat.isSymbolicLink()) {
862− if (!allowSymlink) {
863− return false;
862+const isRegularFilePath = (
863+ path: string,
864+ allowSymlink: boolean,
865+): Effect.Effect<boolean, Error, FileSystem.FileSystem> =>
866+ Effect.gen(function* () {
867+ const fs = yield* FileSystem.FileSystem;
868+ const stat = yield* fs.stat(path).pipe(Effect.mapError(toError));
869+ if (stat.type === "SymbolicLink") {
870+ if (!allowSymlink) {
871+ return false;
872+ }
873+
874+ const resolvedPath = yield* fs.realPath(path).pipe(Effect.mapError(toError));
875+ const targetStat = yield* fs.stat(resolvedPath).pipe(Effect.mapError(toError));
876+ return targetStat.type === "File";
864877 }
865− const targetStat = lstatSync(realpathSync(path));
866− return targetStat.isFile();
867− }
868− return stat.isFile();
869−};
870878
871−const ensureTrustedDir = (path: string, trustedDirs: readonly string[] | undefined): boolean => {
872− if (!trustedDirs || trustedDirs.length === 0) {
873− return true;
874− }
879+ return stat.type === "File";
880+ });
881+
882+const ensureTrustedDir = (
883+ path: string,
884+ trustedDirs: readonly string[] | undefined,
885+): Effect.Effect<boolean, Error, FileSystem.FileSystem> =>
886+ Effect.gen(function* () {
887+ if (!trustedDirs || trustedDirs.length === 0) {
888+ return true;
889+ }
890+
891+ const fs = yield* FileSystem.FileSystem;
892+ const real = yield* fs.realPath(path).pipe(Effect.mapError(toError));
893+
894+ for (const dir of trustedDirs) {
895+ const trusted = yield* fs.realPath(dir).pipe(Effect.mapError(toError));
896+ if (real === trusted || real.startsWith(`${trusted}/`)) {
897+ return true;
898+ }
899+ }
875900
876− const real = realpathSync(path);
877− return trustedDirs.some((dir) => {
878− const trusted = realpathSync(dir);
879− return real === trusted || real.startsWith(`${trusted}/`);
901+ return false;
880902 });
881−};
882903
883904 const readFileSecretValue = (input: {
884905 provider: Extract<LocalConfigSecretProvider, { source: "file" }>;
885906 ref: SecretRef;
886907 workspaceRoot: string;
887−}): Effect.Effect<string, Error, never> =>
888− Effect.tryPromise({
889− try: async () => {
890− const resolvedPath = resolveConfigRelativePath({
891− path: input.provider.path,
892− workspaceRoot: input.workspaceRoot,
893− });
894− const raw = await fs.readFile(resolvedPath, "utf8");
895− const mode = input.provider.mode ?? "singleValue";
896− if (mode === "singleValue") {
897− return raw.trim();
898− }
908+}): Effect.Effect<string, Error, FileSystem.FileSystem> =>
909+ Effect.gen(function* () {
910+ const fs = yield* FileSystem.FileSystem;
911+ const resolvedPath = resolveConfigRelativePath({
912+ path: input.provider.path,
913+ workspaceRoot: input.workspaceRoot,
914+ });
915+ const raw = yield* fs.readFileString(resolvedPath, "utf8").pipe(
916+ Effect.mapError(toError),
917+ );
918+ const mode = input.provider.mode ?? "singleValue";
919+ if (mode === "singleValue") {
920+ return raw.trim();
921+ }
899922
900− const parsed = JSON.parse(raw) as unknown;
901− if (input.ref.handle.startsWith("/")) {
902− const segments = input.ref.handle
903− .split("/")
904− .slice(1)
905− .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
906− let current: unknown = parsed;
907− for (const segment of segments) {
908− if (typeof current !== "object" || current === null || !(segment in current)) {
909− throw new Error(`Secret path not found in ${resolvedPath}: ${input.ref.handle}`);
923+ return yield* Effect.try({
924+ try: () => {
925+ const parsed = JSON.parse(raw) as unknown;
926+ if (input.ref.handle.startsWith("/")) {
927+ const segments = input.ref.handle
928+ .split("/")
929+ .slice(1)
930+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
931+ let current: unknown = parsed;
932+ for (const segment of segments) {
933+ if (typeof current !== "object" || current === null || !(segment in current)) {
934+ throw new Error(`Secret path not found in ${resolvedPath}: ${input.ref.handle}`);
935+ }
936+ current = (current as Record<string, unknown>)[segment];
910937 }
911− current = (current as Record<string, unknown>)[segment];
912− }
913− if (typeof current !== "string" || current.trim().length === 0) {
914− throw new Error(`Secret path did not resolve to a string: ${input.ref.handle}`);
938+ if (typeof current !== "string" || current.trim().length === 0) {
939+ throw new Error(`Secret path did not resolve to a string: ${input.ref.handle}`);
940+ }
941+ return current;
915942 }
916− return current;
917− }
918943
919− if (typeof parsed !== "object" || parsed === null) {
920− throw new Error(`JSON secret provider must resolve to an object: ${resolvedPath}`);
921− }
922− const value = (parsed as Record<string, unknown>)[input.ref.handle];
923− if (typeof value !== "string" || value.trim().length === 0) {
924− throw new Error(`Secret key not found in ${resolvedPath}: ${input.ref.handle}`);
925− }
926− return value;
927− },
928− catch: (cause) =>
929− cause instanceof Error ? cause : new Error(String(cause)),
944+ if (typeof parsed !== "object" || parsed === null) {
945+ throw new Error(`JSON secret provider must resolve to an object: ${resolvedPath}`);
946+ }
947+ const value = (parsed as Record<string, unknown>)[input.ref.handle];
948+ if (typeof value !== "string" || value.trim().length === 0) {
949+ throw new Error(`Secret key not found in ${resolvedPath}: ${input.ref.handle}`);
950+ }
951+ return value;
952+ },
953+ catch: toError,
954+ });
930955 });
931956
932957 const resolveConfiguredSecretProvider = (input: {
933958 ref: SecretRef;
934959 runtime: SecretMaterialProviderRuntime;
935−}): Effect.Effect<string, Error, never> => {
936− const providerAlias = fromConfigSecretProviderId(input.ref.providerId);
937− if (providerAlias === null) {
938− return Effect.fail(runtimeEffectError("local/secret-material-providers", `Unsupported secret provider: ${input.ref.providerId}`));
939− }
960+}): Effect.Effect<string, Error, FileSystem.FileSystem> =>
961+ Effect.gen(function* () {
962+ const providerAlias = fromConfigSecretProviderId(input.ref.providerId);
963+ if (providerAlias === null) {
964+ return yield* runtimeEffectError(
965+ "local/secret-material-providers",
966+ `Unsupported secret provider: ${input.ref.providerId}`,
967+ );
968+ }
940969
941− const provider = input.runtime.localConfig?.secrets?.providers?.[providerAlias];
942− if (!provider) {
943− return Effect.fail(
944− runtimeEffectError("local/secret-material-providers", `Config secret provider "${providerAlias}" is not configured`),
945− );
946− }
947− if (input.runtime.workspaceRoot === null) {
948− return Effect.fail(
949− runtimeEffectError("local/secret-material-providers", `Config secret provider "${providerAlias}" requires a workspace root`),
950− );
951− }
970+ const provider = input.runtime.localConfig?.secrets?.providers?.[providerAlias];
971+ if (!provider) {
972+ return yield* runtimeEffectError(
973+ "local/secret-material-providers",
974+ `Config secret provider "${providerAlias}" is not configured`,
975+ );
976+ }
977+ if (input.runtime.workspaceRoot === null) {
978+ return yield* runtimeEffectError(
979+ "local/secret-material-providers",
980+ `Config secret provider "${providerAlias}" requires a workspace root`,
981+ );
982+ }
952983
953− if (provider.source === "env") {
954− const value = ensureNonEmptyString(input.runtime.env[input.ref.handle]);
955− return value === null
956− ? Effect.fail(runtimeEffectError("local/secret-material-providers", `Environment variable ${input.ref.handle} is not set`))
957− : Effect.succeed(value);
958− }
984+ if (provider.source === "env") {
985+ const value = ensureNonEmptyString(input.runtime.env[input.ref.handle]);
986+ if (value === null) {
987+ return yield* runtimeEffectError(
988+ "local/secret-material-providers",
989+ `Environment variable ${input.ref.handle} is not set`,
990+ );
991+ }
992+ return value;
993+ }
959994
960− if (provider.source === "file") {
961− return readFileSecretValue({
962− provider,
963− ref: input.ref,
964− workspaceRoot: input.runtime.workspaceRoot,
965− });
966− }
995+ if (provider.source === "file") {
996+ return yield* readFileSecretValue({
997+ provider,
998+ ref: input.ref,
999+ workspaceRoot: input.runtime.workspaceRoot,
1000+ });
1001+ }
9671002
968− const command = provider.command.trim();
969− if (!isAbsolute(command)) {
970− return Effect.fail(
971− runtimeEffectError("local/secret-material-providers", `Exec secret provider command must be absolute: ${command}`),
972− );
973− }
974− if (!isRegularFilePath(command, provider.allowSymlinkCommand ?? false)) {
975− return Effect.fail(
976− runtimeEffectError("local/secret-material-providers", `Exec secret provider command is not an allowed regular file: ${command}`),
977− );
978− }
979− if (!ensureTrustedDir(command, provider.trustedDirs)) {
980− return Effect.fail(
981− runtimeEffectError("local/secret-material-providers", `Exec secret provider command is outside trustedDirs: ${command}`),
1003+ const command = provider.command.trim();
1004+ if (!isAbsolute(command)) {
1005+ return yield* runtimeEffectError(
1006+ "local/secret-material-providers",
1007+ `Exec secret provider command must be absolute: ${command}`,
1008+ );
1009+ }
1010+ const isRegularCommand = yield* isRegularFilePath(
1011+ command,
1012+ provider.allowSymlinkCommand ?? false,
9821013 );
983− }
1014+ if (!isRegularCommand) {
1015+ return yield* runtimeEffectError(
1016+ "local/secret-material-providers",
1017+ `Exec secret provider command is not an allowed regular file: ${command}`,
1018+ );
1019+ }
1020+ const isTrustedCommand = yield* ensureTrustedDir(command, provider.trustedDirs);
1021+ if (!isTrustedCommand) {
1022+ return yield* runtimeEffectError(
1023+ "local/secret-material-providers",
1024+ `Exec secret provider command is outside trustedDirs: ${command}`,
1025+ );
1026+ }
9841027
985− return runCommand({
986− command,
987− args: [...(provider.args ?? []), input.ref.handle],
988− env: {
989− ...input.runtime.env,
990− ...provider.env,
991− },
992− operation: `config-secret.get:${providerAlias}`,
993− }).pipe(
994− Effect.flatMap((result) =>
995− ensureCommandSuccess({
996− result,
997− operation: `config-secret.get:${providerAlias}`,
998− message: "Failed resolving configured exec secret",
999− }),
1000− ),
1001− Effect.map((result) => result.stdout.trimEnd()),
1002− );
1003−};
1028+ return yield* runCommand({
1029+ command,
1030+ args: [...(provider.args ?? []), input.ref.handle],
1031+ env: {
1032+ ...input.runtime.env,
1033+ ...provider.env,
1034+ },
1035+ operation: `config-secret.get:${providerAlias}`,
1036+ }).pipe(
1037+ Effect.flatMap((result) =>
1038+ ensureCommandSuccess({
1039+ result,
1040+ operation: `config-secret.get:${providerAlias}`,
1041+ message: "Failed resolving configured exec secret",
1042+ }),
1043+ ),
1044+ Effect.map((result) => result.stdout.trimEnd()),
1045+ );
1046+ });
10041047
10051048 export const createDefaultSecretMaterialResolver = (input: {
10061049 rows: ControlPlaneStoreShape;
@@ -1037,7 +1080,7 @@ export const createDefaultSecretMaterialResolver = (input: { · 30 unmodified lines
10371080 context: context ?? {},
10381081 runtime,
10391082 });
1040− });
1083+ }).pipe(Effect.provide(NodeFileSystem.layer));
10411084 };
10421085
10431086 export const createDefaultSecretMaterialStorer = (input: {
packages/platform/control-plane/src/runtime/local/source-artifacts.test.ts+32−21
@@ -1,6 +1,6 @@
1−import { existsSync, mkdtempSync, readFileSync } from "node:fs";
21 import { tmpdir } from "node:os";
32 import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
44 import { NodeFileSystem } from "@effect/platform-node";
55 import { describe, expect, it } from "@effect/vitest";
66 import * as Effect from "effect/Effect";
@@ -17,20 +17,30 @@ import { createCatalogImportMetadata } from "@executor/source-core"; · 10 unmodified lines
1717 import { createGraphqlCatalogFragment } from "@executor/source-graphql";
1818 import { createOpenApiCatalogFragment } from "@executor/source-openapi";
1919
20−const makeContext = (): ResolvedLocalWorkspaceContext => {
21− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-artifacts-"));
22− return {
23− cwd: workspaceRoot,
24− workspaceRoot,
25− workspaceName: "executor-artifacts",
26− configDirectory: join(workspaceRoot, ".executor"),
27− projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
28− homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
29− homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
30− artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
31− stateDirectory: join(workspaceRoot, ".executor", "state"),
32− };
33−};
20+const makeContext = (): Effect.Effect<
21+ ResolvedLocalWorkspaceContext,
22+ never,
23+ FileSystem.FileSystem
24+> =>
25+ Effect.gen(function* () {
26+ const fs = yield* FileSystem.FileSystem;
27+ const workspaceRoot = yield* fs.makeTempDirectory({
28+ directory: tmpdir(),
29+ prefix: "executor-artifacts-",
30+ }).pipe(Effect.orDie);
31+
32+ return {
33+ cwd: workspaceRoot,
34+ workspaceRoot,
35+ workspaceName: "executor-artifacts",
36+ configDirectory: join(workspaceRoot, ".executor"),
37+ projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
38+ homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
39+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
40+ artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
41+ stateDirectory: join(workspaceRoot, ".executor", "state"),
42+ };
43+ });
3444
3545 const makeSource = (): Source => ({
3646 id: SourceIdSchema.make("src_test"),
@@ -148,7 +158,8 @@ const makeGraphqlArtifact = () => { · 111 unmodified lines
148158 describe("local-source-artifacts", () => {
149159 it.effect("writes canonical uncompressed artifacts and reads them back", () =>
150160 Effect.gen(function* () {
151− const context = makeContext();
161+ const fs = yield* FileSystem.FileSystem;
162+ const context = yield* makeContext();
152163 const artifact = makeArtifact();
153164 const path = join(context.artifactsDirectory, "sources", "src_test.json");
154165 const rawDocumentPath = join(
@@ -165,11 +176,11 @@ describe("local-source-artifacts", () => { · 10 unmodified lines
165176 artifact,
166177 });
167178
168− expect(existsSync(path)).toBe(true);
169− expect(readFileSync(path, "utf8").startsWith("{")).toBe(true);
170− expect(existsSync(rawDocumentPath)).toBe(true);
179+ expect(yield* fs.exists(path)).toBe(true);
180+ expect((yield* fs.readFileString(path, "utf8")).startsWith("{")).toBe(true);
181+ expect(yield* fs.exists(rawDocumentPath)).toBe(true);
171182
172− const persistedArtifact = JSON.parse(readFileSync(path, "utf8"));
183+ const persistedArtifact = JSON.parse(yield* fs.readFileString(path, "utf8"));
173184 const persistedDocument = Object.values(
174185 persistedArtifact.snapshot.catalog.documents,
175186 )[0] as {
@@ -220,7 +231,7 @@ describe("local-source-artifacts", () => { · 44 unmodified lines
220231 "preserves GraphQL execution metadata across a write/read round-trip",
221232 () =>
222233 Effect.gen(function* () {
223− const context = makeContext();
234+ const context = yield* makeContext();
224235 const artifact = makeGraphqlArtifact();
225236
226237 yield* writeLocalSourceArtifact({
packages/platform/control-plane/src/runtime/local/tools.ts+7−7
@@ -1,7 +1,7 @@
11 import { createHash } from "node:crypto";
2−import { existsSync } from "node:fs";
3−import { dirname, extname, join, relative, resolve, sep } from "node:path";
4−import { fileURLToPath, pathToFileURL } from "node:url";
2+import { createRequire } from "node:module";
3+import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
4+import { pathToFileURL } from "node:url";
55 import { FileSystem } from "@effect/platform";
66 import {
77 createToolCatalogFromTools,
@@ -332,12 +332,12 @@ const transpileSourceFile = (input: { · 324 unmodified lines
332332 });
333333
334334 const resolveExecutorNodeModulesDirectory = (): string | null => {
335− let current = dirname(fileURLToPath(import.meta.url));
335+ const require = createRequire(import.meta.url);
336+ let current = dirname(require.resolve("typescript"));
336337
337338 while (true) {
338− const candidate = join(current, "node_modules");
339− if (existsSync(candidate)) {
340− return candidate;
339+ if (basename(current) === "node_modules") {
340+ return current;
341341 }
342342
343343 const parent = dirname(current);
packages/platform/control-plane/src/runtime/sources/source-adapter-fixture-matrix.test.ts+19−9
@@ -1,13 +1,15 @@
1−import { readFileSync } from "node:fs";
21 import { createServer } from "node:http";
2+import { fileURLToPath } from "node:url";
33
44 import {
5+ FileSystem,
56 HttpApi,
67 HttpApiEndpoint,
78 HttpApiGroup,
89 HttpApiSchema,
910 OpenApi,
1011 } from "@effect/platform";
12+import { NodeFileSystem } from "@effect/platform-node";
1113 import {
1214 buildOpenApiToolPresentation,
1315 compileOpenApiToolDefinitions,
@@ -57,8 +59,16 @@ import { createOpenApiCatalogSnapshot } from "@executor/source-openapi"; · 43 unmodified lines
5759
5860 const FIXTURE_WORKSPACE_ID = WorkspaceIdSchema.make("ws_source_fixture_matrix");
5961
60−const readFixture = (name: string): string =>
61− readFileSync(new URL(`../fixtures/${name}`, import.meta.url), "utf8");
62+const readFixture = (name: string) =>
63+ FileSystem.FileSystem.pipe(
64+ Effect.flatMap((fs) =>
65+ fs.readFileString(
66+ fileURLToPath(new URL(`../fixtures/${name}`, import.meta.url)),
67+ "utf8",
68+ )
69+ ),
70+ Effect.provide(NodeFileSystem.layer),
71+ );
6272
6373 const makeSource = (input: {
6474 id: string;
@@ -324,7 +334,7 @@ describe("source adapter fixture matrix", () => { · 259 unmodified lines
324334 "imports the raw recorded Vercel OpenAPI spec into IR and discover projections",
325335 () =>
326336 Effect.gen(function* () {
327− const specText = readFixture("vercel-openapi.json");
337+ const specText = yield* readFixture("vercel-openapi.json");
328338 const source = makeSource({
329339 id: "src_vercel_fixture",
330340 name: "Vercel",
@@ -393,7 +403,7 @@ describe("source adapter fixture matrix", () => { · 62 unmodified lines
393403 "imports the raw recorded Neon OpenAPI spec into IR with resolved request body schemas",
394404 () =>
395405 Effect.gen(function* () {
396− const specText = readFixture("neon-openapi.json");
406+ const specText = yield* readFixture("neon-openapi.json");
397407 const source = makeSource({
398408 id: "src_neon_fixture",
399409 name: "Neon API",
@@ -935,7 +945,7 @@ describe("source adapter fixture matrix", () => { · 535 unmodified lines
935945 "imports the raw recorded Google Sheets discovery document into IR and discover projections",
936946 () =>
937947 Effect.gen(function* () {
938− const documentText = readFixture("google-sheets-discovery.json");
948+ const documentText = yield* readFixture("google-sheets-discovery.json");
939949 const source = makeSource({
940950 id: "src_google_sheets_fixture",
941951 name: "Google Sheets",
@@ -1008,7 +1018,7 @@ describe("source adapter fixture matrix", () => { · 66 unmodified lines
10081018 "projects every raw recorded Google Sheets discovery method into schemas and capabilities",
10091019 () =>
10101020 Effect.gen(function* () {
1011− const documentText = readFixture("google-sheets-discovery.json");
1021+ const documentText = yield* readFixture("google-sheets-discovery.json");
10121022 const source = makeSource({
10131023 id: "src_google_sheets_coverage",
10141024 name: "Google Sheets",
@@ -1070,7 +1080,7 @@ describe("source adapter fixture matrix", () => { · 55 unmodified lines
10701080 "imports the raw recorded Linear GraphQL introspection dump into IR and resolves nested input refs",
10711081 () =>
10721082 Effect.gen(function* () {
1073− const documentText = readFixture("linear-introspection.json");
1083+ const documentText = yield* readFixture("linear-introspection.json");
10741084 const source = makeSource({
10751085 id: "src_linear_fixture",
10761086 name: "Linear GraphQL",
@@ -1370,7 +1380,7 @@ describe("source adapter fixture matrix", () => { · 293 unmodified lines
13701380 "projects every raw recorded Linear GraphQL tool into schemas and capabilities",
13711381 () =>
13721382 Effect.gen(function* () {
1373− const documentText = readFixture("linear-introspection.json");
1383+ const documentText = yield* readFixture("linear-introspection.json");
13741384 const source = makeSource({
13751385 id: "src_linear_coverage",
13761386 name: "Linear GraphQL",
packages/platform/control-plane/src/runtime/sources/source-auth-service.ts+156−15
@@ -1,6 +1,7 @@
11 import {
22 createSdkMcpConnector,
33 discoverMcpToolsFromConnector,
4+ isMcpStdioTransport,
45 type McpDiscoveryElicitationContext,
56 } from "@executor/source-mcp";
67 import {
@@ -125,6 +126,73 @@ const defaultNamespaceFromName = (name: string): string => { · 118 unmodified lines
125126 return normalized.length > 0 ? normalized : "source";
126127 };
127128
129+const defaultSourceNameFromCommand = (command: string): string => {
130+ const segments = command
131+ .split(/[\\/]+/)
132+ .map((segment) => segment.trim())
133+ .filter((segment) => segment.length > 0);
134+
135+ return segments[segments.length - 1] ?? command;
136+};
137+
138+const normalizeStringArray = (
139+ value: ReadonlyArray<string> | null | undefined,
140+): string[] | null => {
141+ if (!value || value.length === 0) {
142+ return null;
143+ }
144+
145+ const normalized = value
146+ .map((entry) => entry.trim())
147+ .filter((entry) => entry.length > 0);
148+
149+ return normalized.length > 0 ? normalized : null;
150+};
151+
152+const slugifySourceLabel = (value: string): string => {
153+ const normalized = value
154+ .trim()
155+ .toLowerCase()
156+ .replace(/[^a-z0-9]+/g, "-")
157+ .replace(/^-+|-+$/g, "");
158+
159+ return normalized.length > 0 ? normalized : "mcp";
160+};
161+
162+const createSyntheticMcpStdioEndpoint = (input: {
163+ endpoint?: string | null;
164+ name?: string | null;
165+ command?: string | null;
166+}): string => {
167+ const label =
168+ trimOrNull(input.name)
169+ ?? trimOrNull(input.endpoint)
170+ ?? trimOrNull(input.command)
171+ ?? "mcp";
172+
173+ return `stdio://local/${slugifySourceLabel(label)}`;
174+};
175+
176+const normalizeMcpEndpoint = (input: {
177+ endpoint?: string | null;
178+ transport?: SourceTransport | null;
179+ command?: string | null;
180+ name?: string | null;
181+}): string => {
182+ if (isMcpStdioTransport({ transport: input.transport ?? undefined, command: input.command ?? undefined })) {
183+ return normalizeEndpoint(
184+ trimOrNull(input.endpoint) ?? createSyntheticMcpStdioEndpoint(input),
185+ );
186+ }
187+
188+ const endpoint = trimOrNull(input.endpoint);
189+ if (endpoint === null) {
190+ throw new Error("Endpoint is required.");
191+ }
192+
193+ return normalizeEndpoint(endpoint);
194+};
195+
128196 const resolveSourceCredentialOauthCompleteUrl = (input: {
129197 baseUrl: string;
130198 workspaceId: WorkspaceId;
@@ -152,10 +220,10 @@ const resolveWorkspaceProviderOauthCompleteUrl = (input: { · 21 unmodified lines
152220 input.baseUrl,
153221 ).toString();
154222
155−const normalizeEndpoint = (endpoint: string): string => {
223+function normalizeEndpoint(endpoint: string): string {
156224 const url = new URL(endpoint.trim());
157225 return url.toString();
158−};
226+}
159227
160228 const defaultGoogleDiscoveryUrl = (service: string, version: string): string =>
161229 `https://www.googleapis.com/discovery/v1/apis/${encodeURIComponent(service)}/${encodeURIComponent(version)}/rest`;
@@ -226,6 +294,10 @@ const probeMcpSourceWithoutAuth = ( · 64 unmodified lines
226294 transport: bindingState.transport ?? undefined,
227295 queryParams: bindingState.queryParams ?? undefined,
228296 headers: bindingState.headers ?? undefined,
297+ command: bindingState.command ?? undefined,
298+ args: bindingState.args ?? undefined,
299+ env: bindingState.env ?? undefined,
300+ cwd: bindingState.cwd ?? undefined,
229301 }),
230302 catch: (cause) =>
231303 cause instanceof Error ? cause : new Error(String(cause)),
@@ -497,9 +569,16 @@ export type ExecutorAddSourceInput = · 265 unmodified lines
497569 actorAccountId?: AccountId | null;
498570 executionId: SourceAuthSession["executionId"];
499571 interactionId: SourceAuthSession["interactionId"];
500− endpoint: string;
572+ endpoint?: string | null;
501573 name?: string | null;
502574 namespace?: string | null;
575+ transport?: SourceTransport | null;
576+ queryParams?: StringMap | null;
577+ headers?: StringMap | null;
578+ command?: string | null;
579+ args?: ReadonlyArray<string> | null;
580+ env?: StringMap | null;
581+ cwd?: string | null;
503582 }
504583 | {
505584 kind: "openapi";
@@ -566,13 +645,17 @@ export type ConnectMcpSourceInput = { · 60 unmodified lines
566645 workspaceId: WorkspaceId;
567646 actorAccountId?: AccountId | null;
568647 sourceId?: Source["id"] | null;
569− endpoint: string;
648+ endpoint?: string | null;
570649 name?: string | null;
571650 namespace?: string | null;
572651 enabled?: boolean;
573− transport?: SourceTransport;
652+ transport?: SourceTransport | null;
574653 queryParams?: StringMap | null;
575654 headers?: StringMap | null;
655+ command?: string | null;
656+ args?: ReadonlyArray<string> | null;
657+ env?: StringMap | null;
658+ cwd?: string | null;
576659 baseUrl?: string | null;
577660 };
578661
@@ -1549,18 +1632,21 @@ const connectMcpSourceInternal = (input: { · 970 unmodified lines
15491632 sourceId?: Source["id"] | null;
15501633 executionId?: SourceAuthSession["executionId"];
15511634 interactionId?: SourceAuthSession["interactionId"];
1552− endpoint: string;
1635+ endpoint?: string | null;
15531636 name?: string | null;
15541637 namespace?: string | null;
15551638 enabled?: boolean;
1556− transport?: SourceTransport;
1639+ transport?: SourceTransport | null;
15571640 queryParams?: StringMap | null;
15581641 headers?: StringMap | null;
1642+ command?: string | null;
1643+ args?: ReadonlyArray<string> | null;
1644+ env?: StringMap | null;
1645+ cwd?: string | null;
15591646 mcpDiscoveryElicitation?: McpDiscoveryElicitationContext;
15601647 resolveSecretMaterial: ResolveSecretMaterial;
15611648 }): Effect.Effect<McpSourceConnectResult, Error, WorkspaceStorageServices> =>
15621649 Effect.gen(function* () {
1563− const normalizedEndpoint = normalizeEndpoint(input.endpoint);
15641650 const existing = yield* (
15651651 input.sourceId
15661652 ? input.sourceStore.loadSourceById({
@@ -1587,21 +1673,57 @@ const connectMcpSourceInternal = (input: { · 20 unmodified lines
15871673 )
15881674 );
15891675
1676+ const existingBinding = existing
1677+ ? yield* sourceBindingStateFromSource(existing)
1678+ : null;
1679+ const chosenCommand =
1680+ input.command !== undefined ? trimOrNull(input.command) : (existingBinding?.command ?? null);
1681+ const chosenTransport = input.transport !== undefined && input.transport !== null
1682+ ? input.transport
1683+ : isMcpStdioTransport({
1684+ transport: existingBinding?.transport ?? undefined,
1685+ command: chosenCommand ?? undefined,
1686+ })
1687+ ? "stdio"
1688+ : (existingBinding?.transport ?? "auto");
1689+ if (chosenTransport === "stdio" && chosenCommand === null) {
1690+ return yield* runtimeEffectError("sources/source-auth-service", "MCP stdio transport requires a command");
1691+ }
1692+ const normalizedEndpoint = normalizeMcpEndpoint({
1693+ endpoint: input.endpoint ?? existing?.endpoint ?? null,
1694+ transport: chosenTransport,
1695+ command: chosenCommand,
1696+ name: input.name ?? existing?.name ?? null,
1697+ });
15901698 const chosenName =
1591− trimOrNull(input.name) ?? existing?.name ?? defaultSourceNameFromEndpoint(normalizedEndpoint);
1699+ trimOrNull(input.name)
1700+ ?? existing?.name
1701+ ?? (chosenTransport === "stdio" && chosenCommand !== null
1702+ ? defaultSourceNameFromCommand(chosenCommand)
1703+ : defaultSourceNameFromEndpoint(normalizedEndpoint));
15921704 const chosenNamespace =
15931705 trimOrNull(input.namespace)
15941706 ?? existing?.namespace
15951707 ?? defaultNamespaceFromName(chosenName);
15961708 const chosenEnabled = input.enabled ?? existing?.enabled ?? true;
1597− const existingBinding = existing
1598− ? yield* sourceBindingStateFromSource(existing)
1599− : null;
1600− const chosenTransport = input.transport ?? existingBinding?.transport ?? "auto";
16011709 const chosenQueryParams =
1602− input.queryParams !== undefined ? input.queryParams : (existingBinding?.queryParams ?? null);
1710+ chosenTransport === "stdio"
1711+ ? null
1712+ : input.queryParams !== undefined
1713+ ? input.queryParams
1714+ : (existingBinding?.queryParams ?? null);
16031715 const chosenHeaders =
1604− input.headers !== undefined ? input.headers : (existingBinding?.headers ?? null);
1716+ chosenTransport === "stdio"
1717+ ? null
1718+ : input.headers !== undefined
1719+ ? input.headers
1720+ : (existingBinding?.headers ?? null);
1721+ const chosenArgs =
1722+ input.args !== undefined ? normalizeStringArray(input.args) : (existingBinding?.args ?? null);
1723+ const chosenEnv =
1724+ input.env !== undefined ? input.env : (existingBinding?.env ?? null);
1725+ const chosenCwd =
1726+ input.cwd !== undefined ? trimOrNull(input.cwd) : (existingBinding?.cwd ?? null);
16051727 const now = Date.now();
16061728
16071729 const draftSource = existing
@@ -1617,6 +1739,10 @@ const connectMcpSourceInternal = (input: { · 9 unmodified lines
16171739 transport: chosenTransport,
16181740 queryParams: chosenQueryParams,
16191741 headers: chosenHeaders,
1742+ command: chosenCommand,
1743+ args: chosenArgs,
1744+ env: chosenEnv,
1745+ cwd: chosenCwd,
16201746 },
16211747 importAuthPolicy: "reuse_runtime",
16221748 importAuth: { kind: "none" },
@@ -1639,6 +1765,10 @@ const connectMcpSourceInternal = (input: { · 16 unmodified lines
16391765 transport: chosenTransport,
16401766 queryParams: chosenQueryParams,
16411767 headers: chosenHeaders,
1768+ command: chosenCommand,
1769+ args: chosenArgs,
1770+ env: chosenEnv,
1771+ cwd: chosenCwd,
16421772 },
16431773 importAuthPolicy: "reuse_runtime",
16441774 importAuth: { kind: "none" },
@@ -2681,6 +2811,13 @@ const createRuntimeSourceConnectionService = ( · 1036 unmodified lines
26812811 endpoint: sourceInput.endpoint,
26822812 name: sourceInput.name,
26832813 namespace: sourceInput.namespace,
2814+ transport: sourceInput.transport,
2815+ queryParams: sourceInput.queryParams,
2816+ headers: sourceInput.headers,
2817+ command: sourceInput.command,
2818+ args: sourceInput.args,
2819+ env: sourceInput.env,
2820+ cwd: sourceInput.cwd,
26842821 mcpDiscoveryElicitation: options?.mcpDiscoveryElicitation,
26852822 baseUrl: options?.baseUrl,
26862823 resolveSecretMaterial: input.resolveSecretMaterial,
@@ -2720,6 +2857,10 @@ const createRuntimeSourceConnectionService = ( · 33 unmodified lines
27202857 transport: sourceInput.transport,
27212858 queryParams: sourceInput.queryParams,
27222859 headers: sourceInput.headers,
2860+ command: sourceInput.command,
2861+ args: sourceInput.args,
2862+ env: sourceInput.env,
2863+ cwd: sourceInput.cwd,
27232864 baseUrl: sourceInput.baseUrl,
27242865 resolveSecretMaterial: input.resolveSecretMaterial,
27252866 }).pipe(
packages/platform/server/package.json+1−0
@@ -11,6 +11,7 @@ · 10 unmodified lines
1111 "dependencies": {
1212 "@effect/opentelemetry": "^0.61.0",
1313 "@effect/platform": "catalog:",
14+ "@effect/platform-node": "catalog:",
1415 "@executor/control-plane": "workspace:*",
1516 "@executor/executor-mcp": "workspace:*",
1617 "@opentelemetry/api": "^1.9.0",
packages/platform/server/src/index.ts+63−37
@@ -1,4 +1,3 @@
1−import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
21 import {
32 createServer as createNodeServer,
43 type IncomingMessage,
@@ -7,7 +6,8 @@ import { · 2 unmodified lines
76 import type { AddressInfo } from "node:net";
87 import { dirname, extname, resolve } from "node:path";
98 import { Readable } from "node:stream";
10−import { HttpApiBuilder, HttpServer } from "@effect/platform";
9+import { FileSystem, HttpApiBuilder, HttpServer } from "@effect/platform";
10+import { NodeFileSystem } from "@effect/platform-node";
1111 import {
1212 createControlPlaneApiLayer,
1313 createControlPlaneRuntime,
@@ -147,9 +147,13 @@ const safeFilePath = (assetsDir: string, pathname: string): string | null => { · 133 unmodified lines
147147 return target.startsWith(root) ? target : null;
148148 };
149149
150−const isRegularFile = async (path: string): Promise<boolean> => {
150+const isRegularFile = async (
151+ fileSystem: FileSystem.FileSystem,
152+ path: string,
153+): Promise<boolean> => {
151154 try {
152− return (await stat(path)).isFile();
155+ const info = await Effect.runPromise(fileSystem.stat(path));
156+ return info.type === "File";
153157 } catch {
154158 return false;
155159 }
@@ -178,11 +182,12 @@ const contentTypeForPath = (path: string): string => · 22 unmodified lines
178182 contentTypeByExtension[extname(path).toLowerCase()] ?? "application/octet-stream";
179183
180184 const readResponseFile = async (
185+ fileSystem: FileSystem.FileSystem,
181186 path: string,
182187 contentType?: string,
183188 ): Promise<Response> => {
184− const body = await readFile(path);
185− return new Response(body, {
189+ const body = await Effect.runPromise(fileSystem.readFile(path));
190+ return new Response(Buffer.from(body), {
186191 headers: {
187192 "content-type": contentType ?? contentTypeForPath(path),
188193 },
@@ -254,7 +259,11 @@ const writeNodeResponse = async ( · 65 unmodified lines
254259 const wantsHtml = (request: Request): boolean =>
255260 request.headers.get("accept")?.includes("text/html") ?? false;
256261
257−const serveUiAsset = async (request: Request, ui: StaticUiOptions): Promise<Response | null> => {
262+const serveUiAsset = async (
263+ fileSystem: FileSystem.FileSystem,
264+ request: Request,
265+ ui: StaticUiOptions,
266+): Promise<Response | null> => {
258267 const url = new URL(request.url);
259268
260269 if (ui.devServerUrl) {
@@ -267,8 +276,8 @@ const serveUiAsset = async (request: Request, ui: StaticUiOptions): Promise<Resp · 6 unmodified lines
267276 }
268277
269278 const candidatePath = safeFilePath(ui.assetsDir, url.pathname);
270− if (candidatePath && await isRegularFile(candidatePath)) {
271− return readResponseFile(candidatePath);
279+ if (candidatePath && await isRegularFile(fileSystem, candidatePath)) {
280+ return readResponseFile(fileSystem, candidatePath);
272281 }
273282
274283 const shouldServeIndex =
@@ -281,11 +290,11 @@ const serveUiAsset = async (request: Request, ui: StaticUiOptions): Promise<Resp · 6 unmodified lines
281290 }
282291
283292 const indexPath = resolve(ui.assetsDir, "index.html");
284− if (!(await isRegularFile(indexPath))) {
293+ if (!(await isRegularFile(fileSystem, indexPath))) {
285294 return null;
286295 }
287296
288− return readResponseFile(indexPath, "text/html; charset=utf-8");
297+ return readResponseFile(fileSystem, indexPath, "text/html; charset=utf-8");
289298 };
290299
291300 const isApiRequest = (request: Request): boolean => {
@@ -297,13 +306,17 @@ export const createLocalExecutorRequestHandler = ( · 5 unmodified lines
297306 options: StartLocalExecutorServerOptions = {},
298307 ): Effect.Effect<LocalExecutorRequestHandler, Error, Scope.Scope> =>
299308 Effect.gen(function* () {
309+ const fileSystem = yield* FileSystem.FileSystem;
300310 const requestedLocalDataDir = options.localDataDir ?? DEFAULT_LOCAL_DATA_DIR;
301311
302312 if (requestedLocalDataDir !== ":memory:") {
303− yield* Effect.tryPromise({
304− try: () => mkdir(dirname(requestedLocalDataDir), { recursive: true }),
305− catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
306− });
313+ yield* fileSystem.makeDirectory(dirname(requestedLocalDataDir), {
314+ recursive: true,
315+ }).pipe(
316+ Effect.mapError((cause) =>
317+ cause instanceof Error ? cause : new Error(String(cause)),
318+ ),
319+ );
307320 }
308321
309322 let baseUrlRef: string | undefined;
@@ -351,12 +364,13 @@ export const createLocalExecutorRequestHandler = ( · 41 unmodified lines
351364 baseUrlRef = baseUrl;
352365 },
353366 } satisfies LocalExecutorRequestHandler;
354− });
367+ }).pipe(Effect.provide(NodeFileSystem.layer));
355368
356369 export const createLocalExecutorServer = (
357370 options: StartLocalExecutorServerOptions = {},
358371 ): Effect.Effect<LocalExecutorServer, Error, Scope.Scope> =>
359372 Effect.gen(function* () {
373+ const fileSystem = yield* FileSystem.FileSystem;
360374 const host = options.host ?? DEFAULT_SERVER_HOST;
361375 const port = options.port ?? DEFAULT_SERVER_PORT;
362376 const requestHandler = yield* createLocalExecutorRequestHandler(options);
@@ -370,7 +384,7 @@ export const createLocalExecutorServer = ( · 7 unmodified lines
370384 const request = toWebRequest(nodeRequest);
371385 const response = isApiRequest(request)
372386 ? await requestHandler.handleApiRequest(request)
373− : await serveUiAsset(request, options.ui ?? {}) ?? new Response("Not Found", { status: 404 });
387+ : await serveUiAsset(fileSystem, request, options.ui ?? {}) ?? new Response("Not Found", { status: 404 });
374388 await writeNodeResponse(nodeResponse, response);
375389 })().catch((cause) => {
376390 nodeResponse.statusCode = 500;
@@ -417,36 +431,48 @@ export const createLocalExecutorServer = ( · 40 unmodified lines
417431 port: resolvedAddress.port,
418432 baseUrl,
419433 } satisfies LocalExecutorServer;
420− });
434+ }).pipe(Effect.provide(NodeFileSystem.layer));
421435
422436 export const runLocalExecutorServer = (
423437 options: StartLocalExecutorServerOptions = {},
424438 ): Effect.Effect<void, Error, never> =>
425439 Effect.scoped(
426440 Effect.gen(function* () {
441+ const fileSystem = yield* FileSystem.FileSystem;
427442 const server = yield* createLocalExecutorServer(options);
428443 const pidFile = options.pidFile ?? DEFAULT_SERVER_PID_FILE;
429444
430445 yield* Effect.acquireRelease(
431− Effect.tryPromise({
432− try: async () => {
433− await mkdir(dirname(pidFile), { recursive: true });
434− await writeFile(pidFile, JSON.stringify({
435− pid: process.pid,
436− port: server.port,
437− host: server.host,
438− baseUrl: server.baseUrl,
439− startedAt: Date.now(),
440− logFile: DEFAULT_SERVER_LOG_FILE,
441− }, null, 2));
442− },
443− catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
446+ Effect.gen(function* () {
447+ yield* fileSystem.makeDirectory(dirname(pidFile), {
448+ recursive: true,
449+ }).pipe(
450+ Effect.mapError((cause) =>
451+ cause instanceof Error ? cause : new Error(String(cause)),
452+ ),
453+ );
454+ yield* fileSystem.writeFileString(pidFile, JSON.stringify({
455+ pid: process.pid,
456+ port: server.port,
457+ host: server.host,
458+ baseUrl: server.baseUrl,
459+ startedAt: Date.now(),
460+ logFile: DEFAULT_SERVER_LOG_FILE,
461+ }, null, 2)).pipe(
462+ Effect.mapError((cause) =>
463+ cause instanceof Error ? cause : new Error(String(cause)),
464+ ),
465+ );
444466 }),
445− () => Effect.tryPromise({
446− try: () => rm(pidFile, { force: true }),
447− catch: (cause) =>
448− cause instanceof Error ? cause : new Error(String(cause ?? "pid file cleanup failed")),
449− }).pipe(Effect.orDie),
467+ () =>
468+ fileSystem.remove(pidFile, { force: true }).pipe(
469+ Effect.mapError((cause) =>
470+ cause instanceof Error
471+ ? cause
472+ : new Error(String(cause ?? "pid file cleanup failed")),
473+ ),
474+ Effect.orDie,
475+ ),
450476 );
451477
452478 console.error(`executor server listening on ${server.baseUrl}`);
@@ -462,4 +488,4 @@ export const runLocalExecutorServer = ( · 9 unmodified lines
462488 });
463489 });
464490 }),
465− );
491+ ).pipe(Effect.provide(NodeFileSystem.layer));
packages/sources/openapi/src/real-spec-coverage.test.ts+15−6
@@ -1,5 +1,7 @@
1−import { readFileSync } from "node:fs";
1+import { fileURLToPath } from "node:url";
22
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
35 import { describe, expect, it } from "@effect/vitest";
46 import * as Effect from "effect/Effect";
57
@@ -13,10 +15,17 @@ import type { · 7 unmodified lines
1315 OpenApiToolManifest,
1416 } from "./types";
1517
16−const readFixture = (name: string): string =>
17− readFileSync(
18− new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url),
19− "utf8",
18+const readFixture = (name: string) =>
19+ FileSystem.FileSystem.pipe(
20+ Effect.flatMap((fs) =>
21+ fs.readFileString(
22+ fileURLToPath(
23+ new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url),
24+ ),
25+ "utf8",
26+ )
27+ ),
28+ Effect.provide(NodeFileSystem.layer),
2029 );
2130
2231 const asObject = (value: unknown): Record<string, unknown> =>
@@ -322,7 +331,7 @@ describe("openapi real spec coverage", () => { · 299 unmodified lines
322331 `projects every ${fixture.name} operation with request/response schemas that match the raw spec`,
323332 () =>
324333 Effect.gen(function* () {
325− const specText = readFixture(fixture.filename);
334+ const specText = yield* readFixture(fixture.filename);
326335 const document = parseOpenApiDocument(specText);
327336 const manifest = yield* extractOpenApiManifest(
328337 fixture.name.toLowerCase(),
packages/sources/openapi/src/tool-presentation.test.ts+17−8
@@ -1,5 +1,7 @@
1−import { readFileSync } from "node:fs";
1+import { fileURLToPath } from "node:url";
22
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
35 import { describe, expect, it } from "@effect/vitest";
46 import * as Effect from "effect/Effect";
57
@@ -8,10 +10,17 @@ import { compileOpenApiToolDefinitions } from "./definitions"; · 2 unmodified lines
810 import { extractOpenApiManifest } from "./extraction";
911 import { buildOpenApiToolPresentation } from "./tool-presentation";
1012
11−const readFixture = (name: string): string =>
12− readFileSync(
13− new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url),
14− "utf8",
13+const readFixture = (name: string) =>
14+ FileSystem.FileSystem.pipe(
15+ Effect.flatMap((fs) =>
16+ fs.readFileString(
17+ fileURLToPath(
18+ new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url),
19+ ),
20+ "utf8",
21+ )
22+ ),
23+ Effect.provide(NodeFileSystem.layer),
1524 );
1625
1726 describe("buildOpenApiToolPresentation", () => {
@@ -201,7 +210,7 @@ describe("buildOpenApiToolPresentation", () => { · 183 unmodified lines
201210 "resolves request and response schemas from ref hints for the real Neon OpenAPI spec",
202211 () =>
203212 Effect.gen(function* () {
204− const specText = readFixture("neon-openapi.json");
213+ const specText = yield* readFixture("neon-openapi.json");
205214 const manifest = yield* extractOpenApiManifest("neon", specText);
206215 const definition = compileOpenApiToolDefinitions(manifest).find(
207216 (candidate) => candidate.toolId === "apiKey.createApiKey",
@@ -254,7 +263,7 @@ describe("buildOpenApiToolPresentation", () => { · 46 unmodified lines
254263 "preserves response wrappers for response-only Neon operations",
255264 () =>
256265 Effect.gen(function* () {
257− const specText = readFixture("neon-openapi.json");
266+ const specText = yield* readFixture("neon-openapi.json");
258267 const manifest = yield* extractOpenApiManifest("neon", specText);
259268 const definition = compileOpenApiToolDefinitions(manifest).find(
260269 (candidate) => candidate.toolId === "apiKey.listApiKeys",
@@ -292,7 +301,7 @@ describe("buildOpenApiToolPresentation", () => { · 31 unmodified lines
292301 "extracts parameter schemas and descriptions for parameter-only Vercel operations",
293302 () =>
294303 Effect.gen(function* () {
295− const specText = readFixture("vercel-openapi.json");
304+ const specText = yield* readFixture("vercel-openapi.json");
296305 const manifest = yield* extractOpenApiManifest("vercel", specText);
297306 const definition = compileOpenApiToolDefinitions(manifest).find(
298307 (candidate) => candidate.toolId === "accessGroups.listAccessGroupProjects",
tools/oxlint/README.md+1−0
@@ -11,6 +11,7 @@ Why it lives here: · 10 unmodified lines
1111 Current rules:
1212
1313 - `executor-monorepo/no-cross-workspace-relative-imports`
14+- `executor-monorepo/no-node-fs-with-effect-imports`
1415 - `executor-monorepo/no-raw-effect-fail-errors`
1516 - `executor-monorepo/no-yield-effect-fail`
1617 - `executor-monorepo/no-workspace-src-imports`
tools/oxlint/plugin.mjs+2−0
@@ -1,6 +1,7 @@
11 import { definePlugin } from "@oxlint/plugins";
22
33 import noCrossWorkspaceRelativeImports from "./rules/no-cross-workspace-relative-imports.mjs";
4+import noNodeFsWithEffectImports from "./rules/no-node-fs-with-effect-imports.mjs";
45 import noRawEffectFailErrors from "./rules/no-raw-effect-fail-errors.mjs";
56 import noYieldEffectFail from "./rules/no-yield-effect-fail.mjs";
67 import noWorkspaceSrcImports from "./rules/no-workspace-src-imports.mjs";
@@ -11,6 +12,7 @@ export default definePlugin({ · 4 unmodified lines
1112 },
1213 rules: {
1314 "no-cross-workspace-relative-imports": noCrossWorkspaceRelativeImports,
15+ "no-node-fs-with-effect-imports": noNodeFsWithEffectImports,
1416 "no-raw-effect-fail-errors": noRawEffectFailErrors,
1517 "no-yield-effect-fail": noYieldEffectFail,
1618 "no-workspace-src-imports": noWorkspaceSrcImports,
tools/oxlint/rules/no-node-fs-with-effect-imports.mjs+70−0
@@ -0,0 +1,70 @@
1+import { defineRule } from "@oxlint/plugins";
2+
3+import {
4+ createModuleSourceVisitor,
5+ readStaticSpecifier,
6+} from "../workspace-utils.mjs";
7+
8+const isEffectSource = (specifier) =>
9+ specifier === "effect"
10+ || specifier.startsWith("effect/")
11+ || specifier.startsWith("@effect/");
12+
13+const isNodeFsSource = (specifier) =>
14+ specifier === "node:fs" || specifier === "node:fs/promises";
15+
16+export default defineRule({
17+ meta: {
18+ type: "suggestion",
19+ docs: {
20+ description:
21+ "Disallow node:fs imports in modules that already depend on Effect.",
22+ recommended: true,
23+ },
24+ messages: {
25+ noNodeFsWithEffectImports:
26+ "This module already depends on Effect. Do not import {{moduleName}} here. Depend on `FileSystem.FileSystem` from `@effect/platform` instead, and provide `NodeFileSystem.layer` only at the Node boundary or in tests.",
27+ },
28+ },
29+ create(context) {
30+ let hasEffectImport = false;
31+ const nodeFsImports = [];
32+
33+ const visitSource = (sourceNode) => {
34+ const specifier = readStaticSpecifier(sourceNode);
35+ if (!specifier) {
36+ return;
37+ }
38+
39+ if (isEffectSource(specifier)) {
40+ hasEffectImport = true;
41+ }
42+
43+ if (isNodeFsSource(specifier)) {
44+ nodeFsImports.push({
45+ moduleName: specifier,
46+ node: sourceNode,
47+ });
48+ }
49+ };
50+
51+ return {
52+ ...createModuleSourceVisitor(visitSource),
53+ "Program:exit"() {
54+ if (!hasEffectImport) {
55+ return;
56+ }
57+
58+ for (const entry of nodeFsImports) {
59+ context.report({
60+ node: entry.node,
61+ messageId: "noNodeFsWithEffectImports",
62+ data: {
63+ moduleName: entry.moduleName,
64+ },
65+ });
66+ }
67+ },
68+ };
69+ },
70+});