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 | 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; |
| @@ -40,11 +40,25 @@ const makeExecutionResolver = () => { · 39 unmodified lines | |||
| 40 | }; | 40 | }; |
| 41 | 41 | ||
| 42 | const makeRuntime = Effect.acquireRelease( | 42 | const 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 | ||
| @@ -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), |
| @@ -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 | ||
| 74 | const detailsFromCause = (cause: unknown): string => | 76 | const 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) => |
| @@ -9,6 +9,7 @@ import * as Effect from "effect/Effect"; · 8 unmodified lines | |||
| 9 | import { | 9 | import { |
| 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(); |
| @@ -28,6 +28,7 @@ const PROJECT_CONFIG_BASENAME = "executor.jsonc"; · 27 unmodified lines | |||
| 28 | const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json"; | 28 | const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json"; |
| 29 | const PROJECT_CONFIG_DIRECTORY = ".executor"; | 29 | const PROJECT_CONFIG_DIRECTORY = ".executor"; |
| 30 | const EXECUTOR_CONFIG_DIR_ENV = "EXECUTOR_CONFIG_DIR"; | 30 | const EXECUTOR_CONFIG_DIR_ENV = "EXECUTOR_CONFIG_DIR"; |
| 31 | const EXECUTOR_STATE_DIR_ENV = "EXECUTOR_STATE_DIR"; | ||
| 31 | 32 | ||
| 32 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => | 33 | const 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 | ||
| 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 | const legacyExecutorConfigDirectory = (input: { | 109 | const 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 | ||
| 157 | export const resolveDefaultHomeStateDirectory = (input: { | ||
| 158 | env?: NodeJS.ProcessEnv; | ||
| 159 | platform?: NodeJS.Platform; | ||
| 160 | homeDirectory?: string; | ||
| 161 | } = {}): string => | ||
| 162 | defaultExecutorStateDirectory(input); | ||
| 163 | |||
| 124 | const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => { | 164 | const 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 | |||
| 328 | export const resolveLocalWorkspaceContext = (input: { | 369 | export 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 | }; |
| @@ -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 | import { randomUUID } from "node:crypto"; | 1 | import { randomUUID } from "node:crypto"; |
| 2 | import { existsSync } from "node:fs"; | 2 | import { existsSync } from "node:fs"; |
| 3 | import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; | 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 | import { | 6 | import { |
| 7 | AuthArtifactSchema, | 7 | AuthArtifactSchema, |
| @@ -26,6 +26,7 @@ import * as Option from "effect/Option"; · 18 unmodified lines | |||
| 26 | import * as Schema from "effect/Schema"; | 26 | import * as Schema from "effect/Schema"; |
| 27 | 27 | ||
| 28 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; | 28 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; |
| 29 | import { deriveLocalInstallation } from "./local-installation"; | ||
| 29 | import { | 30 | import { |
| 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 | ||
| 102 | const localControlPlaneStatePath = ( | 103 | const 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 | ||
| 106 | const readStateFromDisk = async ( | 113 | const 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 | ||
| @@ -10,11 +10,15 @@ import { getOrProvisionLocalInstallation } from "./local-installation"; · 9 unmodified lines | |||
| 10 | import { resolveLocalWorkspaceContext } from "./local-config"; | 10 | import { resolveLocalWorkspaceContext } from "./local-config"; |
| 11 | 11 | ||
| 12 | const TEST_WORKSPACE_ROOT = mkdtempSync(join(tmpdir(), "executor-local-installation-")); | 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 | const makeRuntime = Effect.acquireRelease( | 16 | const 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; |
| @@ -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 | }; |