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
35 const workspaceRoot = yield* fs.makeTempDirectoryScoped({35 const workspaceRoot = yield* fs.makeTempDirectoryScoped({
36 prefix: "executor-control-plane-runtime-",36 prefix: "executor-control-plane-runtime-",
37 });37 });
38 const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc");
39 const homeStateDirectory = join(workspaceRoot, ".executor-home-state");
38 40
39 return yield* Effect.acquireRelease(41 return yield* Effect.acquireRelease(
40 createControlPlaneRuntime({ workspaceRoot }),42 createControlPlaneRuntime({
43 workspaceRoot,
44 homeConfigPath,
45 homeStateDirectory,
46 }),
41 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),47 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
42 );48 );
43}).pipe(Effect.provide(NodeFileSystem.layer));49}).pipe(Effect.provide(NodeFileSystem.layer));
@@ -132,9 +138,13 @@ describe("control-plane-runtime", () => { · 88 unmodified lines
132 const workspaceRoot = yield* fs.makeTempDirectoryScoped({138 const workspaceRoot = yield* fs.makeTempDirectoryScoped({
133 prefix: "executor-local-config-runtime-",139 prefix: "executor-local-config-runtime-",
134 });140 });
141 const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc");
142 const homeStateDirectory = join(workspaceRoot, ".executor-home-state");
135 const runtime = yield* Effect.acquireRelease(143 const runtime = yield* Effect.acquireRelease(
136 createControlPlaneRuntime({144 createControlPlaneRuntime({
137 workspaceRoot,145 workspaceRoot,
146 homeConfigPath,
147 homeStateDirectory,
138 }),148 }),
139 (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie),149 (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie),
140 );150 );
@@ -358,6 +368,107 @@ describe("control-plane-runtime", () => { · 217 unmodified lines
358 }),368 }),
359 );369 );
360 370
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
361 it.scoped("allows continuing an OpenAPI source credential request without auth", () =>472 it.scoped("allows continuing an OpenAPI source credential request without auth", () =>
362 Effect.gen(function* () {473 Effect.gen(function* () {
363 const runtime = yield* makeRuntime;474 const runtime = yield* makeRuntime;
packages/control-plane/src/runtime/execution-http.test.ts+19−5
@@ -40,11 +40,25 @@ const makeExecutionResolver = () => { · 39 unmodified lines
40};40};
41 41
42const makeRuntime = Effect.acquireRelease(42const makeRuntime = Effect.acquireRelease(
43 createControlPlaneRuntime({43 Effect.sync(() => {
44 localDataDir: ":memory:",44 const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-execution-http-"));
45 workspaceRoot: mkdtempSync(join(tmpdir(), "executor-execution-http-")),45
46 executionResolver: makeExecutionResolver(),46 return {
47 }),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 ),
48 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),62 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
49);63);
50 64
packages/control-plane/src/runtime/execution-mcp-resume.test.ts+4−1
@@ -283,10 +283,13 @@ describe("execution-mcp-resume", () => { · 282 unmodified lines
283 it.scoped("keeps live form elicitation resumable without replaying the run", () =>283 it.scoped("keeps live form elicitation resumable without replaying the run", () =>
284 Effect.gen(function* () {284 Effect.gen(function* () {
285 const mcpServer = yield* makeFormElicitationServer;285 const mcpServer = yield* makeFormElicitationServer;
286 const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-execution-mcp-resume-"));
286 const runtime = yield* Effect.acquireRelease(287 const runtime = yield* Effect.acquireRelease(
287 createControlPlaneRuntime({288 createControlPlaneRuntime({
288 localDataDir: ":memory:",289 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"),
290 executionResolver: makeMcpExecutionResolver(mcpServer.endpoint),293 executionResolver: makeMcpExecutionResolver(mcpServer.endpoint),
291 }),294 }),
292 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),295 (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
69 resolveSecretMaterial?: ResolveSecretMaterial;69 resolveSecretMaterial?: ResolveSecretMaterial;
70 getLocalServerBaseUrl?: () => string | undefined;70 getLocalServerBaseUrl?: () => string | undefined;
71 workspaceRoot?: string;71 workspaceRoot?: string;
72 homeConfigPath?: string;
73 homeStateDirectory?: string;
72};74};
73 75
74const detailsFromCause = (cause: unknown): string =>76const detailsFromCause = (cause: unknown): string =>
@@ -201,6 +203,8 @@ export const createControlPlaneRuntime = ( · 126 unmodified lines
201 203
202 const localWorkspaceContext = yield* resolveLocalWorkspaceContext({204 const localWorkspaceContext = yield* resolveLocalWorkspaceContext({
203 workspaceRoot: options.workspaceRoot,205 workspaceRoot: options.workspaceRoot,
206 homeConfigPath: options.homeConfigPath,
207 homeStateDirectory: options.homeStateDirectory,
204 }).pipe(208 }).pipe(
205 Effect.mapError(toLocalRuntimeBootstrapError),209 Effect.mapError(toLocalRuntimeBootstrapError),
206 Effect.catchAll((error) =>210 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
9import {9import {
10 loadLocalExecutorConfig,10 loadLocalExecutorConfig,
11 resolveDefaultHomeConfigCandidates,11 resolveDefaultHomeConfigCandidates,
12 resolveDefaultHomeStateDirectory,
12 resolveHomeConfigPath,13 resolveHomeConfigPath,
13 resolveLocalWorkspaceContext,14 resolveLocalWorkspaceContext,
14} from "./local-config";15} from "./local-config";
@@ -51,6 +52,7 @@ describe("local-config", () => { · 36 unmodified lines
51 expect(loaded.config?.sources?.github?.connection.endpoint).toBe(52 expect(loaded.config?.sources?.github?.connection.endpoint).toBe(
52 "https://api.github.com",53 "https://api.github.com",
53 );54 );
55 expect(context.homeStateDirectory).toContain("executor");
54 }).pipe(Effect.provide(NodeFileSystem.layer)),56 }).pipe(Effect.provide(NodeFileSystem.layer)),
55 );57 );
56 58
@@ -104,6 +106,24 @@ describe("local-config", () => { · 47 unmodified lines
104 expect(macCandidates[2]).toBe("/Users/alice/.config/executor/executor.jsonc");106 expect(macCandidates[2]).toBe("/Users/alice/.config/executor/executor.jsonc");
105 });107 });
106 108
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
107 it.effect("prefers an existing legacy home config path before the canonical path", () =>127 it.effect("prefers an existing legacy home config path before the canonical path", () =>
108 Effect.gen(function* () {128 Effect.gen(function* () {
109 const homeDirectory = makeWorkspaceRoot();129 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
28const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json";28const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json";
29const PROJECT_CONFIG_DIRECTORY = ".executor";29const PROJECT_CONFIG_DIRECTORY = ".executor";
30const EXECUTOR_CONFIG_DIR_ENV = "EXECUTOR_CONFIG_DIR";30const EXECUTOR_CONFIG_DIR_ENV = "EXECUTOR_CONFIG_DIR";
31const EXECUTOR_STATE_DIR_ENV = "EXECUTOR_STATE_DIR";
31 32
32const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>33const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
33 new LocalFileSystemError({34 new LocalFileSystemError({
@@ -73,6 +74,38 @@ const defaultExecutorConfigDirectory = (input: { · 39 unmodified lines
73 );74 );
74};75};
75 76
77const 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
76const legacyExecutorConfigDirectory = (input: {109const legacyExecutorConfigDirectory = (input: {
77 homeDirectory?: string;110 homeDirectory?: string;
78} = {}): string =>111} = {}): string =>
@@ -121,6 +154,13 @@ export const resolveHomeConfigPath = (input: { · 42 unmodified lines
121 return candidates[0]!;154 return candidates[0]!;
122 });155 });
123 156
157export const resolveDefaultHomeStateDirectory = (input: {
158 env?: NodeJS.ProcessEnv;
159 platform?: NodeJS.Platform;
160 homeDirectory?: string;
161} = {}): string =>
162 defaultExecutorStateDirectory(input);
163
124const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => {164const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => {
125 const lines = content.split("\n");165 const lines = content.split("\n");
126 166
@@ -313,6 +353,7 @@ export type ResolvedLocalWorkspaceContext = { · 186 unmodified lines
313 configDirectory: string;353 configDirectory: string;
314 projectConfigPath: string;354 projectConfigPath: string;
315 homeConfigPath: string;355 homeConfigPath: string;
356 homeStateDirectory: string;
316 artifactsDirectory: string;357 artifactsDirectory: string;
317 stateDirectory: string;358 stateDirectory: string;
318};359};
@@ -328,6 +369,8 @@ export type LoadedLocalExecutorConfig = { · 9 unmodified lines
328export const resolveLocalWorkspaceContext = (input: {369export const resolveLocalWorkspaceContext = (input: {
329 cwd?: string;370 cwd?: string;
330 workspaceRoot?: string;371 workspaceRoot?: string;
372 homeConfigPath?: string;
373 homeStateDirectory?: string;
331} = {}): Effect.Effect<374} = {}): Effect.Effect<
332 ResolvedLocalWorkspaceContext,375 ResolvedLocalWorkspaceContext,
333 LocalFileSystemError,376 LocalFileSystemError,
@@ -340,7 +383,12 @@ export const resolveLocalWorkspaceContext = (input: { · 6 unmodified lines
340 );383 );
341 const workspaceName = basename(workspaceRoot) || "workspace";384 const workspaceName = basename(workspaceRoot) || "workspace";
342 const projectConfigPath = yield* resolveProjectConfigPathEffect(workspaceRoot);385 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 );
344 392
345 return {393 return {
346 cwd,394 cwd,
@@ -349,6 +397,7 @@ export const resolveLocalWorkspaceContext = (input: { · 2 unmodified lines
349 configDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY),397 configDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY),
350 projectConfigPath,398 projectConfigPath,
351 homeConfigPath,399 homeConfigPath,
400 homeStateDirectory,
352 artifactsDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "artifacts"),401 artifactsDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "artifacts"),
353 stateDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "state"),402 stateDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "state"),
354 };403 };
packages/control-plane/src/runtime/local-control-plane-store.test.ts+65−0
@@ -0,0 +1,65 @@
1import { existsSync, mkdtempSync, statSync } from "node:fs";
2import { tmpdir } from "node:os";
3import { join } from "node:path";
4import { describe, expect, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";
6
7import type { ResolvedLocalWorkspaceContext } from "./local-config";
8import {
9 loadLocalControlPlaneState,
10 localControlPlaneStatePath,
11 writeLocalControlPlaneState,
12} from "./local-control-plane-store";
13
14const 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
30describe("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 @@
1import { randomUUID } from "node:crypto";1import { randomUUID } from "node:crypto";
2import { existsSync } from "node:fs";2import { existsSync } from "node:fs";
3import { mkdir, readFile, rename, writeFile } from "node:fs/promises";3import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4import { dirname, join, resolve } from "node:path";4import { dirname, join } from "node:path";
5 5
6import {6import {
7 AuthArtifactSchema,7 AuthArtifactSchema,
@@ -26,6 +26,7 @@ import * as Option from "effect/Option"; · 18 unmodified lines
26import * as Schema from "effect/Schema";26import * as Schema from "effect/Schema";
27 27
28import type { ResolvedLocalWorkspaceContext } from "./local-config";28import type { ResolvedLocalWorkspaceContext } from "./local-config";
29import { deriveLocalInstallation } from "./local-installation";
29import {30import {
30 LocalFileSystemError,31 LocalFileSystemError,
31 unknownLocalErrorDetails,32 unknownLocalErrorDetails,
@@ -101,7 +102,13 @@ const sortByUpdatedAtAndIdDesc = <T extends { updatedAt: number; id: string }>( · 69 unmodified lines
101 102
102const localControlPlaneStatePath = (103const localControlPlaneStatePath = (
103 context: ResolvedLocalWorkspaceContext,104 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 );
105 112
106const readStateFromDisk = async (113const readStateFromDisk = async (
107 context: ResolvedLocalWorkspaceContext,114 context: ResolvedLocalWorkspaceContext,
@@ -122,7 +129,10 @@ const writeStateToDisk = async ( · 14 unmodified lines
122 const path = localControlPlaneStatePath(context);129 const path = localControlPlaneStatePath(context);
123 await mkdir(dirname(path), { recursive: true });130 await mkdir(dirname(path), { recursive: true });
124 const tempPath = `${path}.${randomUUID()}.tmp`;131 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 });
126 await rename(tempPath, path);136 await rename(tempPath, path);
127};137};
128 138
packages/control-plane/src/runtime/local-installation.test.ts+6−0
@@ -10,11 +10,15 @@ import { getOrProvisionLocalInstallation } from "./local-installation"; · 9 unmodified lines
10import { resolveLocalWorkspaceContext } from "./local-config";10import { resolveLocalWorkspaceContext } from "./local-config";
11 11
12const TEST_WORKSPACE_ROOT = mkdtempSync(join(tmpdir(), "executor-local-installation-"));12const TEST_WORKSPACE_ROOT = mkdtempSync(join(tmpdir(), "executor-local-installation-"));
13const TEST_HOME_CONFIG_PATH = join(TEST_WORKSPACE_ROOT, ".executor-home.jsonc");
14const TEST_HOME_STATE_DIRECTORY = join(TEST_WORKSPACE_ROOT, ".executor-home-state");
13 15
14const makeRuntime = Effect.acquireRelease(16const makeRuntime = Effect.acquireRelease(
15 createControlPlaneRuntime({17 createControlPlaneRuntime({
16 localDataDir: ":memory:",18 localDataDir: ":memory:",
17 workspaceRoot: TEST_WORKSPACE_ROOT,19 workspaceRoot: TEST_WORKSPACE_ROOT,
20 homeConfigPath: TEST_HOME_CONFIG_PATH,
21 homeStateDirectory: TEST_HOME_STATE_DIRECTORY,
18 }),22 }),
19 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),23 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
20);24);
@@ -37,6 +41,8 @@ describe("local-installation", () => { · 16 unmodified lines
37 const runtime = yield* makeRuntime;41 const runtime = yield* makeRuntime;
38 const context = yield* resolveLocalWorkspaceContext({42 const context = yield* resolveLocalWorkspaceContext({
39 workspaceRoot: TEST_WORKSPACE_ROOT,43 workspaceRoot: TEST_WORKSPACE_ROOT,
44 homeConfigPath: TEST_HOME_CONFIG_PATH,
45 homeStateDirectory: TEST_HOME_STATE_DIRECTORY,
40 });46 });
41 47
42 const first = runtime.localInstallation;48 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
285 configDirectory: join(workspaceRoot, ".executor"),285 configDirectory: join(workspaceRoot, ".executor"),
286 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),286 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
287 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),287 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
288 homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
288 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),289 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
289 stateDirectory: join(workspaceRoot, ".executor", "state"),290 stateDirectory: join(workspaceRoot, ".executor", "state"),
290 };291 };
@@ -338,6 +339,7 @@ describe("source-type-declarations", () => { · 47 unmodified lines
338 configDirectory: join(workspaceRoot, ".executor"),339 configDirectory: join(workspaceRoot, ".executor"),
339 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),340 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
340 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),341 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
342 homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
341 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),343 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
342 stateDirectory: join(workspaceRoot, ".executor", "state"),344 stateDirectory: join(workspaceRoot, ".executor", "state"),
343 };345 };
@@ -375,6 +377,7 @@ describe("source-type-declarations", () => { · 31 unmodified lines
375 configDirectory: join(workspaceRoot, ".executor"),377 configDirectory: join(workspaceRoot, ".executor"),
376 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),378 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
377 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),379 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
380 homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
378 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),381 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
379 stateDirectory: join(workspaceRoot, ".executor", "state"),382 stateDirectory: join(workspaceRoot, ".executor", "state"),
380 };383 };
@@ -488,6 +491,7 @@ describe("source-type-declarations", () => { · 107 unmodified lines
488 configDirectory: join(workspaceRoot, ".executor"),491 configDirectory: join(workspaceRoot, ".executor"),
489 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),492 projectConfigPath: join(workspaceRoot, ".executor", "executor.jsonc"),
490 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),493 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
494 homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
491 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),495 artifactsDirectory: join(workspaceRoot, ".executor", "artifacts"),
492 stateDirectory: join(workspaceRoot, ".executor", "state"),496 stateDirectory: join(workspaceRoot, ".executor", "state"),
493 };497 };