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