History

feat: move private auth state to platform-standard home state directory

Introduce homeStateDirectory in ResolvedLocalWorkspaceContext so that secret-bearing control-plane state (credentials, OAuth sessions, leases) is stored under the user's home directory instead of the project workspace tree. State files are written with 0o600 permissions. Resolves XDG_STATE_HOME on Linux, ~/Library/Application Support on macOS, and LOCALAPPDATA on Windows.

Rhys Sullivan committed 7 months ago

Commit a4b2cb71 parent bdbcd4e10 files · +297 −11Browse files

Unified · Split

10 files
  1. packages/control-plane/src/runtime/control-plane-runtime.test.ts +112 −1
  2. packages/control-plane/src/runtime/execution-http.test.ts +19 −5
  3. packages/control-plane/src/runtime/execution-mcp-resume.test.ts +4 −1
  4. packages/control-plane/src/runtime/index.ts +4 −0
  5. packages/control-plane/src/runtime/local-config.test.ts +20 −0
  6. packages/control-plane/src/runtime/local-config.ts +50 −1
  7. packages/control-plane/src/runtime/local-control-plane-store.test.ts +65 −0
  8. packages/control-plane/src/runtime/local-control-plane-store.ts +13 −3
  9. packages/control-plane/src/runtime/local-installation.test.ts +6 −0
  10. packages/control-plane/src/runtime/source-type-declarations.test.ts +4 −0
packages/control-plane/src/runtime/control-plane-runtime.test.ts+112−1
@@ -35,9 +35,15 @@ const makeRuntime = Effect.gen(function* () { · 34 unmodified lines
3535 const workspaceRoot = yield* fs.makeTempDirectoryScoped({
3636 prefix: "executor-control-plane-runtime-",
3737 });
38+ const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc");
39+ const homeStateDirectory = join(workspaceRoot, ".executor-home-state");
3840
3941 return yield* Effect.acquireRelease(
40− createControlPlaneRuntime({ workspaceRoot }),
42+ createControlPlaneRuntime({
43+ workspaceRoot,
44+ homeConfigPath,
45+ homeStateDirectory,
46+ }),
4147 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
4248 );
4349 }).pipe(Effect.provide(NodeFileSystem.layer));
@@ -132,9 +138,13 @@ describe("control-plane-runtime", () => { · 88 unmodified lines
132138 const workspaceRoot = yield* fs.makeTempDirectoryScoped({
133139 prefix: "executor-local-config-runtime-",
134140 });
141+ const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc");
142+ const homeStateDirectory = join(workspaceRoot, ".executor-home-state");
135143 const runtime = yield* Effect.acquireRelease(
136144 createControlPlaneRuntime({
137145 workspaceRoot,
146+ homeConfigPath,
147+ homeStateDirectory,
138148 }),
139149 (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie),
140150 );
@@ -358,6 +368,107 @@ describe("control-plane-runtime", () => { · 217 unmodified lines
358368 }),
359369 );
360370
371+ it.scoped("returns an empty inspection bundle for auth-required sources without a catalog artifact", () =>
372+ Effect.gen(function* () {
373+ const runtime = yield* makeRuntime;
374+ const installation = runtime.localInstallation;
375+ const sourceId = SourceIdSchema.make("googleapis");
376+ const now = Date.now();
377+
378+ const localSource = yield* createSourceFromPayload({
379+ workspaceId: installation.workspaceId,
380+ sourceId,
381+ payload: {
382+ name: "Google Drive",
383+ kind: "google_discovery",
384+ endpoint: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
385+ status: "auth_required",
386+ enabled: true,
387+ namespace: "googleapis",
388+ importAuthPolicy: "reuse_runtime",
389+ binding: {
390+ service: "drive",
391+ version: "v3",
392+ discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
393+ scopes: ["https://www.googleapis.com/auth/drive.readonly"],
394+ },
395+ importAuth: { kind: "none" },
396+ auth: { kind: "none" },
397+ },
398+ now,
399+ }).pipe(Effect.orDie);
400+ yield* persistSource(runtime.persistence.rows, localSource, {
401+ actorAccountId: installation.accountId,
402+ }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie);
403+
404+ const inspection = yield* withControlPlaneClient(
405+ { runtime, accountId: installation.accountId },
406+ (client) =>
407+ client.sources.inspection({
408+ path: {
409+ workspaceId: installation.workspaceId,
410+ sourceId,
411+ },
412+ }),
413+ );
414+
415+ expect(inspection.source.id).toBe(sourceId);
416+ expect(inspection.source.status).toBe("auth_required");
417+ expect(inspection.toolCount).toBe(0);
418+ expect(inspection.tools).toEqual([]);
419+ }),
420+ );
421+
422+ it.scoped("still fails inspection when a connected source is missing its catalog artifact", () =>
423+ Effect.gen(function* () {
424+ const runtime = yield* makeRuntime;
425+ const installation = runtime.localInstallation;
426+ const sourceId = SourceIdSchema.make("googleapis");
427+ const now = Date.now();
428+
429+ const localSource = yield* createSourceFromPayload({
430+ workspaceId: installation.workspaceId,
431+ sourceId,
432+ payload: {
433+ name: "Google Drive",
434+ kind: "google_discovery",
435+ endpoint: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
436+ status: "connected",
437+ enabled: true,
438+ namespace: "googleapis",
439+ importAuthPolicy: "reuse_runtime",
440+ binding: {
441+ service: "drive",
442+ version: "v3",
443+ discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
444+ scopes: ["https://www.googleapis.com/auth/drive.readonly"],
445+ },
446+ importAuth: { kind: "none" },
447+ auth: { kind: "none" },
448+ },
449+ now,
450+ }).pipe(Effect.orDie);
451+ yield* persistSource(runtime.persistence.rows, localSource, {
452+ actorAccountId: installation.accountId,
453+ }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie);
454+
455+ const error = yield* expectLeft(
456+ withControlPlaneClient(
457+ { runtime, accountId: installation.accountId },
458+ (client) =>
459+ client.sources.inspection({
460+ path: {
461+ workspaceId: installation.workspaceId,
462+ sourceId,
463+ },
464+ }),
465+ ),
466+ );
467+
468+ expect(error.message).toContain("Catalog artifact missing for source googleapis");
469+ }),
470+ );
471+
361472 it.scoped("allows continuing an OpenAPI source credential request without auth", () =>
362473 Effect.gen(function* () {
363474 const runtime = yield* makeRuntime;
packages/control-plane/src/runtime/execution-http.test.ts+19−5
@@ -40,11 +40,25 @@ const makeExecutionResolver = () => { · 39 unmodified lines
4040 };
4141
4242 const makeRuntime = Effect.acquireRelease(
43− createControlPlaneRuntime({
44− localDataDir: ":memory:",
45− workspaceRoot: mkdtempSync(join(tmpdir(), "executor-execution-http-")),
46− executionResolver: makeExecutionResolver(),
47− }),
43+ Effect.sync(() => {
44+ const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-execution-http-"));
45+
46+ return {
47+ workspaceRoot,
48+ homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
49+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
50+ };
51+ }).pipe(
52+ Effect.flatMap(({ workspaceRoot, homeConfigPath, homeStateDirectory }) =>
53+ createControlPlaneRuntime({
54+ localDataDir: ":memory:",
55+ workspaceRoot,
56+ homeConfigPath,
57+ homeStateDirectory,
58+ executionResolver: makeExecutionResolver(),
59+ }),
60+ ),
61+ ),
4862 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
4963 );
5064
packages/control-plane/src/runtime/execution-mcp-resume.test.ts+4−1
@@ -283,10 +283,13 @@ describe("execution-mcp-resume", () => { · 282 unmodified lines
283283 it.scoped("keeps live form elicitation resumable without replaying the run", () =>
284284 Effect.gen(function* () {
285285 const mcpServer = yield* makeFormElicitationServer;
286+ const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-execution-mcp-resume-"));
286287 const runtime = yield* Effect.acquireRelease(
287288 createControlPlaneRuntime({
288289 localDataDir: ":memory:",
289− workspaceRoot: mkdtempSync(join(tmpdir(), "executor-execution-mcp-resume-")),
290+ workspaceRoot,
291+ homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
292+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
290293 executionResolver: makeMcpExecutionResolver(mcpServer.endpoint),
291294 }),
292295 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
packages/control-plane/src/runtime/index.ts+4−0
@@ -69,6 +69,8 @@ export type RuntimeControlPlaneOptions = { · 68 unmodified lines
6969 resolveSecretMaterial?: ResolveSecretMaterial;
7070 getLocalServerBaseUrl?: () => string | undefined;
7171 workspaceRoot?: string;
72+ homeConfigPath?: string;
73+ homeStateDirectory?: string;
7274 };
7375
7476 const detailsFromCause = (cause: unknown): string =>
@@ -201,6 +203,8 @@ export const createControlPlaneRuntime = ( · 126 unmodified lines
201203
202204 const localWorkspaceContext = yield* resolveLocalWorkspaceContext({
203205 workspaceRoot: options.workspaceRoot,
206+ homeConfigPath: options.homeConfigPath,
207+ homeStateDirectory: options.homeStateDirectory,
204208 }).pipe(
205209 Effect.mapError(toLocalRuntimeBootstrapError),
206210 Effect.catchAll((error) =>
packages/control-plane/src/runtime/local-config.test.ts+20−0
@@ -9,6 +9,7 @@ import * as Effect from "effect/Effect"; · 8 unmodified lines
99 import {
1010 loadLocalExecutorConfig,
1111 resolveDefaultHomeConfigCandidates,
12+ resolveDefaultHomeStateDirectory,
1213 resolveHomeConfigPath,
1314 resolveLocalWorkspaceContext,
1415 } from "./local-config";
@@ -51,6 +52,7 @@ describe("local-config", () => { · 36 unmodified lines
5152 expect(loaded.config?.sources?.github?.connection.endpoint).toBe(
5253 "https://api.github.com",
5354 );
55+ expect(context.homeStateDirectory).toContain("executor");
5456 }).pipe(Effect.provide(NodeFileSystem.layer)),
5557 );
5658
@@ -104,6 +106,24 @@ describe("local-config", () => { · 47 unmodified lines
104106 expect(macCandidates[2]).toBe("/Users/alice/.config/executor/executor.jsonc");
105107 });
106108
109+ it("uses platform-standard home state directories", () => {
110+ const linuxStateDirectory = resolveDefaultHomeStateDirectory({
111+ platform: "linux",
112+ homeDirectory: "/home/alice",
113+ env: {},
114+ });
115+ const macStateDirectory = resolveDefaultHomeStateDirectory({
116+ platform: "darwin",
117+ homeDirectory: "/Users/alice",
118+ env: {},
119+ });
120+
121+ expect(linuxStateDirectory).toBe("/home/alice/.local/state/executor");
122+ expect(macStateDirectory).toBe(
123+ "/Users/alice/Library/Application Support/Executor/State",
124+ );
125+ });
126+
107127 it.effect("prefers an existing legacy home config path before the canonical path", () =>
108128 Effect.gen(function* () {
109129 const homeDirectory = makeWorkspaceRoot();
packages/control-plane/src/runtime/local-config.ts+50−1
@@ -28,6 +28,7 @@ const PROJECT_CONFIG_BASENAME = "executor.jsonc"; · 27 unmodified lines
2828 const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json";
2929 const PROJECT_CONFIG_DIRECTORY = ".executor";
3030 const EXECUTOR_CONFIG_DIR_ENV = "EXECUTOR_CONFIG_DIR";
31+const EXECUTOR_STATE_DIR_ENV = "EXECUTOR_STATE_DIR";
3132
3233 const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
3334 new LocalFileSystemError({
@@ -73,6 +74,38 @@ const defaultExecutorConfigDirectory = (input: { · 39 unmodified lines
7374 );
7475 };
7576
77+const defaultExecutorStateDirectory = (input: {
78+ env?: NodeJS.ProcessEnv;
79+ platform?: NodeJS.Platform;
80+ homeDirectory?: string;
81+} = {}): string => {
82+ const env = input.env ?? process.env;
83+ const platform = input.platform ?? process.platform;
84+ const homeDirectory = input.homeDirectory ?? homedir();
85+ const explicitStateDirectory = trimOrUndefined(env[EXECUTOR_STATE_DIR_ENV]);
86+
87+ if (explicitStateDirectory) {
88+ return explicitStateDirectory;
89+ }
90+
91+ if (platform === "win32") {
92+ return join(
93+ trimOrUndefined(env.LOCALAPPDATA) ?? join(homeDirectory, "AppData", "Local"),
94+ "Executor",
95+ "State",
96+ );
97+ }
98+
99+ if (platform === "darwin") {
100+ return join(homeDirectory, "Library", "Application Support", "Executor", "State");
101+ }
102+
103+ return join(
104+ trimOrUndefined(env.XDG_STATE_HOME) ?? join(homeDirectory, ".local", "state"),
105+ "executor",
106+ );
107+};
108+
76109 const legacyExecutorConfigDirectory = (input: {
77110 homeDirectory?: string;
78111 } = {}): string =>
@@ -121,6 +154,13 @@ export const resolveHomeConfigPath = (input: { · 42 unmodified lines
121154 return candidates[0]!;
122155 });
123156
157+export const resolveDefaultHomeStateDirectory = (input: {
158+ env?: NodeJS.ProcessEnv;
159+ platform?: NodeJS.Platform;
160+ homeDirectory?: string;
161+} = {}): string =>
162+ defaultExecutorStateDirectory(input);
163+
124164 const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => {
125165 const lines = content.split("\n");
126166
@@ -313,6 +353,7 @@ export type ResolvedLocalWorkspaceContext = { · 186 unmodified lines
313353 configDirectory: string;
314354 projectConfigPath: string;
315355 homeConfigPath: string;
356+ homeStateDirectory: string;
316357 artifactsDirectory: string;
317358 stateDirectory: string;
318359 };
@@ -328,6 +369,8 @@ export type LoadedLocalExecutorConfig = { · 9 unmodified lines
328369 export const resolveLocalWorkspaceContext = (input: {
329370 cwd?: string;
330371 workspaceRoot?: string;
372+ homeConfigPath?: string;
373+ homeStateDirectory?: string;
331374 } = {}): Effect.Effect<
332375 ResolvedLocalWorkspaceContext,
333376 LocalFileSystemError,
@@ -340,7 +383,12 @@ export const resolveLocalWorkspaceContext = (input: { · 6 unmodified lines
340383 );
341384 const workspaceName = basename(workspaceRoot) || "workspace";
342385 const projectConfigPath = yield* resolveProjectConfigPathEffect(workspaceRoot);
343− const homeConfigPath = yield* resolveHomeConfigPath();
386+ const homeConfigPath = resolve(
387+ input.homeConfigPath ?? (yield* resolveHomeConfigPath()),
388+ );
389+ const homeStateDirectory = resolve(
390+ input.homeStateDirectory ?? resolveDefaultHomeStateDirectory(),
391+ );
344392
345393 return {
346394 cwd,
@@ -349,6 +397,7 @@ export const resolveLocalWorkspaceContext = (input: { · 2 unmodified lines
349397 configDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY),
350398 projectConfigPath,
351399 homeConfigPath,
400+ homeStateDirectory,
352401 artifactsDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "artifacts"),
353402 stateDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "state"),
354403 };
packages/control-plane/src/runtime/local-control-plane-store.test.ts+65−0
@@ -0,0 +1,65 @@
1+import { existsSync, mkdtempSync, statSync } from "node:fs";
2+import { tmpdir } from "node:os";
3+import { join } from "node:path";
4+import { describe, expect, it } from "@effect/vitest";
5+import * as Effect from "effect/Effect";
6+
7+import type { ResolvedLocalWorkspaceContext } from "./local-config";
8+import {
9+ loadLocalControlPlaneState,
10+ localControlPlaneStatePath,
11+ writeLocalControlPlaneState,
12+} from "./local-control-plane-store";
13+
14+const makeContext = (): ResolvedLocalWorkspaceContext => {
15+ const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-control-plane-store-"));
16+
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+};
29+
30+describe("local-control-plane-store", () => {
31+ it.effect("stores secret-bearing control-plane state outside the workspace", () =>
32+ Effect.gen(function* () {
33+ const context = makeContext();
34+ const expectedPath = localControlPlaneStatePath(context);
35+ const workspacePath = join(context.stateDirectory, "control-plane-state.json");
36+
37+ yield* writeLocalControlPlaneState({
38+ context,
39+ state: {
40+ version: 1,
41+ authArtifacts: [],
42+ authLeases: [],
43+ sourceOauthClients: [],
44+ sourceAuthSessions: [],
45+ secretMaterials: [],
46+ executions: [],
47+ executionInteractions: [],
48+ executionSteps: [],
49+ },
50+ });
51+
52+ expect(expectedPath.startsWith(context.homeStateDirectory)).toBe(true);
53+ expect(existsSync(expectedPath)).toBe(true);
54+ expect(existsSync(workspacePath)).toBe(false);
55+
56+ const loaded = yield* loadLocalControlPlaneState(context);
57+ expect(loaded.version).toBe(1);
58+ expect(loaded.secretMaterials).toEqual([]);
59+
60+ if (process.platform !== "win32") {
61+ expect(statSync(expectedPath).mode & 0o777).toBe(0o600);
62+ }
63+ }),
64+ );
65+});
packages/control-plane/src/runtime/local-control-plane-store.ts+13−3
@@ -1,7 +1,7 @@
11 import { randomUUID } from "node:crypto";
22 import { existsSync } from "node:fs";
33 import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4−import { dirname, join, resolve } from "node:path";
4+import { dirname, join } from "node:path";
55
66 import {
77 AuthArtifactSchema,
@@ -26,6 +26,7 @@ import * as Option from "effect/Option"; · 18 unmodified lines
2626 import * as Schema from "effect/Schema";
2727
2828 import type { ResolvedLocalWorkspaceContext } from "./local-config";
29+import { deriveLocalInstallation } from "./local-installation";
2930 import {
3031 LocalFileSystemError,
3132 unknownLocalErrorDetails,
@@ -101,7 +102,13 @@ const sortByUpdatedAtAndIdDesc = <T extends { updatedAt: number; id: string }>( · 69 unmodified lines
101102
102103 const localControlPlaneStatePath = (
103104 context: ResolvedLocalWorkspaceContext,
104−): string => join(context.stateDirectory, LOCAL_CONTROL_PLANE_STATE_BASENAME);
105+): string =>
106+ join(
107+ context.homeStateDirectory,
108+ "workspaces",
109+ deriveLocalInstallation(context).workspaceId,
110+ LOCAL_CONTROL_PLANE_STATE_BASENAME,
111+ );
105112
106113 const readStateFromDisk = async (
107114 context: ResolvedLocalWorkspaceContext,
@@ -122,7 +129,10 @@ const writeStateToDisk = async ( · 14 unmodified lines
122129 const path = localControlPlaneStatePath(context);
123130 await mkdir(dirname(path), { recursive: true });
124131 const tempPath = `${path}.${randomUUID()}.tmp`;
125− await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
132+ await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, {
133+ encoding: "utf8",
134+ mode: 0o600,
135+ });
126136 await rename(tempPath, path);
127137 };
128138
packages/control-plane/src/runtime/local-installation.test.ts+6−0
@@ -10,11 +10,15 @@ import { getOrProvisionLocalInstallation } from "./local-installation"; · 9 unmodified lines
1010 import { resolveLocalWorkspaceContext } from "./local-config";
1111
1212 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");
1315
1416 const makeRuntime = Effect.acquireRelease(
1517 createControlPlaneRuntime({
1618 localDataDir: ":memory:",
1719 workspaceRoot: TEST_WORKSPACE_ROOT,
20+ homeConfigPath: TEST_HOME_CONFIG_PATH,
21+ homeStateDirectory: TEST_HOME_STATE_DIRECTORY,
1822 }),
1923 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
2024 );
@@ -37,6 +41,8 @@ describe("local-installation", () => { · 16 unmodified lines
3741 const runtime = yield* makeRuntime;
3842 const context = yield* resolveLocalWorkspaceContext({
3943 workspaceRoot: TEST_WORKSPACE_ROOT,
44+ homeConfigPath: TEST_HOME_CONFIG_PATH,
45+ homeStateDirectory: TEST_HOME_STATE_DIRECTORY,
4046 });
4147
4248 const first = runtime.localInstallation;
packages/control-plane/src/runtime/source-type-declarations.test.ts+4−0
@@ -285,6 +285,7 @@ describe("source-type-declarations", () => { · 284 unmodified lines
285285 configDirectory: join(workspaceRoot, ".executor"),
286286 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
287287 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
288+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
288289 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
289290 stateDirectory: join(workspaceRoot, ".executor", "state"),
290291 };
@@ -338,6 +339,7 @@ describe("source-type-declarations", () => { · 47 unmodified lines
338339 configDirectory: join(workspaceRoot, ".executor"),
339340 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
340341 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
342+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
341343 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
342344 stateDirectory: join(workspaceRoot, ".executor", "state"),
343345 };
@@ -375,6 +377,7 @@ describe("source-type-declarations", () => { · 31 unmodified lines
375377 configDirectory: join(workspaceRoot, ".executor"),
376378 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
377379 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
380+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
378381 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
379382 stateDirectory: join(workspaceRoot, ".executor", "state"),
380383 };
@@ -488,6 +491,7 @@ describe("source-type-declarations", () => { · 107 unmodified lines
488491 configDirectory: join(workspaceRoot, ".executor"),
489492 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
490493 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
494+ homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
491495 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
492496 stateDirectory: join(workspaceRoot, ".executor", "state"),
493497 };