Unified · Split
| @@ -6,10 +6,10 @@ import * as Data from "effect/Data"; · 5 unmodified lines | |||
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Exit from "effect/Exit"; | 7 | import * as Exit from "effect/Exit"; |
| 8 | 8 | ||
| 9 | import type { LocalExecutorConfig } from "@executor/platform-sdk/schema"; | 9 | import type { ExecutorScopeConfig } from "@executor/platform-sdk/schema"; |
| 10 | 10 | ||
| 11 | import { | 11 | import { |
| 12 | readOptionalLocalExecutorConfig, | 12 | readOptionalExecutorScopeConfig, |
| 13 | resolveLocalWorkspaceContext, | 13 | resolveLocalWorkspaceContext, |
| 14 | } from "./config"; | 14 | } from "./config"; |
| 15 | import { | 15 | import { |
| @@ -186,8 +186,8 @@ const copyRecursive = ( · 170 unmodified lines | |||
| 186 | }); | 186 | }); |
| 187 | 187 | ||
| 188 | const sanitizeSourceConfig = ( | 188 | const sanitizeSourceConfig = ( |
| 189 | sourceConfig: NonNullable<LocalExecutorConfig["sources"]>[string], | 189 | sourceConfig: NonNullable<ExecutorScopeConfig["sources"]>[string], |
| 190 | ): NonNullable<LocalExecutorConfig["sources"]>[string] => sourceConfig; | 190 | ): NonNullable<ExecutorScopeConfig["sources"]>[string] => sourceConfig; |
| 191 | 191 | ||
| 192 | const main = Effect.gen(function* () { | 192 | const main = Effect.gen(function* () { |
| 193 | const args = yield* Effect.try({ | 193 | const args = yield* Effect.try({ |
| @@ -218,7 +218,7 @@ const main = Effect.gen(function* () { · 24 unmodified lines | |||
| 218 | ), | 218 | ), |
| 219 | ); | 219 | ); |
| 220 | 220 | ||
| 221 | const projectConfig = yield* readOptionalLocalExecutorConfig( | 221 | const projectConfig = yield* readOptionalExecutorScopeConfig( |
| 222 | context.projectConfigPath, | 222 | context.projectConfigPath, |
| 223 | ).pipe( | 223 | ).pipe( |
| 224 | Effect.mapError((cause) => | 224 | Effect.mapError((cause) => |
| @@ -293,7 +293,7 @@ const main = Effect.gen(function* () { · 68 unmodified lines | |||
| 293 | sources: { | 293 | sources: { |
| 294 | [args.sourceId]: sanitizeSourceConfig(sourceConfig), | 294 | [args.sourceId]: sanitizeSourceConfig(sourceConfig), |
| 295 | }, | 295 | }, |
| 296 | } satisfies LocalExecutorConfig; | 296 | } satisfies ExecutorScopeConfig; |
| 297 | 297 | ||
| 298 | const fixtureState = { | 298 | const fixtureState = { |
| 299 | version: 1 as const, | 299 | version: 1 as const, |
| @@ -0,0 +1,93 @@ | |||
| 1 | import { | ||
| 2 | type ParseError as JsoncParseError, | ||
| 3 | parse as parseJsoncDocument, | ||
| 4 | printParseErrorCode, | ||
| 5 | } from "jsonc-parser/lib/esm/main.js"; | ||
| 6 | |||
| 7 | import { | ||
| 8 | ExecutorScopeConfigSchema, | ||
| 9 | type ExecutorScopeConfig, | ||
| 10 | } from "@executor/platform-sdk/schema"; | ||
| 11 | import * as Schema from "effect/Schema"; | ||
| 12 | import { | ||
| 13 | ExecutorScopeConfigDecodeError, | ||
| 14 | unknownLocalErrorDetails, | ||
| 15 | } from "./errors"; | ||
| 16 | |||
| 17 | export const decodeExecutorScopeConfig = Schema.decodeUnknownSync( | ||
| 18 | ExecutorScopeConfigSchema, | ||
| 19 | { | ||
| 20 | onExcessProperty: "error", | ||
| 21 | }, | ||
| 22 | ); | ||
| 23 | |||
| 24 | export const encodeExecutorScopeConfig = ( | ||
| 25 | config: ExecutorScopeConfig, | ||
| 26 | ): string => `${JSON.stringify(config, null, 2)}\n`; | ||
| 27 | |||
| 28 | const formatJsoncParseErrors = ( | ||
| 29 | content: string, | ||
| 30 | errors: readonly JsoncParseError[], | ||
| 31 | ): string => { | ||
| 32 | const lines = content.split("\n"); | ||
| 33 | |||
| 34 | return errors | ||
| 35 | .map((error) => { | ||
| 36 | const beforeOffset = content.slice(0, error.offset).split("\n"); | ||
| 37 | const line = beforeOffset.length; | ||
| 38 | const column = beforeOffset[beforeOffset.length - 1]?.length ?? 0; | ||
| 39 | const lineText = lines[line - 1]; | ||
| 40 | const location = `line ${line}, column ${column + 1}`; | ||
| 41 | const detail = printParseErrorCode(error.error); | ||
| 42 | |||
| 43 | if (!lineText) { | ||
| 44 | return `${detail} at ${location}`; | ||
| 45 | } | ||
| 46 | |||
| 47 | return `${detail} at ${location}\n${lineText}`; | ||
| 48 | }) | ||
| 49 | .join("\n"); | ||
| 50 | }; | ||
| 51 | |||
| 52 | export const parseJsoncValue = (input: { | ||
| 53 | path: string; | ||
| 54 | content: string; | ||
| 55 | }): unknown => { | ||
| 56 | const errors: JsoncParseError[] = []; | ||
| 57 | const parsed = parseJsoncDocument(input.content, errors, { | ||
| 58 | allowTrailingComma: true, | ||
| 59 | }); | ||
| 60 | |||
| 61 | if (errors.length > 0) { | ||
| 62 | throw new ExecutorScopeConfigDecodeError({ | ||
| 63 | message: `Invalid executor config at ${input.path}: ${formatJsoncParseErrors(input.content, errors)}`, | ||
| 64 | path: input.path, | ||
| 65 | details: formatJsoncParseErrors(input.content, errors), | ||
| 66 | }); | ||
| 67 | } | ||
| 68 | |||
| 69 | return parsed; | ||
| 70 | }; | ||
| 71 | |||
| 72 | export const parseExecutorScopeConfig = (input: { | ||
| 73 | path: string; | ||
| 74 | content: string; | ||
| 75 | }): ExecutorScopeConfig => { | ||
| 76 | try { | ||
| 77 | return decodeExecutorScopeConfig( | ||
| 78 | parseJsoncValue({ | ||
| 79 | path: input.path, | ||
| 80 | content: input.content, | ||
| 81 | }), | ||
| 82 | ); | ||
| 83 | } catch (cause) { | ||
| 84 | if (cause instanceof ExecutorScopeConfigDecodeError) { | ||
| 85 | throw cause; | ||
| 86 | } | ||
| 87 | throw new ExecutorScopeConfigDecodeError({ | ||
| 88 | message: `Invalid executor config at ${input.path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 89 | path: input.path, | ||
| 90 | details: unknownLocalErrorDetails(cause), | ||
| 91 | }); | ||
| 92 | } | ||
| 93 | }; | ||
| @@ -0,0 +1,188 @@ | |||
| 1 | import { tmpdir } from "node:os"; | ||
| 2 | import { join } from "node:path"; | ||
| 3 | import { FileSystem } from "@effect/platform"; | ||
| 4 | import { NodeFileSystem } from "@effect/platform-node"; | ||
| 5 | import { describe, expect, it } from "@effect/vitest"; | ||
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | |||
| 8 | import { | ||
| 9 | loadExecutorScopeConfig, | ||
| 10 | resolveLocalWorkspaceContext, | ||
| 11 | } from "./config"; | ||
| 12 | import { | ||
| 13 | migrateLegacyExecutorScopeConfigs, | ||
| 14 | } from "./config-migrations"; | ||
| 15 | import { | ||
| 16 | createLocalExecutorRepositoriesEffect, | ||
| 17 | } from "./index"; | ||
| 18 | |||
| 19 | const resolveMaybeEffect = <T>( | ||
| 20 | value: T | Promise<T> | Effect.Effect<T, Error, never>, | ||
| 21 | ): Effect.Effect<T, Error, never> => | ||
| 22 | Effect.isEffect(value) | ||
| 23 | ? value | ||
| 24 | : value instanceof Promise | ||
| 25 | ? Effect.promise(() => value) | ||
| 26 | : Effect.succeed(value); | ||
| 27 | |||
| 28 | const makeWorkspaceRoot = () => | ||
| 29 | FileSystem.FileSystem.pipe( | ||
| 30 | Effect.flatMap((fs) => | ||
| 31 | fs.makeTempDirectory({ | ||
| 32 | directory: tmpdir(), | ||
| 33 | prefix: "executor-config-migrations-", | ||
| 34 | }) | ||
| 35 | ), | ||
| 36 | ); | ||
| 37 | |||
| 38 | describe("config-migrations", () => { | ||
| 39 | it.effect("rewrites legacy source entries into the current config shape", () => | ||
| 40 | Effect.gen(function* () { | ||
| 41 | const fs = yield* FileSystem.FileSystem; | ||
| 42 | const workspaceRoot = yield* makeWorkspaceRoot(); | ||
| 43 | const configDirectory = join(workspaceRoot, ".executor"); | ||
| 44 | yield* fs.makeDirectory(configDirectory, { recursive: true }); | ||
| 45 | yield* fs.writeFileString( | ||
| 46 | join(configDirectory, "executor.jsonc"), | ||
| 47 | `{ | ||
| 48 | "sources": { | ||
| 49 | "github": { | ||
| 50 | "kind": "openapi", | ||
| 51 | "name": "GitHub", | ||
| 52 | "connection": { | ||
| 53 | "endpoint": "https://api.github.com" | ||
| 54 | }, | ||
| 55 | "binding": { | ||
| 56 | "specUrl": "https://example.com/openapi.json", | ||
| 57 | "defaultHeaders": { | ||
| 58 | "x-test": "1" | ||
| 59 | } | ||
| 60 | } | ||
| 61 | }, | ||
| 62 | "google-calendar": { | ||
| 63 | "kind": "google_discovery", | ||
| 64 | "name": "Google Calendar", | ||
| 65 | "namespace": "google.calendar", | ||
| 66 | "connection": { | ||
| 67 | "endpoint": "https://calendar-json.googleapis.com/$discovery/rest?version=v3" | ||
| 68 | }, | ||
| 69 | "binding": { | ||
| 70 | "service": "calendar", | ||
| 71 | "version": "v3", | ||
| 72 | "discoveryUrl": "https://calendar-json.googleapis.com/$discovery/rest?version=v3", | ||
| 73 | "defaultHeaders": { | ||
| 74 | "x-goog-api-client": "executor" | ||
| 75 | }, | ||
| 76 | "scopes": [ | ||
| 77 | "https://www.googleapis.com/auth/calendar.readonly" | ||
| 78 | ] | ||
| 79 | } | ||
| 80 | } | ||
| 81 | } | ||
| 82 | } | ||
| 83 | `, | ||
| 84 | ); | ||
| 85 | |||
| 86 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); | ||
| 87 | const beforeMigration = yield* Effect.flip(loadExecutorScopeConfig(context)); | ||
| 88 | expect(beforeMigration.message).toContain("Invalid executor config"); | ||
| 89 | |||
| 90 | const migratedPaths = yield* migrateLegacyExecutorScopeConfigs(context); | ||
| 91 | expect(migratedPaths).toContain(context.projectConfigPath); | ||
| 92 | expect(yield* fs.exists(`${context.projectConfigPath}.legacy-backup`)).toBe(true); | ||
| 93 | |||
| 94 | const loaded = yield* loadExecutorScopeConfig(context); | ||
| 95 | const rewritten = yield* fs.readFileString(context.projectConfigPath, "utf8"); | ||
| 96 | |||
| 97 | expect(rewritten).not.toContain("\"connection\""); | ||
| 98 | expect(rewritten).not.toContain("\"binding\""); | ||
| 99 | expect(loaded.config?.sources?.github?.config).toEqual({ | ||
| 100 | specUrl: "https://example.com/openapi.json", | ||
| 101 | baseUrl: "https://api.github.com", | ||
| 102 | auth: { | ||
| 103 | kind: "none", | ||
| 104 | }, | ||
| 105 | defaultHeaders: { | ||
| 106 | "x-test": "1", | ||
| 107 | }, | ||
| 108 | }); | ||
| 109 | |||
| 110 | expect(loaded.config?.sources?.["google-calendar"]?.config).toEqual({ | ||
| 111 | service: "calendar", | ||
| 112 | version: "v3", | ||
| 113 | discoveryUrl: "https://calendar-json.googleapis.com/$discovery/rest?version=v3", | ||
| 114 | defaultHeaders: { | ||
| 115 | "x-goog-api-client": "executor", | ||
| 116 | }, | ||
| 117 | scopes: ["https://www.googleapis.com/auth/calendar.readonly"], | ||
| 118 | auth: { | ||
| 119 | kind: "none", | ||
| 120 | }, | ||
| 121 | }); | ||
| 122 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 123 | ); | ||
| 124 | |||
| 125 | it.effect("runs startup migrations before the strict config loader", () => | ||
| 126 | Effect.gen(function* () { | ||
| 127 | const fs = yield* FileSystem.FileSystem; | ||
| 128 | const workspaceRoot = yield* makeWorkspaceRoot(); | ||
| 129 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 130 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 131 | const configDirectory = join(workspaceRoot, ".executor"); | ||
| 132 | const projectConfigPath = join(configDirectory, "executor.jsonc"); | ||
| 133 | yield* fs.makeDirectory(configDirectory, { recursive: true }); | ||
| 134 | yield* fs.writeFileString( | ||
| 135 | projectConfigPath, | ||
| 136 | `{ | ||
| 137 | "secrets": { | ||
| 138 | "providers": {}, | ||
| 139 | "defaults": {} | ||
| 140 | }, | ||
| 141 | "sources": { | ||
| 142 | "github": { | ||
| 143 | "kind": "openapi", | ||
| 144 | "config": { | ||
| 145 | "specUrl": "https://example.com/openapi.json", | ||
| 146 | "baseUrl": "https://api.github.com", | ||
| 147 | "auth": { | ||
| 148 | "kind": "none" | ||
| 149 | }, | ||
| 150 | "defaultHeaders": null | ||
| 151 | } | ||
| 152 | } | ||
| 153 | } | ||
| 154 | } | ||
| 155 | `, | ||
| 156 | ); | ||
| 157 | |||
| 158 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 159 | workspaceRoot, | ||
| 160 | homeConfigPath, | ||
| 161 | homeStateDirectory, | ||
| 162 | }); | ||
| 163 | const beforeStartup = yield* Effect.flip(loadExecutorScopeConfig(context)); | ||
| 164 | expect(beforeStartup.message).toContain("Invalid executor config"); | ||
| 165 | |||
| 166 | const repositories = yield* createLocalExecutorRepositoriesEffect({ | ||
| 167 | workspaceRoot, | ||
| 168 | homeConfigPath, | ||
| 169 | homeStateDirectory, | ||
| 170 | }); | ||
| 171 | const rewritten = yield* fs.readFileString(projectConfigPath, "utf8"); | ||
| 172 | const loaded = yield* resolveMaybeEffect( | ||
| 173 | repositories.workspace.config.load(), | ||
| 174 | ); | ||
| 175 | |||
| 176 | expect(yield* fs.exists(`${projectConfigPath}.legacy-backup`)).toBe(true); | ||
| 177 | expect(rewritten).not.toContain("\"secrets\""); | ||
| 178 | expect(loaded.projectConfig?.sources?.github?.config).toEqual({ | ||
| 179 | specUrl: "https://example.com/openapi.json", | ||
| 180 | baseUrl: "https://api.github.com", | ||
| 181 | auth: { | ||
| 182 | kind: "none", | ||
| 183 | }, | ||
| 184 | defaultHeaders: null, | ||
| 185 | }); | ||
| 186 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 187 | ); | ||
| 188 | }); | ||
| @@ -0,0 +1,48 @@ | |||
| 1 | import { FileSystem } from "@effect/platform"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | import type { | ||
| 4 | ExecutorScopeConfigDecodeError, | ||
| 5 | LocalFileSystemError, | ||
| 6 | } from "../errors"; | ||
| 7 | import { | ||
| 8 | migrateLegacyExecutorScopeConfigs, | ||
| 9 | type StartupConfigMigrationContext, | ||
| 10 | } from "./legacy-local-config"; | ||
| 11 | |||
| 12 | type StartupConfigMigration = ( | ||
| 13 | context: StartupConfigMigrationContext, | ||
| 14 | ) => Effect.Effect< | ||
| 15 | ReadonlyArray<string>, | ||
| 16 | LocalFileSystemError | ExecutorScopeConfigDecodeError, | ||
| 17 | FileSystem.FileSystem | ||
| 18 | >; | ||
| 19 | |||
| 20 | // Keep legacy upgrade steps out of steady-state config loading so they can be | ||
| 21 | // extended or deleted without touching the main application path. | ||
| 22 | const startupConfigMigrations: readonly StartupConfigMigration[] = [ | ||
| 23 | migrateLegacyExecutorScopeConfigs, | ||
| 24 | ]; | ||
| 25 | |||
| 26 | export const runStartupConfigMigrations = ( | ||
| 27 | context: StartupConfigMigrationContext, | ||
| 28 | ): Effect.Effect< | ||
| 29 | ReadonlyArray<string>, | ||
| 30 | LocalFileSystemError | ExecutorScopeConfigDecodeError, | ||
| 31 | FileSystem.FileSystem | ||
| 32 | > => | ||
| 33 | Effect.gen(function* () { | ||
| 34 | const migratedPaths = new Set<string>(); | ||
| 35 | |||
| 36 | for (const migrate of startupConfigMigrations) { | ||
| 37 | for (const path of yield* migrate(context)) { | ||
| 38 | migratedPaths.add(path); | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | return [...migratedPaths]; | ||
| 43 | }); | ||
| 44 | |||
| 45 | export { | ||
| 46 | migrateLegacyExecutorScopeConfigs, | ||
| 47 | type StartupConfigMigrationContext, | ||
| 48 | } from "./legacy-local-config"; | ||
| @@ -0,0 +1,538 @@ | |||
| 1 | import { FileSystem } from "@effect/platform"; | ||
| 2 | import { | ||
| 3 | type ExecutorScopeConfigSource, | ||
| 4 | type ExecutorScopeConfig, | ||
| 5 | } from "@executor/platform-sdk/schema"; | ||
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | import { | ||
| 8 | decodeExecutorScopeConfig, | ||
| 9 | encodeExecutorScopeConfig, | ||
| 10 | parseJsoncValue, | ||
| 11 | } from "../config-codec"; | ||
| 12 | import { | ||
| 13 | ExecutorScopeConfigDecodeError, | ||
| 14 | LocalFileSystemError, | ||
| 15 | unknownLocalErrorDetails, | ||
| 16 | } from "../errors"; | ||
| 17 | |||
| 18 | const LEGACY_CONFIG_BACKUP_SUFFIX = ".legacy-backup"; | ||
| 19 | |||
| 20 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => | ||
| 21 | new LocalFileSystemError({ | ||
| 22 | message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 23 | action, | ||
| 24 | path, | ||
| 25 | details: unknownLocalErrorDetails(cause), | ||
| 26 | }); | ||
| 27 | |||
| 28 | const asRecord = (value: unknown): Record<string, unknown> | null => | ||
| 29 | typeof value === "object" && value !== null && !Array.isArray(value) | ||
| 30 | ? value as Record<string, unknown> | ||
| 31 | : null; | ||
| 32 | |||
| 33 | const asString = (value: unknown): string | null => | ||
| 34 | typeof value === "string" && value.trim().length > 0 ? value.trim() : null; | ||
| 35 | |||
| 36 | const asStringArray = (value: unknown): Array<string> | null => { | ||
| 37 | if (!Array.isArray(value)) { | ||
| 38 | return null; | ||
| 39 | } | ||
| 40 | |||
| 41 | const normalized = value | ||
| 42 | .map((entry) => asString(entry)) | ||
| 43 | .filter((entry): entry is string => entry !== null); | ||
| 44 | |||
| 45 | return normalized.length > 0 ? normalized : []; | ||
| 46 | }; | ||
| 47 | |||
| 48 | const asStringRecord = (value: unknown): Record<string, string> | null => { | ||
| 49 | const record = asRecord(value); | ||
| 50 | if (!record) { | ||
| 51 | return null; | ||
| 52 | } | ||
| 53 | |||
| 54 | const normalized = Object.fromEntries( | ||
| 55 | Object.entries(record).flatMap(([key, entry]) => { | ||
| 56 | const normalizedKey = asString(key); | ||
| 57 | const normalizedValue = asString(entry); | ||
| 58 | return normalizedKey && normalizedValue | ||
| 59 | ? [[normalizedKey, normalizedValue]] | ||
| 60 | : []; | ||
| 61 | }), | ||
| 62 | ); | ||
| 63 | |||
| 64 | return Object.keys(normalized).length > 0 ? normalized : null; | ||
| 65 | }; | ||
| 66 | |||
| 67 | const asFiniteNumber = (value: unknown): number | null => | ||
| 68 | typeof value === "number" && Number.isFinite(value) ? value : null; | ||
| 69 | |||
| 70 | const asBoolean = (value: unknown): boolean | null => | ||
| 71 | typeof value === "boolean" ? value : null; | ||
| 72 | |||
| 73 | const asSecretRef = (value: unknown): { secretId: string } | null => { | ||
| 74 | const record = asRecord(value); | ||
| 75 | const secretId = asString(record?.secretId); | ||
| 76 | return secretId ? { secretId } : null; | ||
| 77 | }; | ||
| 78 | |||
| 79 | const normalizeLegacyHeaderName = (value: unknown): string | null => | ||
| 80 | asString(value); | ||
| 81 | |||
| 82 | const normalizeLegacyMcpTransport = ( | ||
| 83 | value: unknown, | ||
| 84 | ): "streamable-http" | "sse" | "stdio" | "auto" | null => { | ||
| 85 | const candidate = asString(value); | ||
| 86 | return candidate === "streamable-http" | ||
| 87 | || candidate === "sse" | ||
| 88 | || candidate === "stdio" | ||
| 89 | || candidate === "auto" | ||
| 90 | ? candidate | ||
| 91 | : null; | ||
| 92 | }; | ||
| 93 | |||
| 94 | const defaultGoogleDiscoveryUrl = ( | ||
| 95 | service: string, | ||
| 96 | version: string, | ||
| 97 | ): string => | ||
| 98 | `https://www.googleapis.com/discovery/v1/apis/${encodeURIComponent(service)}/${encodeURIComponent(version)}/rest`; | ||
| 99 | |||
| 100 | const legacyMigrationError = (input: { | ||
| 101 | path: string; | ||
| 102 | sourceId: string; | ||
| 103 | details: string; | ||
| 104 | }) => | ||
| 105 | new ExecutorScopeConfigDecodeError({ | ||
| 106 | message: `Unsupported legacy executor config at ${input.path} for source ${input.sourceId}: ${input.details}`, | ||
| 107 | path: input.path, | ||
| 108 | details: input.details, | ||
| 109 | }); | ||
| 110 | |||
| 111 | const normalizeCurrentSourceBase = ( | ||
| 112 | source: Record<string, unknown>, | ||
| 113 | ): Omit<ExecutorScopeConfigSource, "kind" | "config"> => ({ | ||
| 114 | ...(asString(source.name) ? { name: asString(source.name)! } : {}), | ||
| 115 | ...(asString(source.namespace) ? { namespace: asString(source.namespace)! } : {}), | ||
| 116 | ...(asBoolean(source.enabled) !== null ? { enabled: source.enabled as boolean } : {}), | ||
| 117 | }); | ||
| 118 | |||
| 119 | const normalizeLegacyBearerAuth = (input: { | ||
| 120 | path: string; | ||
| 121 | sourceId: string; | ||
| 122 | value: unknown; | ||
| 123 | }) => { | ||
| 124 | const record = asRecord(input.value); | ||
| 125 | const kind = asString(record?.kind); | ||
| 126 | |||
| 127 | if (input.value === undefined || kind === "none") { | ||
| 128 | return { kind: "none" } as const; | ||
| 129 | } | ||
| 130 | |||
| 131 | const tokenSecretRef = asSecretRef(record?.tokenSecretRef ?? input.value); | ||
| 132 | if (!tokenSecretRef) { | ||
| 133 | throw legacyMigrationError({ | ||
| 134 | path: input.path, | ||
| 135 | sourceId: input.sourceId, | ||
| 136 | details: | ||
| 137 | "legacy bearer auth references config providers and cannot be auto-migrated yet", | ||
| 138 | }); | ||
| 139 | } | ||
| 140 | |||
| 141 | return { | ||
| 142 | kind: "bearer" as const, | ||
| 143 | tokenSecretRef, | ||
| 144 | headerName: normalizeLegacyHeaderName(record?.headerName), | ||
| 145 | prefix: asString(record?.prefix), | ||
| 146 | }; | ||
| 147 | }; | ||
| 148 | |||
| 149 | const normalizeLegacyMcpAuth = (input: { | ||
| 150 | path: string; | ||
| 151 | sourceId: string; | ||
| 152 | value: unknown; | ||
| 153 | }) => { | ||
| 154 | const record = asRecord(input.value); | ||
| 155 | const kind = asString(record?.kind); | ||
| 156 | |||
| 157 | if (input.value === undefined || kind === "none") { | ||
| 158 | return { kind: "none" } as const; | ||
| 159 | } | ||
| 160 | |||
| 161 | if (kind !== "oauth2") { | ||
| 162 | throw legacyMigrationError({ | ||
| 163 | path: input.path, | ||
| 164 | sourceId: input.sourceId, | ||
| 165 | details: "legacy MCP auth is not compatible with the current config format", | ||
| 166 | }); | ||
| 167 | } | ||
| 168 | |||
| 169 | const accessTokenRef = asSecretRef(record?.accessTokenRef); | ||
| 170 | const redirectUri = asString(record?.redirectUri); | ||
| 171 | if (!accessTokenRef || !redirectUri) { | ||
| 172 | throw legacyMigrationError({ | ||
| 173 | path: input.path, | ||
| 174 | sourceId: input.sourceId, | ||
| 175 | details: "legacy MCP oauth2 auth is missing required secret refs", | ||
| 176 | }); | ||
| 177 | } | ||
| 178 | |||
| 179 | return { | ||
| 180 | kind: "oauth2" as const, | ||
| 181 | redirectUri, | ||
| 182 | accessTokenRef, | ||
| 183 | refreshTokenRef: asSecretRef(record?.refreshTokenRef), | ||
| 184 | tokenType: asString(record?.tokenType) ?? "Bearer", | ||
| 185 | expiresAt: asFiniteNumber(record?.expiresAt), | ||
| 186 | scope: asString(record?.scope), | ||
| 187 | resourceMetadataUrl: asString(record?.resourceMetadataUrl), | ||
| 188 | authorizationServerUrl: asString(record?.authorizationServerUrl), | ||
| 189 | resourceMetadata: asRecord(record?.resourceMetadata), | ||
| 190 | authorizationServerMetadata: asRecord(record?.authorizationServerMetadata), | ||
| 191 | clientInformation: asRecord(record?.clientInformation), | ||
| 192 | }; | ||
| 193 | }; | ||
| 194 | |||
| 195 | const normalizeLegacyGoogleDiscoveryAuth = (input: { | ||
| 196 | path: string; | ||
| 197 | sourceId: string; | ||
| 198 | value: unknown; | ||
| 199 | }) => { | ||
| 200 | const record = asRecord(input.value); | ||
| 201 | const kind = asString(record?.kind); | ||
| 202 | |||
| 203 | if (kind === "oauth2") { | ||
| 204 | const clientId = asString(record?.clientId); | ||
| 205 | const authorizationEndpoint = asString(record?.authorizationEndpoint); | ||
| 206 | const tokenEndpoint = asString(record?.tokenEndpoint); | ||
| 207 | const accessTokenRef = asSecretRef(record?.accessTokenRef); | ||
| 208 | |||
| 209 | if (!clientId || !authorizationEndpoint || !tokenEndpoint || !accessTokenRef) { | ||
| 210 | throw legacyMigrationError({ | ||
| 211 | path: input.path, | ||
| 212 | sourceId: input.sourceId, | ||
| 213 | details: "legacy Google Discovery oauth2 auth is missing required secret refs", | ||
| 214 | }); | ||
| 215 | } | ||
| 216 | |||
| 217 | const clientAuthentication = | ||
| 218 | asString(record?.clientAuthentication) === "client_secret_post" | ||
| 219 | ? "client_secret_post" | ||
| 220 | : "none"; | ||
| 221 | |||
| 222 | return { | ||
| 223 | kind: "oauth2" as const, | ||
| 224 | clientId, | ||
| 225 | clientSecretRef: asSecretRef(record?.clientSecretRef), | ||
| 226 | clientAuthentication, | ||
| 227 | authorizationEndpoint, | ||
| 228 | tokenEndpoint, | ||
| 229 | scopes: asStringArray(record?.scopes) ?? [], | ||
| 230 | accessTokenRef, | ||
| 231 | refreshTokenRef: asSecretRef(record?.refreshTokenRef), | ||
| 232 | expiresAt: asFiniteNumber(record?.expiresAt), | ||
| 233 | }; | ||
| 234 | } | ||
| 235 | |||
| 236 | return normalizeLegacyBearerAuth(input); | ||
| 237 | }; | ||
| 238 | |||
| 239 | const normalizeSourceRecord = (input: { | ||
| 240 | path: string; | ||
| 241 | sourceId: string; | ||
| 242 | source: Record<string, unknown>; | ||
| 243 | }): { | ||
| 244 | source: ExecutorScopeConfigSource | Record<string, unknown>; | ||
| 245 | migrated: boolean; | ||
| 246 | } => { | ||
| 247 | const kind = asString(input.source.kind); | ||
| 248 | const config = input.source.config; | ||
| 249 | const connection = asRecord(input.source.connection); | ||
| 250 | const binding = asRecord(input.source.binding); | ||
| 251 | const base = normalizeCurrentSourceBase(input.source); | ||
| 252 | |||
| 253 | if (config !== undefined) { | ||
| 254 | return { | ||
| 255 | source: { | ||
| 256 | ...base, | ||
| 257 | ...(kind ? { kind } : {}), | ||
| 258 | config, | ||
| 259 | }, | ||
| 260 | migrated: | ||
| 261 | Object.prototype.hasOwnProperty.call(input.source, "connection") | ||
| 262 | || Object.prototype.hasOwnProperty.call(input.source, "binding"), | ||
| 263 | }; | ||
| 264 | } | ||
| 265 | |||
| 266 | if (!kind || (connection === null && binding === null)) { | ||
| 267 | return { | ||
| 268 | source: input.source, | ||
| 269 | migrated: false, | ||
| 270 | }; | ||
| 271 | } | ||
| 272 | |||
| 273 | switch (kind) { | ||
| 274 | case "openapi": { | ||
| 275 | const baseUrl = asString(connection?.endpoint); | ||
| 276 | const specUrl = asString(binding?.specUrl) ?? baseUrl; | ||
| 277 | if (!specUrl) { | ||
| 278 | return { | ||
| 279 | source: input.source, | ||
| 280 | migrated: false, | ||
| 281 | }; | ||
| 282 | } | ||
| 283 | |||
| 284 | return { | ||
| 285 | source: { | ||
| 286 | ...base, | ||
| 287 | kind: "openapi", | ||
| 288 | config: { | ||
| 289 | specUrl, | ||
| 290 | baseUrl, | ||
| 291 | auth: normalizeLegacyBearerAuth({ | ||
| 292 | path: input.path, | ||
| 293 | sourceId: input.sourceId, | ||
| 294 | value: connection?.auth, | ||
| 295 | }), | ||
| 296 | defaultHeaders: asStringRecord(binding?.defaultHeaders), | ||
| 297 | }, | ||
| 298 | }, | ||
| 299 | migrated: true, | ||
| 300 | }; | ||
| 301 | } | ||
| 302 | |||
| 303 | case "graphql": { | ||
| 304 | const endpoint = asString(connection?.endpoint); | ||
| 305 | if (!endpoint) { | ||
| 306 | return { | ||
| 307 | source: input.source, | ||
| 308 | migrated: false, | ||
| 309 | }; | ||
| 310 | } | ||
| 311 | |||
| 312 | return { | ||
| 313 | source: { | ||
| 314 | ...base, | ||
| 315 | kind: "graphql", | ||
| 316 | config: { | ||
| 317 | endpoint, | ||
| 318 | defaultHeaders: asStringRecord(binding?.defaultHeaders), | ||
| 319 | auth: normalizeLegacyBearerAuth({ | ||
| 320 | path: input.path, | ||
| 321 | sourceId: input.sourceId, | ||
| 322 | value: connection?.auth, | ||
| 323 | }), | ||
| 324 | }, | ||
| 325 | }, | ||
| 326 | migrated: true, | ||
| 327 | }; | ||
| 328 | } | ||
| 329 | |||
| 330 | case "mcp": | ||
| 331 | return { | ||
| 332 | source: { | ||
| 333 | ...base, | ||
| 334 | kind: "mcp", | ||
| 335 | config: { | ||
| 336 | endpoint: asString(connection?.endpoint), | ||
| 337 | transport: normalizeLegacyMcpTransport(binding?.transport), | ||
| 338 | queryParams: asStringRecord(binding?.queryParams), | ||
| 339 | headers: asStringRecord(binding?.headers), | ||
| 340 | command: asString(binding?.command), | ||
| 341 | args: asStringArray(binding?.args), | ||
| 342 | env: asStringRecord(binding?.env), | ||
| 343 | cwd: asString(binding?.cwd), | ||
| 344 | auth: normalizeLegacyMcpAuth({ | ||
| 345 | path: input.path, | ||
| 346 | sourceId: input.sourceId, | ||
| 347 | value: connection?.auth, | ||
| 348 | }), | ||
| 349 | }, | ||
| 350 | }, | ||
| 351 | migrated: true, | ||
| 352 | }; | ||
| 353 | |||
| 354 | case "google_discovery": { | ||
| 355 | const service = asString(binding?.service); | ||
| 356 | const version = asString(binding?.version); | ||
| 357 | if (!service || !version) { | ||
| 358 | return { | ||
| 359 | source: input.source, | ||
| 360 | migrated: false, | ||
| 361 | }; | ||
| 362 | } | ||
| 363 | |||
| 364 | return { | ||
| 365 | source: { | ||
| 366 | ...base, | ||
| 367 | kind: "google_discovery", | ||
| 368 | config: { | ||
| 369 | service, | ||
| 370 | version, | ||
| 371 | discoveryUrl: | ||
| 372 | asString(binding?.discoveryUrl) | ||
| 373 | ?? asString(connection?.endpoint) | ||
| 374 | ?? defaultGoogleDiscoveryUrl(service, version), | ||
| 375 | defaultHeaders: asStringRecord(binding?.defaultHeaders), | ||
| 376 | scopes: asStringArray(binding?.scopes) ?? [], | ||
| 377 | auth: normalizeLegacyGoogleDiscoveryAuth({ | ||
| 378 | path: input.path, | ||
| 379 | sourceId: input.sourceId, | ||
| 380 | value: connection?.auth, | ||
| 381 | }), | ||
| 382 | }, | ||
| 383 | }, | ||
| 384 | migrated: true, | ||
| 385 | }; | ||
| 386 | } | ||
| 387 | |||
| 388 | default: | ||
| 389 | return { | ||
| 390 | source: input.source, | ||
| 391 | migrated: false, | ||
| 392 | }; | ||
| 393 | } | ||
| 394 | }; | ||
| 395 | |||
| 396 | const migrateLegacyExecutorConfigValue = (input: { | ||
| 397 | path: string; | ||
| 398 | value: unknown; | ||
| 399 | }): { | ||
| 400 | config: ExecutorScopeConfig; | ||
| 401 | migrated: boolean; | ||
| 402 | } => { | ||
| 403 | const root = asRecord(input.value); | ||
| 404 | if (root === null) { | ||
| 405 | return { | ||
| 406 | config: decodeExecutorScopeConfig(input.value), | ||
| 407 | migrated: false, | ||
| 408 | }; | ||
| 409 | } | ||
| 410 | |||
| 411 | let migrated = Object.prototype.hasOwnProperty.call(root, "secrets"); | ||
| 412 | const rawSources = asRecord(root.sources); | ||
| 413 | const sources = rawSources | ||
| 414 | ? Object.fromEntries( | ||
| 415 | Object.entries(rawSources).map(([sourceId, sourceValue]) => { | ||
| 416 | const sourceRecord = asRecord(sourceValue); | ||
| 417 | if (sourceRecord === null) { | ||
| 418 | return [sourceId, sourceValue]; | ||
| 419 | } | ||
| 420 | |||
| 421 | const normalized = normalizeSourceRecord({ | ||
| 422 | path: input.path, | ||
| 423 | sourceId, | ||
| 424 | source: sourceRecord, | ||
| 425 | }); | ||
| 426 | migrated ||= normalized.migrated; | ||
| 427 | return [sourceId, normalized.source]; | ||
| 428 | }), | ||
| 429 | ) | ||
| 430 | : root.sources; | ||
| 431 | |||
| 432 | return { | ||
| 433 | config: decodeExecutorScopeConfig({ | ||
| 434 | ...(root.runtime !== undefined ? { runtime: root.runtime } : {}), | ||
| 435 | ...(root.workspace !== undefined ? { workspace: root.workspace } : {}), | ||
| 436 | ...(sources !== undefined ? { sources } : {}), | ||
| 437 | ...(root.policies !== undefined ? { policies: root.policies } : {}), | ||
| 438 | }), | ||
| 439 | migrated, | ||
| 440 | }; | ||
| 441 | }; | ||
| 442 | |||
| 443 | const migrateLegacyExecutorConfigFile = ( | ||
| 444 | path: string, | ||
| 445 | ): Effect.Effect< | ||
| 446 | boolean, | ||
| 447 | LocalFileSystemError | ExecutorScopeConfigDecodeError, | ||
| 448 | FileSystem.FileSystem | ||
| 449 | > => | ||
| 450 | Effect.gen(function* () { | ||
| 451 | const fs = yield* FileSystem.FileSystem; | ||
| 452 | const exists = yield* fs.exists(path).pipe( | ||
| 453 | Effect.mapError(mapFileSystemError(path, "check config path")), | ||
| 454 | ); | ||
| 455 | if (!exists) { | ||
| 456 | return false; | ||
| 457 | } | ||
| 458 | |||
| 459 | const content = yield* fs.readFileString(path, "utf8").pipe( | ||
| 460 | Effect.mapError(mapFileSystemError(path, "read config")), | ||
| 461 | ); | ||
| 462 | const parsed = yield* Effect.try({ | ||
| 463 | try: () => | ||
| 464 | parseJsoncValue({ | ||
| 465 | path, | ||
| 466 | content, | ||
| 467 | }), | ||
| 468 | catch: (cause) => | ||
| 469 | cause instanceof ExecutorScopeConfigDecodeError | ||
| 470 | ? cause | ||
| 471 | : new ExecutorScopeConfigDecodeError({ | ||
| 472 | message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 473 | path, | ||
| 474 | details: unknownLocalErrorDetails(cause), | ||
| 475 | }), | ||
| 476 | }); | ||
| 477 | const migrated = yield* Effect.try({ | ||
| 478 | try: () => | ||
| 479 | migrateLegacyExecutorConfigValue({ | ||
| 480 | path, | ||
| 481 | value: parsed, | ||
| 482 | }), | ||
| 483 | catch: (cause) => | ||
| 484 | cause instanceof ExecutorScopeConfigDecodeError | ||
| 485 | ? cause | ||
| 486 | : new ExecutorScopeConfigDecodeError({ | ||
| 487 | message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 488 | path, | ||
| 489 | details: unknownLocalErrorDetails(cause), | ||
| 490 | }), | ||
| 491 | }); | ||
| 492 | |||
| 493 | if (!migrated.migrated) { | ||
| 494 | return false; | ||
| 495 | } | ||
| 496 | |||
| 497 | const backupPath = `${path}${LEGACY_CONFIG_BACKUP_SUFFIX}`; | ||
| 498 | const backupExists = yield* fs.exists(backupPath).pipe( | ||
| 499 | Effect.mapError(mapFileSystemError(backupPath, "check backup path")), | ||
| 500 | ); | ||
| 501 | if (!backupExists) { | ||
| 502 | yield* fs.writeFileString(backupPath, content).pipe( | ||
| 503 | Effect.mapError(mapFileSystemError(backupPath, "write legacy config backup")), | ||
| 504 | ); | ||
| 505 | } | ||
| 506 | |||
| 507 | yield* fs.writeFileString(path, encodeExecutorScopeConfig(migrated.config)).pipe( | ||
| 508 | Effect.mapError(mapFileSystemError(path, "write migrated config")), | ||
| 509 | ); | ||
| 510 | |||
| 511 | return true; | ||
| 512 | }); | ||
| 513 | |||
| 514 | export type StartupConfigMigrationContext = { | ||
| 515 | homeConfigPath: string; | ||
| 516 | projectConfigPath: string; | ||
| 517 | }; | ||
| 518 | |||
| 519 | export const migrateLegacyExecutorScopeConfigs = ( | ||
| 520 | context: StartupConfigMigrationContext, | ||
| 521 | ): Effect.Effect< | ||
| 522 | ReadonlyArray<string>, | ||
| 523 | LocalFileSystemError | ExecutorScopeConfigDecodeError, | ||
| 524 | FileSystem.FileSystem | ||
| 525 | > => | ||
| 526 | Effect.gen(function* () { | ||
| 527 | const migratedPaths: Array<string> = []; | ||
| 528 | const paths = [...new Set([context.homeConfigPath, context.projectConfigPath])]; | ||
| 529 | |||
| 530 | for (const path of paths) { | ||
| 531 | const migrated = yield* migrateLegacyExecutorConfigFile(path); | ||
| 532 | if (migrated) { | ||
| 533 | migratedPaths.push(path); | ||
| 534 | } | ||
| 535 | } | ||
| 536 | |||
| 537 | return migratedPaths; | ||
| 538 | }); | ||
| @@ -6,8 +6,8 @@ import { describe, expect, it } from "@effect/vitest"; · 5 unmodified lines | |||
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 7 | 7 | ||
| 8 | import { | 8 | import { |
| 9 | loadLocalExecutorConfig, | 9 | loadExecutorScopeConfig, |
| 10 | mergeLocalExecutorConfigs, | 10 | mergeExecutorScopeConfigs, |
| 11 | resolveDefaultHomeConfigCandidates, | 11 | resolveDefaultHomeConfigCandidates, |
| 12 | resolveDefaultHomeStateDirectory, | 12 | resolveDefaultHomeStateDirectory, |
| 13 | resolveLocalWorkspaceContext, | 13 | resolveLocalWorkspaceContext, |
| @@ -53,7 +53,7 @@ describe("local-config", () => { · 39 unmodified lines | |||
| 53 | ); | 53 | ); |
| 54 | 54 | ||
| 55 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); | 55 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); |
| 56 | const loaded = yield* loadLocalExecutorConfig(context); | 56 | const loaded = yield* loadExecutorScopeConfig(context); |
| 57 | 57 | ||
| 58 | expect(loaded.config?.sources?.github?.kind).toBe("openapi"); | 58 | expect(loaded.config?.sources?.github?.kind).toBe("openapi"); |
| 59 | expect(loaded.config?.runtime).toBe("ses"); | 59 | expect(loaded.config?.runtime).toBe("ses"); |
| @@ -83,7 +83,7 @@ describe("local-config", () => { · 23 unmodified lines | |||
| 83 | ); | 83 | ); |
| 84 | 84 | ||
| 85 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); | 85 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); |
| 86 | const failure = yield* Effect.flip(loadLocalExecutorConfig(context)); | 86 | const failure = yield* Effect.flip(loadExecutorScopeConfig(context)); |
| 87 | 87 | ||
| 88 | expect(failure.message).toContain("Invalid executor config"); | 88 | expect(failure.message).toContain("Invalid executor config"); |
| 89 | expect(failure.message).toContain("line 5, column 7"); | 89 | expect(failure.message).toContain("line 5, column 7"); |
| @@ -129,7 +129,7 @@ describe("local-config", () => { · 39 unmodified lines | |||
| 129 | }); | 129 | }); |
| 130 | 130 | ||
| 131 | it("lets project config override the merged runtime", () => { | 131 | it("lets project config override the merged runtime", () => { |
| 132 | const merged = mergeLocalExecutorConfigs( | 132 | const merged = mergeExecutorScopeConfigs( |
| 133 | { | 133 | { |
| 134 | runtime: "quickjs", | 134 | runtime: "quickjs", |
| 135 | sources: {}, | 135 | sources: {}, |
| @@ -1,37 +1,30 @@ | |||
| 1 | import { homedir } from "node:os"; | 1 | import { homedir } from "node:os"; |
| 2 | import { basename, dirname, isAbsolute, join, resolve } from "node:path"; | 2 | import { basename, dirname, isAbsolute, join, resolve } from "node:path"; |
| 3 | import { FileSystem } from "@effect/platform"; | 3 | import { FileSystem } from "@effect/platform"; |
| 4 | import { | ||
| 5 | type ParseError as JsoncParseError, | ||
| 6 | parse as parseJsoncDocument, | ||
| 7 | printParseErrorCode, | ||
| 8 | } from "jsonc-parser/lib/esm/main.js"; | ||
| 9 | 4 | ||
| 10 | import { | 5 | import { |
| 11 | LocalExecutorConfigSchema, | 6 | type ExecutorScopeConfig, |
| 12 | type LocalExecutorConfig, | 7 | type ExecutorScopeConfigPolicy, |
| 13 | type LocalConfigPolicy, | 8 | type ExecutorScopeConfigSource, |
| 14 | type LocalConfigSource, | ||
| 15 | } from "@executor/platform-sdk/schema"; | 9 | } from "@executor/platform-sdk/schema"; |
| 16 | import type { LoadedLocalExecutorConfig } from "@executor/platform-sdk/runtime"; | 10 | import type { LoadedExecutorScopeConfig } from "@executor/platform-sdk/runtime"; |
| 17 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 18 | import * as Schema from "effect/Schema"; | ||
| 19 | import { | 12 | import { |
| 20 | LocalExecutorConfigDecodeError, | 13 | decodeExecutorScopeConfig, |
| 14 | encodeExecutorScopeConfig, | ||
| 15 | parseExecutorScopeConfig, | ||
| 16 | } from "./config-codec"; | ||
| 17 | import { | ||
| 18 | ExecutorScopeConfigDecodeError, | ||
| 21 | LocalFileSystemError, | 19 | LocalFileSystemError, |
| 22 | unknownLocalErrorDetails, | 20 | unknownLocalErrorDetails, |
| 23 | } from "./errors"; | 21 | } from "./errors"; |
| 24 | 22 | ||
| 25 | const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema); | 23 | export type FileLoadedExecutorConfig = LoadedExecutorScopeConfig & { |
| 26 | |||
| 27 | export type FileLoadedExecutorConfig = LoadedLocalExecutorConfig & { | ||
| 28 | homeConfigPath: string; | 24 | homeConfigPath: string; |
| 29 | projectConfigPath: string; | 25 | projectConfigPath: string; |
| 30 | }; | 26 | }; |
| 31 | 27 | ||
| 32 | export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string => | ||
| 33 | `${JSON.stringify(config, null, 2)}\n`; | ||
| 34 | |||
| 35 | export const resolveConfigRelativePath = (input: { | 28 | export const resolveConfigRelativePath = (input: { |
| 36 | path: string; | 29 | path: string; |
| 37 | scopeRoot: string; | 30 | scopeRoot: string; |
| @@ -49,7 +42,8 @@ export const resolveConfigRelativePath = (input: { · 11 unmodified lines | |||
| 49 | return resolve(input.scopeRoot, trimmed); | 42 | return resolve(input.scopeRoot, trimmed); |
| 50 | }; | 43 | }; |
| 51 | 44 | ||
| 52 | export type { LoadedLocalExecutorConfig } from "@executor/platform-sdk/runtime"; | 45 | export type { LoadedExecutorScopeConfig } from "@executor/platform-sdk/runtime"; |
| 46 | export { encodeExecutorScopeConfig } from "./config-codec"; | ||
| 53 | 47 | ||
| 54 | const PROJECT_CONFIG_BASENAME = "executor.jsonc"; | 48 | const PROJECT_CONFIG_BASENAME = "executor.jsonc"; |
| 55 | const PROJECT_CONFIG_DIRECTORY = ".executor"; | 49 | const PROJECT_CONFIG_DIRECTORY = ".executor"; |
| @@ -173,59 +167,10 @@ export const resolveDefaultHomeStateDirectory = (input: { · 117 unmodified lines | |||
| 173 | } = {}): string => | 167 | } = {}): string => |
| 174 | defaultExecutorStateDirectory(input); | 168 | defaultExecutorStateDirectory(input); |
| 175 | 169 | ||
| 176 | const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => { | ||
| 177 | const lines = content.split("\n"); | ||
| 178 | |||
| 179 | return errors | ||
| 180 | .map((error) => { | ||
| 181 | const beforeOffset = content.slice(0, error.offset).split("\n"); | ||
| 182 | const line = beforeOffset.length; | ||
| 183 | const column = beforeOffset[beforeOffset.length - 1]?.length ?? 0; | ||
| 184 | const lineText = lines[line - 1]; | ||
| 185 | const location = `line ${line}, column ${column + 1}`; | ||
| 186 | const detail = printParseErrorCode(error.error); | ||
| 187 | |||
| 188 | if (!lineText) { | ||
| 189 | return `${detail} at ${location}`; | ||
| 190 | } | ||
| 191 | |||
| 192 | return `${detail} at ${location}\n${lineText}`; | ||
| 193 | }) | ||
| 194 | .join("\n"); | ||
| 195 | }; | ||
| 196 | |||
| 197 | const parseJsonc = (input: { path: string; content: string }): LocalExecutorConfig => { | ||
| 198 | const errors: JsoncParseError[] = []; | ||
| 199 | |||
| 200 | try { | ||
| 201 | const parsed = parseJsoncDocument(input.content, errors, { | ||
| 202 | allowTrailingComma: true, | ||
| 203 | }); | ||
| 204 | if (errors.length > 0) { | ||
| 205 | throw new LocalExecutorConfigDecodeError({ | ||
| 206 | message: `Invalid executor config at ${input.path}: ${formatJsoncParseErrors(input.content, errors)}`, | ||
| 207 | path: input.path, | ||
| 208 | details: formatJsoncParseErrors(input.content, errors), | ||
| 209 | }); | ||
| 210 | } | ||
| 211 | |||
| 212 | return decodeLocalExecutorConfig(parsed); | ||
| 213 | } catch (cause) { | ||
| 214 | if (cause instanceof LocalExecutorConfigDecodeError) { | ||
| 215 | throw cause; | ||
| 216 | } | ||
| 217 | throw new LocalExecutorConfigDecodeError({ | ||
| 218 | message: `Invalid executor config at ${input.path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 219 | path: input.path, | ||
| 220 | details: unknownLocalErrorDetails(cause), | ||
| 221 | }); | ||
| 222 | } | ||
| 223 | }; | ||
| 224 | |||
| 225 | const mergeSourceMaps = ( | 170 | const mergeSourceMaps = ( |
| 226 | base: Record<string, LocalConfigSource> | undefined, | 171 | base: Record<string, ExecutorScopeConfigSource> | undefined, |
| 227 | extra: Record<string, LocalConfigSource> | undefined, | 172 | extra: Record<string, ExecutorScopeConfigSource> | undefined, |
| 228 | ): Record<string, LocalConfigSource> | undefined => { | 173 | ): Record<string, ExecutorScopeConfigSource> | undefined => { |
| 229 | if (!base && !extra) { | 174 | if (!base && !extra) { |
| 230 | return undefined; | 175 | return undefined; |
| 231 | } | 176 | } |
| @@ -236,9 +181,9 @@ const mergeSourceMaps = ( · 4 unmodified lines | |||
| 236 | }; | 181 | }; |
| 237 | 182 | ||
| 238 | const mergePolicyMaps = ( | 183 | const mergePolicyMaps = ( |
| 239 | base: Record<string, LocalConfigPolicy> | undefined, | 184 | base: Record<string, ExecutorScopeConfigPolicy> | undefined, |
| 240 | extra: Record<string, LocalConfigPolicy> | undefined, | 185 | extra: Record<string, ExecutorScopeConfigPolicy> | undefined, |
| 241 | ): Record<string, LocalConfigPolicy> | undefined => { | 186 | ): Record<string, ExecutorScopeConfigPolicy> | undefined => { |
| 242 | if (!base && !extra) { | 187 | if (!base && !extra) { |
| 243 | return undefined; | 188 | return undefined; |
| 244 | } | 189 | } |
| @@ -248,15 +193,15 @@ const mergePolicyMaps = ( · 3 unmodified lines | |||
| 248 | }; | 193 | }; |
| 249 | }; | 194 | }; |
| 250 | 195 | ||
| 251 | export const mergeLocalExecutorConfigs = ( | 196 | export const mergeExecutorScopeConfigs = ( |
| 252 | base: LocalExecutorConfig | null, | 197 | base: ExecutorScopeConfig | null, |
| 253 | extra: LocalExecutorConfig | null, | 198 | extra: ExecutorScopeConfig | null, |
| 254 | ): LocalExecutorConfig | null => { | 199 | ): ExecutorScopeConfig | null => { |
| 255 | if (!base && !extra) { | 200 | if (!base && !extra) { |
| 256 | return null; | 201 | return null; |
| 257 | } | 202 | } |
| 258 | 203 | ||
| 259 | return decodeLocalExecutorConfig({ | 204 | return decodeExecutorScopeConfig({ |
| 260 | runtime: extra?.runtime ?? base?.runtime, | 205 | runtime: extra?.runtime ?? base?.runtime, |
| 261 | workspace: { | 206 | workspace: { |
| 262 | ...base?.workspace, | 207 | ...base?.workspace, |
| @@ -370,11 +315,11 @@ export const resolveLocalWorkspaceContext = (input: { · 107 unmodified lines | |||
| 370 | }; | 315 | }; |
| 371 | }); | 316 | }); |
| 372 | 317 | ||
| 373 | export const readOptionalLocalExecutorConfig = ( | 318 | export const readOptionalExecutorScopeConfig = ( |
| 374 | path: string, | 319 | path: string, |
| 375 | ): Effect.Effect< | 320 | ): Effect.Effect< |
| 376 | LocalExecutorConfig | null, | 321 | ExecutorScopeConfig | null, |
| 377 | LocalFileSystemError | LocalExecutorConfigDecodeError, | 322 | LocalFileSystemError | ExecutorScopeConfigDecodeError, |
| 378 | FileSystem.FileSystem | 323 | FileSystem.FileSystem |
| 379 | > => | 324 | > => |
| 380 | Effect.gen(function* () { | 325 | Effect.gen(function* () { |
| @@ -390,11 +335,11 @@ export const readOptionalLocalExecutorConfig = ( · 9 unmodified lines | |||
| 390 | Effect.mapError(mapFileSystemError(path, "read config")), | 335 | Effect.mapError(mapFileSystemError(path, "read config")), |
| 391 | ); | 336 | ); |
| 392 | return yield* Effect.try({ | 337 | return yield* Effect.try({ |
| 393 | try: () => parseJsonc({ path, content }), | 338 | try: () => parseExecutorScopeConfig({ path, content }), |
| 394 | catch: (cause) => | 339 | catch: (cause) => |
| 395 | cause instanceof LocalExecutorConfigDecodeError | 340 | cause instanceof ExecutorScopeConfigDecodeError |
| 396 | ? cause | 341 | ? cause |
| 397 | : new LocalExecutorConfigDecodeError({ | 342 | : new ExecutorScopeConfigDecodeError({ |
| 398 | message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`, | 343 | message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 399 | path, | 344 | path, |
| 400 | details: unknownLocalErrorDetails(cause), | 345 | details: unknownLocalErrorDetails(cause), |
| @@ -402,21 +347,21 @@ export const readOptionalLocalExecutorConfig = ( · 1 unmodified line | |||
| 402 | }); | 347 | }); |
| 403 | }); | 348 | }); |
| 404 | 349 | ||
| 405 | export const loadLocalExecutorConfig = ( | 350 | export const loadExecutorScopeConfig = ( |
| 406 | context: ResolvedLocalWorkspaceContext, | 351 | context: ResolvedLocalWorkspaceContext, |
| 407 | ): Effect.Effect< | 352 | ): Effect.Effect< |
| 408 | FileLoadedExecutorConfig, | 353 | FileLoadedExecutorConfig, |
| 409 | LocalFileSystemError | LocalExecutorConfigDecodeError, | 354 | LocalFileSystemError | ExecutorScopeConfigDecodeError, |
| 410 | FileSystem.FileSystem | 355 | FileSystem.FileSystem |
| 411 | > => | 356 | > => |
| 412 | Effect.gen(function* () { | 357 | Effect.gen(function* () { |
| 413 | const [homeConfig, projectConfig] = yield* Effect.all([ | 358 | const [homeConfig, projectConfig] = yield* Effect.all([ |
| 414 | readOptionalLocalExecutorConfig(context.homeConfigPath), | 359 | readOptionalExecutorScopeConfig(context.homeConfigPath), |
| 415 | readOptionalLocalExecutorConfig(context.projectConfigPath), | 360 | readOptionalExecutorScopeConfig(context.projectConfigPath), |
| 416 | ]); | 361 | ]); |
| 417 | 362 | ||
| 418 | return { | 363 | return { |
| 419 | config: mergeLocalExecutorConfigs(homeConfig, projectConfig), | 364 | config: mergeExecutorScopeConfigs(homeConfig, projectConfig), |
| 420 | homeConfig, | 365 | homeConfig, |
| 421 | projectConfig, | 366 | projectConfig, |
| 422 | homeConfigPath: context.homeConfigPath, | 367 | homeConfigPath: context.homeConfigPath, |
| @@ -424,9 +369,9 @@ export const loadLocalExecutorConfig = ( · 1 unmodified line | |||
| 424 | }; | 369 | }; |
| 425 | }); | 370 | }); |
| 426 | 371 | ||
| 427 | export const writeProjectLocalExecutorConfig = (input: { | 372 | export const writeProjectExecutorScopeConfig = (input: { |
| 428 | context: ResolvedLocalWorkspaceContext; | 373 | context: ResolvedLocalWorkspaceContext; |
| 429 | config: LocalExecutorConfig; | 374 | config: ExecutorScopeConfig; |
| 430 | }): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> => | 375 | }): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> => |
| 431 | Effect.gen(function* () { | 376 | Effect.gen(function* () { |
| 432 | const fs = yield* FileSystem.FileSystem; | 377 | const fs = yield* FileSystem.FileSystem; |
| @@ -435,7 +380,7 @@ export const writeProjectLocalExecutorConfig = (input: { · 2 unmodified lines | |||
| 435 | ); | 380 | ); |
| 436 | yield* fs.writeFileString( | 381 | yield* fs.writeFileString( |
| 437 | input.context.projectConfigPath, | 382 | input.context.projectConfigPath, |
| 438 | encodeLocalExecutorConfig(input.config), | 383 | encodeExecutorScopeConfig(input.config), |
| 439 | ).pipe( | 384 | ).pipe( |
| 440 | Effect.mapError(mapFileSystemError(input.context.projectConfigPath, "write config")), | 385 | Effect.mapError(mapFileSystemError(input.context.projectConfigPath, "write config")), |
| 441 | ); | 386 | ); |
| @@ -5,7 +5,7 @@ export { · 4 unmodified lines | |||
| 5 | createLocalExecutorRepositoriesEffect, | 5 | createLocalExecutorRepositoriesEffect, |
| 6 | deriveLocalInstallation, | 6 | deriveLocalInstallation, |
| 7 | getOrProvisionLocalInstallation, | 7 | getOrProvisionLocalInstallation, |
| 8 | loadLocalExecutorConfig, | 8 | loadExecutorScopeConfig, |
| 9 | loadLocalExecutorStateSnapshot, | 9 | loadLocalExecutorStateSnapshot, |
| 10 | loadLocalInstallation, | 10 | loadLocalInstallation, |
| 11 | loadLocalWorkspaceState, | 11 | loadLocalWorkspaceState, |
| @@ -20,9 +20,9 @@ export { · 8 unmodified lines | |||
| 20 | writeLocalExecutorStateSnapshot, | 20 | writeLocalExecutorStateSnapshot, |
| 21 | writeLocalSourceArtifact, | 21 | writeLocalSourceArtifact, |
| 22 | writeLocalWorkspaceState, | 22 | writeLocalWorkspaceState, |
| 23 | writeProjectLocalExecutorConfig, | 23 | writeProjectExecutorScopeConfig, |
| 24 | } from "./index"; | 24 | } from "./index"; |
| 25 | export type { | 25 | export type { |
| 26 | LoadedLocalExecutorConfig, | 26 | LoadedExecutorScopeConfig, |
| 27 | ResolvedLocalWorkspaceContext, | 27 | ResolvedLocalWorkspaceContext, |
| 28 | } from "./index"; | 28 | } from "./index"; |
| @@ -12,8 +12,8 @@ export class LocalFileSystemError extends Data.TaggedError( · 11 unmodified lines | |||
| 12 | readonly details: string; | 12 | readonly details: string; |
| 13 | }> {} | 13 | }> {} |
| 14 | 14 | ||
| 15 | export class LocalExecutorConfigDecodeError extends Data.TaggedError( | 15 | export class ExecutorScopeConfigDecodeError extends Data.TaggedError( |
| 16 | "LocalExecutorConfigDecodeError", | 16 | "ExecutorScopeConfigDecodeError", |
| 17 | )<{ | 17 | )<{ |
| 18 | readonly message: string; | 18 | readonly message: string; |
| 19 | readonly path: string; | 19 | readonly path: string; |
| @@ -0,0 +1,82 @@ | |||
| 1 | import { join } from "node:path"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | type Execution, | ||
| 5 | type ExecutionInteraction, | ||
| 6 | type ExecutionStep, | ||
| 7 | ExecutionInteractionSchema, | ||
| 8 | ExecutionSchema, | ||
| 9 | ExecutionStepSchema, | ||
| 10 | SecretMaterialSchema, | ||
| 11 | type SecretMaterial, | ||
| 12 | SecretStoreSchema, | ||
| 13 | type SecretStore, | ||
| 14 | } from "@executor/platform-sdk/schema"; | ||
| 15 | import * as Schema from "effect/Schema"; | ||
| 16 | |||
| 17 | import type { ResolvedLocalWorkspaceContext } from "./config"; | ||
| 18 | import { deriveLocalInstallation } from "./installation"; | ||
| 19 | |||
| 20 | export const LOCAL_EXECUTOR_STATE_VERSION = 2 as const; | ||
| 21 | export const LOCAL_EXECUTOR_STATE_BASENAME = "executor-state.json"; | ||
| 22 | |||
| 23 | export const SecretMaterialStoredDataRecordSchema = Schema.Struct({ | ||
| 24 | secretId: Schema.String, | ||
| 25 | data: Schema.Unknown, | ||
| 26 | }); | ||
| 27 | |||
| 28 | export type SecretMaterialStoredDataRecord = | ||
| 29 | typeof SecretMaterialStoredDataRecordSchema.Type; | ||
| 30 | |||
| 31 | export const LocalExecutorStateSnapshotSchema = Schema.Struct({ | ||
| 32 | version: Schema.Literal(LOCAL_EXECUTOR_STATE_VERSION), | ||
| 33 | secretStores: Schema.Array(SecretStoreSchema), | ||
| 34 | secretMaterials: Schema.Array(SecretMaterialSchema), | ||
| 35 | secretMaterialStoredData: Schema.Array(SecretMaterialStoredDataRecordSchema), | ||
| 36 | executions: Schema.Array(ExecutionSchema), | ||
| 37 | executionInteractions: Schema.Array(ExecutionInteractionSchema), | ||
| 38 | executionSteps: Schema.Array(ExecutionStepSchema), | ||
| 39 | }); | ||
| 40 | |||
| 41 | export type LocalExecutorStateSnapshot = | ||
| 42 | typeof LocalExecutorStateSnapshotSchema.Type; | ||
| 43 | |||
| 44 | export const decodeLocalExecutorStateSnapshot = Schema.decodeUnknownSync( | ||
| 45 | LocalExecutorStateSnapshotSchema, | ||
| 46 | { | ||
| 47 | onExcessProperty: "error", | ||
| 48 | }, | ||
| 49 | ); | ||
| 50 | |||
| 51 | export const encodeLocalExecutorStateSnapshot = ( | ||
| 52 | state: LocalExecutorStateSnapshot, | ||
| 53 | ): string => `${JSON.stringify(state, null, 2)}\n`; | ||
| 54 | |||
| 55 | export const defaultLocalExecutorStateSnapshot = | ||
| 56 | (): LocalExecutorStateSnapshot => ({ | ||
| 57 | version: LOCAL_EXECUTOR_STATE_VERSION, | ||
| 58 | secretStores: [], | ||
| 59 | secretMaterials: [], | ||
| 60 | secretMaterialStoredData: [], | ||
| 61 | executions: [], | ||
| 62 | executionInteractions: [], | ||
| 63 | executionSteps: [], | ||
| 64 | }); | ||
| 65 | |||
| 66 | export const localExecutorStatePath = ( | ||
| 67 | context: ResolvedLocalWorkspaceContext, | ||
| 68 | ): string => | ||
| 69 | join( | ||
| 70 | context.homeStateDirectory, | ||
| 71 | "workspaces", | ||
| 72 | deriveLocalInstallation(context).scopeId, | ||
| 73 | LOCAL_EXECUTOR_STATE_BASENAME, | ||
| 74 | ); | ||
| 75 | |||
| 76 | export type { | ||
| 77 | Execution, | ||
| 78 | ExecutionInteraction, | ||
| 79 | ExecutionStep, | ||
| 80 | SecretMaterial, | ||
| 81 | SecretStore, | ||
| 82 | }; | ||
| @@ -0,0 +1,231 @@ | |||
| 1 | import { dirname, join } from "node:path"; | ||
| 2 | |||
| 3 | import { FileSystem } from "@effect/platform"; | ||
| 4 | import { NodeFileSystem } from "@effect/platform-node"; | ||
| 5 | import { describe, expect, it } from "@effect/vitest"; | ||
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | |||
| 8 | import { resolveLocalWorkspaceContext } from "./config"; | ||
| 9 | import { | ||
| 10 | createLocalExecutorRepositoriesEffect, | ||
| 11 | loadLocalExecutorStateSnapshot, | ||
| 12 | } from "./index"; | ||
| 13 | import { migrateLegacyLocalExecutorState } from "./executor-state-migrations"; | ||
| 14 | import { localExecutorStatePath } from "./executor-state-store"; | ||
| 15 | import { deriveLocalInstallation } from "./installation"; | ||
| 16 | |||
| 17 | const makeWorkspaceRoot = () => | ||
| 18 | FileSystem.FileSystem.pipe( | ||
| 19 | Effect.flatMap((fs) => | ||
| 20 | fs.makeTempDirectory({ | ||
| 21 | prefix: "executor-state-migrations-", | ||
| 22 | }) | ||
| 23 | ), | ||
| 24 | ); | ||
| 25 | |||
| 26 | describe("executor-state-migrations", () => { | ||
| 27 | it.effect("rewrites legacy executor state into the current snapshot shape", () => | ||
| 28 | Effect.gen(function* () { | ||
| 29 | const fs = yield* FileSystem.FileSystem; | ||
| 30 | const workspaceRoot = yield* makeWorkspaceRoot(); | ||
| 31 | const homeStateDirectory = join(workspaceRoot, ".home"); | ||
| 32 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 33 | workspaceRoot, | ||
| 34 | homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"), | ||
| 35 | homeStateDirectory, | ||
| 36 | }); | ||
| 37 | const scopeId = deriveLocalInstallation(context).scopeId; | ||
| 38 | const path = localExecutorStatePath(context); | ||
| 39 | yield* fs.makeDirectory(dirname(path), { recursive: true }); | ||
| 40 | yield* fs.writeFileString( | ||
| 41 | path, | ||
| 42 | `${JSON.stringify({ | ||
| 43 | version: 1, | ||
| 44 | secretStores: [ | ||
| 45 | { | ||
| 46 | id: "sts_builtin_local", | ||
| 47 | scopeId, | ||
| 48 | name: "Local Store", | ||
| 49 | kind: "local", | ||
| 50 | status: "connected", | ||
| 51 | enabled: true, | ||
| 52 | createdAt: 1, | ||
| 53 | updatedAt: 1, | ||
| 54 | }, | ||
| 55 | { | ||
| 56 | id: "sts_builtin_keychain", | ||
| 57 | scopeId, | ||
| 58 | name: "Desktop Keyring", | ||
| 59 | kind: "keychain", | ||
| 60 | status: "connected", | ||
| 61 | enabled: true, | ||
| 62 | createdAt: 2, | ||
| 63 | updatedAt: 2, | ||
| 64 | }, | ||
| 65 | ], | ||
| 66 | secretMaterials: [ | ||
| 67 | { | ||
| 68 | id: "sec_local", | ||
| 69 | name: "Local Secret", | ||
| 70 | purpose: "auth_material", | ||
| 71 | storeId: "sts_builtin_local", | ||
| 72 | handle: "local:abc", | ||
| 73 | value: "token-from-local", | ||
| 74 | createdAt: 3, | ||
| 75 | updatedAt: 4, | ||
| 76 | }, | ||
| 77 | { | ||
| 78 | id: "sec_keychain", | ||
| 79 | name: "Keychain Secret", | ||
| 80 | purpose: "auth_material", | ||
| 81 | storeId: "sts_builtin_keychain", | ||
| 82 | handle: "account-123", | ||
| 83 | value: null, | ||
| 84 | createdAt: 5, | ||
| 85 | updatedAt: 6, | ||
| 86 | }, | ||
| 87 | ], | ||
| 88 | executions: [ | ||
| 89 | { | ||
| 90 | id: "exe_1", | ||
| 91 | workspaceId: scopeId, | ||
| 92 | createdByAccountId: scopeId, | ||
| 93 | status: "completed", | ||
| 94 | code: "1 + 1", | ||
| 95 | resultJson: "2", | ||
| 96 | errorText: null, | ||
| 97 | logsJson: null, | ||
| 98 | startedAt: 10, | ||
| 99 | completedAt: 11, | ||
| 100 | createdAt: 10, | ||
| 101 | updatedAt: 11, | ||
| 102 | }, | ||
| 103 | ], | ||
| 104 | executionInteractions: [], | ||
| 105 | executionSteps: [], | ||
| 106 | }, null, 2)}\n`, | ||
| 107 | ); | ||
| 108 | |||
| 109 | const beforeMigration = yield* Effect.flip(loadLocalExecutorStateSnapshot(context)); | ||
| 110 | expect(beforeMigration.message).toContain("Expected 2, actual 1"); | ||
| 111 | |||
| 112 | const migratedPaths = yield* migrateLegacyLocalExecutorState(context); | ||
| 113 | expect(migratedPaths).toContain(path); | ||
| 114 | expect(yield* fs.exists(`${path}.legacy-backup`)).toBe(true); | ||
| 115 | |||
| 116 | const loaded = yield* loadLocalExecutorStateSnapshot(context); | ||
| 117 | const rewritten = yield* fs.readFileString(path, "utf8"); | ||
| 118 | |||
| 119 | expect(rewritten).not.toContain("\"handle\""); | ||
| 120 | expect(rewritten).not.toContain("\"workspaceId\""); | ||
| 121 | expect(rewritten).not.toContain("\"createdByAccountId\""); | ||
| 122 | expect(loaded.secretMaterials).toEqual([ | ||
| 123 | { | ||
| 124 | id: "sec_local", | ||
| 125 | name: "Local Secret", | ||
| 126 | purpose: "auth_material", | ||
| 127 | storeId: "sts_builtin_local", | ||
| 128 | createdAt: 3, | ||
| 129 | updatedAt: 4, | ||
| 130 | }, | ||
| 131 | { | ||
| 132 | id: "sec_keychain", | ||
| 133 | name: "Keychain Secret", | ||
| 134 | purpose: "auth_material", | ||
| 135 | storeId: "sts_builtin_keychain", | ||
| 136 | createdAt: 5, | ||
| 137 | updatedAt: 6, | ||
| 138 | }, | ||
| 139 | ]); | ||
| 140 | expect(loaded.secretMaterialStoredData).toEqual([ | ||
| 141 | { | ||
| 142 | secretId: "sec_local", | ||
| 143 | data: { | ||
| 144 | value: "token-from-local", | ||
| 145 | }, | ||
| 146 | }, | ||
| 147 | { | ||
| 148 | secretId: "sec_keychain", | ||
| 149 | data: { | ||
| 150 | account: "account-123", | ||
| 151 | }, | ||
| 152 | }, | ||
| 153 | ]); | ||
| 154 | expect(loaded.executions[0]).toMatchObject({ | ||
| 155 | scopeId, | ||
| 156 | createdByScopeId: scopeId, | ||
| 157 | }); | ||
| 158 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 159 | ); | ||
| 160 | |||
| 161 | it.effect("runs executor state migrations before startup loads the state store", () => | ||
| 162 | Effect.gen(function* () { | ||
| 163 | const fs = yield* FileSystem.FileSystem; | ||
| 164 | const workspaceRoot = yield* makeWorkspaceRoot(); | ||
| 165 | const homeStateDirectory = join(workspaceRoot, ".home"); | ||
| 166 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 167 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 168 | workspaceRoot, | ||
| 169 | homeConfigPath, | ||
| 170 | homeStateDirectory, | ||
| 171 | }); | ||
| 172 | const scopeId = deriveLocalInstallation(context).scopeId; | ||
| 173 | const path = localExecutorStatePath(context); | ||
| 174 | yield* fs.makeDirectory(dirname(path), { recursive: true }); | ||
| 175 | yield* fs.writeFileString( | ||
| 176 | path, | ||
| 177 | `${JSON.stringify({ | ||
| 178 | version: 1, | ||
| 179 | secretStores: [], | ||
| 180 | secretMaterials: [ | ||
| 181 | { | ||
| 182 | id: "sec_local", | ||
| 183 | name: "Startup Secret", | ||
| 184 | purpose: "auth_material", | ||
| 185 | providerId: "local", | ||
| 186 | handle: "local:start", | ||
| 187 | value: "startup-token", | ||
| 188 | createdAt: 1, | ||
| 189 | updatedAt: 1, | ||
| 190 | }, | ||
| 191 | ], | ||
| 192 | executions: [], | ||
| 193 | executionInteractions: [], | ||
| 194 | executionSteps: [], | ||
| 195 | }, null, 2)}\n`, | ||
| 196 | ); | ||
| 197 | |||
| 198 | const beforeStartup = yield* Effect.flip(loadLocalExecutorStateSnapshot(context)); | ||
| 199 | expect(beforeStartup.message).toContain("Expected 2, actual 1"); | ||
| 200 | |||
| 201 | yield* createLocalExecutorRepositoriesEffect({ | ||
| 202 | workspaceRoot, | ||
| 203 | homeConfigPath, | ||
| 204 | homeStateDirectory, | ||
| 205 | }); | ||
| 206 | |||
| 207 | const loaded = yield* loadLocalExecutorStateSnapshot(context); | ||
| 208 | expect(yield* fs.exists(`${path}.legacy-backup`)).toBe(true); | ||
| 209 | expect(loaded.secretStores).toEqual([ | ||
| 210 | { | ||
| 211 | id: "sts_builtin_local", | ||
| 212 | scopeId, | ||
| 213 | name: "Local Store", | ||
| 214 | kind: "local", | ||
| 215 | status: "connected", | ||
| 216 | enabled: true, | ||
| 217 | createdAt: 1, | ||
| 218 | updatedAt: 1, | ||
| 219 | }, | ||
| 220 | ]); | ||
| 221 | expect(loaded.secretMaterialStoredData).toEqual([ | ||
| 222 | { | ||
| 223 | secretId: "sec_local", | ||
| 224 | data: { | ||
| 225 | value: "startup-token", | ||
| 226 | }, | ||
| 227 | }, | ||
| 228 | ]); | ||
| 229 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 230 | ); | ||
| 231 | }); | ||
| @@ -0,0 +1,32 @@ | |||
| 1 | import { FileSystem } from "@effect/platform"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | |||
| 4 | import type { ResolvedLocalWorkspaceContext } from "../config"; | ||
| 5 | import type { LocalFileSystemError } from "../errors"; | ||
| 6 | import { migrateLegacyLocalExecutorState } from "./legacy-local-executor-state"; | ||
| 7 | |||
| 8 | type StartupExecutorStateMigration = ( | ||
| 9 | context: ResolvedLocalWorkspaceContext, | ||
| 10 | ) => Effect.Effect<ReadonlyArray<string>, LocalFileSystemError, FileSystem.FileSystem>; | ||
| 11 | |||
| 12 | // Keep legacy executor-state upgrades isolated from the live store path. | ||
| 13 | const startupExecutorStateMigrations: readonly StartupExecutorStateMigration[] = [ | ||
| 14 | migrateLegacyLocalExecutorState, | ||
| 15 | ]; | ||
| 16 | |||
| 17 | export const runStartupExecutorStateMigrations = ( | ||
| 18 | context: ResolvedLocalWorkspaceContext, | ||
| 19 | ): Effect.Effect<ReadonlyArray<string>, LocalFileSystemError, FileSystem.FileSystem> => | ||
| 20 | Effect.gen(function* () { | ||
| 21 | const migratedPaths = new Set<string>(); | ||
| 22 | |||
| 23 | for (const migrate of startupExecutorStateMigrations) { | ||
| 24 | for (const path of yield* migrate(context)) { | ||
| 25 | migratedPaths.add(path); | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | return [...migratedPaths]; | ||
| 30 | }); | ||
| 31 | |||
| 32 | export { migrateLegacyLocalExecutorState } from "./legacy-local-executor-state"; | ||
| @@ -0,0 +1,356 @@ | |||
| 1 | import { FileSystem } from "@effect/platform"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | |||
| 4 | import type { ResolvedLocalWorkspaceContext } from "../config"; | ||
| 5 | import { | ||
| 6 | decodeLocalExecutorStateSnapshot, | ||
| 7 | encodeLocalExecutorStateSnapshot, | ||
| 8 | localExecutorStatePath, | ||
| 9 | LOCAL_EXECUTOR_STATE_VERSION, | ||
| 10 | type LocalExecutorStateSnapshot, | ||
| 11 | } from "../executor-state-codec"; | ||
| 12 | import { | ||
| 13 | LocalFileSystemError, | ||
| 14 | unknownLocalErrorDetails, | ||
| 15 | } from "../errors"; | ||
| 16 | import { deriveLocalInstallation } from "../installation"; | ||
| 17 | |||
| 18 | const LEGACY_EXECUTOR_STATE_BACKUP_SUFFIX = ".legacy-backup"; | ||
| 19 | const LEGACY_LOCAL_SECRET_STORE_ID = "sts_builtin_local"; | ||
| 20 | const LEGACY_KEYCHAIN_SECRET_STORE_ID = "sts_builtin_keychain"; | ||
| 21 | |||
| 22 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => | ||
| 23 | new LocalFileSystemError({ | ||
| 24 | message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`, | ||
| 25 | action, | ||
| 26 | path, | ||
| 27 | details: unknownLocalErrorDetails(cause), | ||
| 28 | }); | ||
| 29 | |||
| 30 | const asRecord = (value: unknown): Record<string, unknown> | null => | ||
| 31 | value !== null && typeof value === "object" && !Array.isArray(value) | ||
| 32 | ? value as Record<string, unknown> | ||
| 33 | : null; | ||
| 34 | |||
| 35 | const asString = (value: unknown): string | null => | ||
| 36 | typeof value === "string" && value.length > 0 ? value : null; | ||
| 37 | |||
| 38 | const asNullableString = (value: unknown): string | null => | ||
| 39 | value === null | ||
| 40 | ? null | ||
| 41 | : typeof value === "string" | ||
| 42 | ? value | ||
| 43 | : null; | ||
| 44 | |||
| 45 | const asBoolean = (value: unknown): boolean | null => | ||
| 46 | typeof value === "boolean" ? value : null; | ||
| 47 | |||
| 48 | const asFiniteNumber = (value: unknown): number | null => | ||
| 49 | typeof value === "number" && Number.isFinite(value) ? value : null; | ||
| 50 | |||
| 51 | const legacySecretStoreIdForProviderId = (providerId: string): string => | ||
| 52 | providerId === "local" | ||
| 53 | ? LEGACY_LOCAL_SECRET_STORE_ID | ||
| 54 | : providerId === "keychain" | ||
| 55 | ? LEGACY_KEYCHAIN_SECRET_STORE_ID | ||
| 56 | : `sts_legacy_${providerId}`; | ||
| 57 | |||
| 58 | const legacySecretStoreNameForProviderId = (providerId: string): string => { | ||
| 59 | switch (providerId) { | ||
| 60 | case "local": | ||
| 61 | return "Local Store"; | ||
| 62 | case "keychain": | ||
| 63 | return process.platform === "darwin" | ||
| 64 | ? "macOS Keychain" | ||
| 65 | : process.platform === "win32" | ||
| 66 | ? "Windows Credential Manager" | ||
| 67 | : "Desktop Keyring"; | ||
| 68 | default: | ||
| 69 | return `Legacy ${providerId}`; | ||
| 70 | } | ||
| 71 | }; | ||
| 72 | |||
| 73 | const migrateLegacyExecutorStateValue = ( | ||
| 74 | value: unknown, | ||
| 75 | ): { | ||
| 76 | value: unknown; | ||
| 77 | migrated: boolean; | ||
| 78 | } => { | ||
| 79 | if (Array.isArray(value)) { | ||
| 80 | let migrated = false; | ||
| 81 | const next = value.map((item) => { | ||
| 82 | const result = migrateLegacyExecutorStateValue(item); | ||
| 83 | migrated ||= result.migrated; | ||
| 84 | return result.value; | ||
| 85 | }); | ||
| 86 | return { value: next, migrated }; | ||
| 87 | } | ||
| 88 | |||
| 89 | const record = asRecord(value); | ||
| 90 | if (record === null) { | ||
| 91 | return { value, migrated: false }; | ||
| 92 | } | ||
| 93 | |||
| 94 | let migrated = false; | ||
| 95 | const next: Record<string, unknown> = {}; | ||
| 96 | |||
| 97 | for (const [key, entry] of Object.entries(record)) { | ||
| 98 | let nextKey = key; | ||
| 99 | if (key === "workspaceId") { | ||
| 100 | nextKey = "scopeId"; | ||
| 101 | migrated = true; | ||
| 102 | } else if (key === "actorAccountId") { | ||
| 103 | nextKey = "actorScopeId"; | ||
| 104 | migrated = true; | ||
| 105 | } else if (key === "createdByAccountId") { | ||
| 106 | nextKey = "createdByScopeId"; | ||
| 107 | migrated = true; | ||
| 108 | } else if (key === "workspaceOauthClients") { | ||
| 109 | nextKey = "scopeOauthClients"; | ||
| 110 | migrated = true; | ||
| 111 | } | ||
| 112 | |||
| 113 | const migratedEntry = migrateLegacyExecutorStateValue(entry); | ||
| 114 | migrated ||= migratedEntry.migrated; | ||
| 115 | next[nextKey] = migratedEntry.value; | ||
| 116 | } | ||
| 117 | |||
| 118 | return { value: next, migrated }; | ||
| 119 | }; | ||
| 120 | |||
| 121 | const normalizeLegacySecretStore = (input: { | ||
| 122 | scopeId: string; | ||
| 123 | record: Record<string, unknown>; | ||
| 124 | }) => { | ||
| 125 | const id = asString(input.record.id); | ||
| 126 | const kind = asString(input.record.kind); | ||
| 127 | if (!id || !kind) { | ||
| 128 | return null; | ||
| 129 | } | ||
| 130 | |||
| 131 | const createdAt = asFiniteNumber(input.record.createdAt) ?? 0; | ||
| 132 | const updatedAt = asFiniteNumber(input.record.updatedAt) ?? createdAt; | ||
| 133 | |||
| 134 | return { | ||
| 135 | id, | ||
| 136 | scopeId: asString(input.record.scopeId) ?? input.scopeId, | ||
| 137 | name: asString(input.record.name) ?? legacySecretStoreNameForProviderId(kind), | ||
| 138 | kind, | ||
| 139 | status: | ||
| 140 | input.record.status === "connected" || input.record.status === "error" | ||
| 141 | ? input.record.status | ||
| 142 | : kind === "local" || kind === "keychain" | ||
| 143 | ? "connected" | ||
| 144 | : "error", | ||
| 145 | enabled: asBoolean(input.record.enabled) ?? true, | ||
| 146 | createdAt, | ||
| 147 | updatedAt, | ||
| 148 | }; | ||
| 149 | }; | ||
| 150 | |||
| 151 | const legacySecretStoredData = (input: { | ||
| 152 | material: Record<string, unknown>; | ||
| 153 | storeKind: string | null; | ||
| 154 | }) => { | ||
| 155 | const handle = asString(input.material.handle); | ||
| 156 | const value = asNullableString(input.material.value); | ||
| 157 | |||
| 158 | switch (input.storeKind) { | ||
| 159 | case "local": | ||
| 160 | return value !== null ? { value } : null; | ||
| 161 | case "keychain": | ||
| 162 | return handle ? { account: handle } : null; | ||
| 163 | default: | ||
| 164 | return handle !== null || value !== null | ||
| 165 | ? { | ||
| 166 | ...(handle !== null ? { handle } : {}), | ||
| 167 | ...(value !== null ? { value } : {}), | ||
| 168 | } | ||
| 169 | : null; | ||
| 170 | } | ||
| 171 | }; | ||
| 172 | |||
| 173 | const canDecodeCurrentExecutorStateSnapshot = (value: unknown): boolean => { | ||
| 174 | try { | ||
| 175 | decodeLocalExecutorStateSnapshot(value); | ||
| 176 | return true; | ||
| 177 | } catch { | ||
| 178 | return false; | ||
| 179 | } | ||
| 180 | }; | ||
| 181 | |||
| 182 | const decodeLegacyExecutorStateSnapshot = (input: { | ||
| 183 | scopeId: string; | ||
| 184 | value: unknown; | ||
| 185 | }): LocalExecutorStateSnapshot => { | ||
| 186 | const root = asRecord(input.value); | ||
| 187 | if (root === null) { | ||
| 188 | return decodeLocalExecutorStateSnapshot(input.value); | ||
| 189 | } | ||
| 190 | |||
| 191 | const secretStores = new Map<string, Record<string, unknown>>(); | ||
| 192 | for (const entry of Array.isArray(root.secretStores) ? root.secretStores : []) { | ||
| 193 | const record = asRecord(entry); | ||
| 194 | const normalized = record | ||
| 195 | ? normalizeLegacySecretStore({ | ||
| 196 | scopeId: input.scopeId, | ||
| 197 | record, | ||
| 198 | }) | ||
| 199 | : null; | ||
| 200 | if (normalized) { | ||
| 201 | secretStores.set(normalized.id, normalized); | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | const secretMaterialStoredData = new Map<string, { secretId: string; data: unknown }>(); | ||
| 206 | for (const entry of Array.isArray(root.secretMaterialStoredData) | ||
| 207 | ? root.secretMaterialStoredData | ||
| 208 | : []) { | ||
| 209 | const record = asRecord(entry); | ||
| 210 | const secretId = asString(record?.secretId); | ||
| 211 | if (!secretId) { | ||
| 212 | continue; | ||
| 213 | } | ||
| 214 | secretMaterialStoredData.set(secretId, { | ||
| 215 | secretId, | ||
| 216 | data: record?.data, | ||
| 217 | }); | ||
| 218 | } | ||
| 219 | |||
| 220 | const secretMaterials = (Array.isArray(root.secretMaterials) | ||
| 221 | ? root.secretMaterials | ||
| 222 | : [] | ||
| 223 | ).flatMap((entry) => { | ||
| 224 | const record = asRecord(entry); | ||
| 225 | const id = asString(record?.id); | ||
| 226 | const purpose = asString(record?.purpose); | ||
| 227 | if (!record || !id || !purpose) { | ||
| 228 | return []; | ||
| 229 | } | ||
| 230 | |||
| 231 | const providerId = asString(record.providerId); | ||
| 232 | const storeId = asString(record.storeId) | ||
| 233 | ?? (providerId ? legacySecretStoreIdForProviderId(providerId) : null); | ||
| 234 | if (!storeId) { | ||
| 235 | return []; | ||
| 236 | } | ||
| 237 | |||
| 238 | const createdAt = asFiniteNumber(record.createdAt) ?? 0; | ||
| 239 | const updatedAt = asFiniteNumber(record.updatedAt) ?? createdAt; | ||
| 240 | |||
| 241 | if (providerId && !secretStores.has(storeId)) { | ||
| 242 | secretStores.set(storeId, { | ||
| 243 | id: storeId, | ||
| 244 | scopeId: input.scopeId, | ||
| 245 | name: legacySecretStoreNameForProviderId(providerId), | ||
| 246 | kind: providerId, | ||
| 247 | status: | ||
| 248 | providerId === "local" || providerId === "keychain" | ||
| 249 | ? "connected" | ||
| 250 | : "error", | ||
| 251 | enabled: true, | ||
| 252 | createdAt, | ||
| 253 | updatedAt, | ||
| 254 | }); | ||
| 255 | } | ||
| 256 | |||
| 257 | const storeKind = asString(secretStores.get(storeId)?.kind) ?? providerId; | ||
| 258 | if (!secretMaterialStoredData.has(id)) { | ||
| 259 | const data = legacySecretStoredData({ | ||
| 260 | material: record, | ||
| 261 | storeKind, | ||
| 262 | }); | ||
| 263 | if (data !== null) { | ||
| 264 | secretMaterialStoredData.set(id, { | ||
| 265 | secretId: id, | ||
| 266 | data, | ||
| 267 | }); | ||
| 268 | } | ||
| 269 | } | ||
| 270 | |||
| 271 | return [{ | ||
| 272 | id, | ||
| 273 | name: asNullableString(record.name), | ||
| 274 | purpose, | ||
| 275 | storeId, | ||
| 276 | createdAt, | ||
| 277 | updatedAt, | ||
| 278 | }]; | ||
| 279 | }); | ||
| 280 | |||
| 281 | return decodeLocalExecutorStateSnapshot({ | ||
| 282 | version: LOCAL_EXECUTOR_STATE_VERSION, | ||
| 283 | secretStores: [...secretStores.values()], | ||
| 284 | secretMaterials, | ||
| 285 | secretMaterialStoredData: [...secretMaterialStoredData.values()], | ||
| 286 | executions: Array.isArray(root.executions) ? root.executions : [], | ||
| 287 | executionInteractions: Array.isArray(root.executionInteractions) | ||
| 288 | ? root.executionInteractions | ||
| 289 | : [], | ||
| 290 | executionSteps: Array.isArray(root.executionSteps) ? root.executionSteps : [], | ||
| 291 | }); | ||
| 292 | }; | ||
| 293 | |||
| 294 | const migrateLegacyExecutorStateFile = ( | ||
| 295 | context: ResolvedLocalWorkspaceContext, | ||
| 296 | ): Effect.Effect<boolean, LocalFileSystemError, FileSystem.FileSystem> => | ||
| 297 | Effect.gen(function* () { | ||
| 298 | const fs = yield* FileSystem.FileSystem; | ||
| 299 | const path = localExecutorStatePath(context); | ||
| 300 | const exists = yield* fs.exists(path).pipe( | ||
| 301 | Effect.mapError(mapFileSystemError(path, "check executor state path")), | ||
| 302 | ); | ||
| 303 | if (!exists) { | ||
| 304 | return false; | ||
| 305 | } | ||
| 306 | |||
| 307 | const content = yield* fs.readFileString(path, "utf8").pipe( | ||
| 308 | Effect.mapError(mapFileSystemError(path, "read executor state")), | ||
| 309 | ); | ||
| 310 | const parsed = yield* Effect.try({ | ||
| 311 | try: () => JSON.parse(content) as unknown, | ||
| 312 | catch: mapFileSystemError(path, "parse executor state"), | ||
| 313 | }); | ||
| 314 | |||
| 315 | if (canDecodeCurrentExecutorStateSnapshot(parsed)) { | ||
| 316 | return false; | ||
| 317 | } | ||
| 318 | |||
| 319 | const migrated = migrateLegacyExecutorStateValue(parsed); | ||
| 320 | const migratedState = yield* Effect.try({ | ||
| 321 | try: () => | ||
| 322 | decodeLegacyExecutorStateSnapshot({ | ||
| 323 | scopeId: deriveLocalInstallation(context).scopeId, | ||
| 324 | value: migrated.value, | ||
| 325 | }), | ||
| 326 | catch: mapFileSystemError(path, "decode executor state"), | ||
| 327 | }); | ||
| 328 | |||
| 329 | const backupPath = `${path}${LEGACY_EXECUTOR_STATE_BACKUP_SUFFIX}`; | ||
| 330 | const backupExists = yield* fs.exists(backupPath).pipe( | ||
| 331 | Effect.mapError(mapFileSystemError(backupPath, "check executor state backup path")), | ||
| 332 | ); | ||
| 333 | if (!backupExists) { | ||
| 334 | yield* fs.writeFileString(backupPath, content, { mode: 0o600 }).pipe( | ||
| 335 | Effect.mapError(mapFileSystemError(backupPath, "write executor state backup")), | ||
| 336 | ); | ||
| 337 | } | ||
| 338 | |||
| 339 | yield* fs.writeFileString( | ||
| 340 | path, | ||
| 341 | encodeLocalExecutorStateSnapshot(migratedState), | ||
| 342 | { mode: 0o600 }, | ||
| 343 | ).pipe( | ||
| 344 | Effect.mapError(mapFileSystemError(path, "write migrated executor state")), | ||
| 345 | ); | ||
| 346 | |||
| 347 | return true; | ||
| 348 | }); | ||
| 349 | |||
| 350 | export const migrateLegacyLocalExecutorState = ( | ||
| 351 | context: ResolvedLocalWorkspaceContext, | ||
| 352 | ): Effect.Effect<ReadonlyArray<string>, LocalFileSystemError, FileSystem.FileSystem> => | ||
| 353 | Effect.gen(function* () { | ||
| 354 | const migrated = yield* migrateLegacyExecutorStateFile(context); | ||
| 355 | return migrated ? [localExecutorStatePath(context)] : []; | ||
| 356 | }); | ||
| @@ -1,5 +1,5 @@ | |||
| 1 | import { randomUUID } from "node:crypto"; | 1 | import { randomUUID } from "node:crypto"; |
| 2 | import { dirname, join } from "node:path"; | 2 | import { dirname } from "node:path"; |
| 3 | import { FileSystem } from "@effect/platform"; | 3 | import { FileSystem } from "@effect/platform"; |
| 4 | import { NodeFileSystem } from "@effect/platform-node"; | 4 | import { NodeFileSystem } from "@effect/platform-node"; |
| 5 | 5 | ||
| @@ -7,66 +7,32 @@ import { · 1 unmodified line | |||
| 7 | type Execution, | 7 | type Execution, |
| 8 | type ExecutionInteraction, | 8 | type ExecutionInteraction, |
| 9 | type ExecutionStep, | 9 | type ExecutionStep, |
| 10 | ExecutionInteractionSchema, | ||
| 11 | ExecutionSchema, | ||
| 12 | ExecutionStepSchema, | ||
| 13 | SecretMaterialSchema, | ||
| 14 | type SecretMaterial, | 10 | type SecretMaterial, |
| 15 | SecretStoreSchema, | ||
| 16 | type SecretStore, | 11 | type SecretStore, |
| 17 | } from "@executor/platform-sdk/schema"; | 12 | } from "@executor/platform-sdk/schema"; |
| 18 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 19 | import * as Option from "effect/Option"; | 14 | import * as Option from "effect/Option"; |
| 20 | import * as Schema from "effect/Schema"; | ||
| 21 | 15 | ||
| 22 | import type { ResolvedLocalWorkspaceContext } from "./config"; | 16 | import type { ResolvedLocalWorkspaceContext } from "./config"; |
| 23 | import { deriveLocalInstallation } from "./installation"; | 17 | import { |
| 18 | decodeLocalExecutorStateSnapshot, | ||
| 19 | defaultLocalExecutorStateSnapshot, | ||
| 20 | encodeLocalExecutorStateSnapshot, | ||
| 21 | localExecutorStatePath, | ||
| 22 | LOCAL_EXECUTOR_STATE_VERSION, | ||
| 23 | type LocalExecutorStateSnapshot, | ||
| 24 | type SecretMaterialStoredDataRecord, | ||
| 25 | } from "./executor-state-codec"; | ||
| 24 | import { | 26 | import { |
| 25 | LocalFileSystemError, | 27 | LocalFileSystemError, |
| 26 | unknownLocalErrorDetails, | 28 | unknownLocalErrorDetails, |
| 27 | } from "./errors"; | 29 | } from "./errors"; |
| 28 | 30 | ||
| 29 | const LOCAL_EXECUTOR_STATE_VERSION = 2 as const; | ||
| 30 | const LOCAL_EXECUTOR_STATE_BASENAME = "executor-state.json"; | ||
| 31 | |||
| 32 | const SecretMaterialStoredDataRecordSchema = Schema.Struct({ | ||
| 33 | secretId: Schema.String, | ||
| 34 | data: Schema.Unknown, | ||
| 35 | }); | ||
| 36 | |||
| 37 | type SecretMaterialStoredDataRecord = typeof SecretMaterialStoredDataRecordSchema.Type; | ||
| 38 | |||
| 39 | const LocalExecutorStateSnapshotSchema = Schema.Struct({ | ||
| 40 | version: Schema.Literal(LOCAL_EXECUTOR_STATE_VERSION), | ||
| 41 | secretStores: Schema.Array(SecretStoreSchema), | ||
| 42 | secretMaterials: Schema.Array(SecretMaterialSchema), | ||
| 43 | secretMaterialStoredData: Schema.Array(SecretMaterialStoredDataRecordSchema), | ||
| 44 | executions: Schema.Array(ExecutionSchema), | ||
| 45 | executionInteractions: Schema.Array(ExecutionInteractionSchema), | ||
| 46 | executionSteps: Schema.Array(ExecutionStepSchema), | ||
| 47 | }); | ||
| 48 | |||
| 49 | export type LocalExecutorStateSnapshot = typeof LocalExecutorStateSnapshotSchema.Type; | ||
| 50 | |||
| 51 | export type LocalExecutorStatePersistence = { | 31 | export type LocalExecutorStatePersistence = { |
| 52 | executorState: LocalExecutorStateStore; | 32 | executorState: LocalExecutorStateStore; |
| 53 | close: () => Promise<void>; | 33 | close: () => Promise<void>; |
| 54 | }; | 34 | }; |
| 55 | 35 | ||
| 56 | const decodeLocalExecutorStateSnapshot = Schema.decodeUnknown( | ||
| 57 | LocalExecutorStateSnapshotSchema, | ||
| 58 | ); | ||
| 59 | |||
| 60 | const defaultLocalExecutorStateSnapshot = (): LocalExecutorStateSnapshot => ({ | ||
| 61 | version: LOCAL_EXECUTOR_STATE_VERSION, | ||
| 62 | secretStores: [], | ||
| 63 | secretMaterials: [], | ||
| 64 | secretMaterialStoredData: [], | ||
| 65 | executions: [], | ||
| 66 | executionInteractions: [], | ||
| 67 | executionSteps: [], | ||
| 68 | }); | ||
| 69 | |||
| 70 | const cloneValue = <T>(value: T): T => | 36 | const cloneValue = <T>(value: T): T => |
| 71 | JSON.parse(JSON.stringify(value)) as T; | 37 | JSON.parse(JSON.stringify(value)) as T; |
| 72 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => | 38 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => |
| @@ -84,16 +50,6 @@ const sortByUpdatedAtAndIdDesc = <T extends { updatedAt: number; id: string }>( · 11 unmodified lines | |||
| 84 | right.updatedAt - left.updatedAt || right.id.localeCompare(left.id), | 50 | right.updatedAt - left.updatedAt || right.id.localeCompare(left.id), |
| 85 | ); | 51 | ); |
| 86 | 52 | ||
| 87 | const localExecutorStatePath = ( | ||
| 88 | context: ResolvedLocalWorkspaceContext, | ||
| 89 | ): string => | ||
| 90 | join( | ||
| 91 | context.homeStateDirectory, | ||
| 92 | "workspaces", | ||
| 93 | deriveLocalInstallation(context).scopeId, | ||
| 94 | LOCAL_EXECUTOR_STATE_BASENAME, | ||
| 95 | ); | ||
| 96 | |||
| 97 | const bindFileSystem = <A, E>( | 53 | const bindFileSystem = <A, E>( |
| 98 | fileSystem: FileSystem.FileSystem, | 54 | fileSystem: FileSystem.FileSystem, |
| 99 | effect: Effect.Effect<A, E, FileSystem.FileSystem>, | 55 | effect: Effect.Effect<A, E, FileSystem.FileSystem>, |
| @@ -125,10 +81,10 @@ const readStateFromDisk = ( · 25 unmodified lines | |||
| 125 | try: () => JSON.parse(content) as unknown, | 81 | try: () => JSON.parse(content) as unknown, |
| 126 | catch: mapFileSystemError(path, "parse executor state"), | 82 | catch: mapFileSystemError(path, "parse executor state"), |
| 127 | }); | 83 | }); |
| 128 | const decoded = yield* decodeLocalExecutorStateSnapshot(parsed).pipe( | 84 | return yield* Effect.try({ |
| 129 | Effect.mapError(mapFileSystemError(path, "decode executor state")), | 85 | try: () => decodeLocalExecutorStateSnapshot(parsed), |
| 130 | ); | 86 | catch: mapFileSystemError(path, "decode executor state"), |
| 131 | return decoded; | 87 | }); |
| 132 | }); | 88 | }); |
| 133 | 89 | ||
| 134 | const writeStateToDisk = ( | 90 | const writeStateToDisk = ( |
| @@ -143,7 +99,7 @@ const writeStateToDisk = ( · 8 unmodified lines | |||
| 143 | yield* fs.makeDirectory(dirname(path), { recursive: true }).pipe( | 99 | yield* fs.makeDirectory(dirname(path), { recursive: true }).pipe( |
| 144 | Effect.mapError(mapFileSystemError(dirname(path), "create executor state directory")), | 100 | Effect.mapError(mapFileSystemError(dirname(path), "create executor state directory")), |
| 145 | ); | 101 | ); |
| 146 | yield* fs.writeFileString(tempPath, `${JSON.stringify(state, null, 2)}\n`, { | 102 | yield* fs.writeFileString(tempPath, encodeLocalExecutorStateSnapshot(state), { |
| 147 | mode: 0o600, | 103 | mode: 0o600, |
| 148 | }).pipe( | 104 | }).pipe( |
| 149 | Effect.mapError(mapFileSystemError(tempPath, "write executor state")), | 105 | Effect.mapError(mapFileSystemError(tempPath, "write executor state")), |
| @@ -19,7 +19,7 @@ import type { · 18 unmodified lines | |||
| 19 | ExecutorSdkPluginExtensions, | 19 | ExecutorSdkPluginExtensions, |
| 20 | } from "@executor/platform-sdk/plugins"; | 20 | } from "@executor/platform-sdk/plugins"; |
| 21 | import type { | 21 | import type { |
| 22 | LocalExecutorConfig, | 22 | ExecutorScopeConfig, |
| 23 | LocalInstallation, | 23 | LocalInstallation, |
| 24 | } from "@executor/platform-sdk/schema"; | 24 | } from "@executor/platform-sdk/schema"; |
| 25 | import { | 25 | import { |
| @@ -30,14 +30,18 @@ import { · 4 unmodified lines | |||
| 30 | } from "@executor/platform-sdk/runtime"; | 30 | } from "@executor/platform-sdk/runtime"; |
| 31 | import * as Effect from "effect/Effect"; | 31 | import * as Effect from "effect/Effect"; |
| 32 | import { | 32 | import { |
| 33 | type LoadedLocalExecutorConfig, | 33 | type LoadedExecutorScopeConfig, |
| 34 | type ResolvedLocalWorkspaceContext, | 34 | type ResolvedLocalWorkspaceContext, |
| 35 | loadLocalExecutorConfig, | 35 | loadExecutorScopeConfig, |
| 36 | resolveConfigRelativePath, | 36 | resolveConfigRelativePath, |
| 37 | resolveLocalWorkspaceContext, | 37 | resolveLocalWorkspaceContext, |
| 38 | writeProjectLocalExecutorConfig, | 38 | writeProjectExecutorScopeConfig, |
| 39 | } from "./config"; | 39 | } from "./config"; |
| 40 | import { | ||
| 41 | runStartupConfigMigrations, | ||
| 42 | } from "./config-migrations"; | ||
| 40 | import { createLocalExecutorStatePersistence } from "./executor-state-store"; | 43 | import { createLocalExecutorStatePersistence } from "./executor-state-store"; |
| 44 | import { runStartupExecutorStateMigrations } from "./executor-state-migrations"; | ||
| 41 | import { | 45 | import { |
| 42 | createDefaultSecretMaterialDeleter, | 46 | createDefaultSecretMaterialDeleter, |
| 43 | createLocalInstanceConfigResolver, | 47 | createLocalInstanceConfigResolver, |
| @@ -114,18 +118,18 @@ const createBoundWorkspaceConfigStore = ( · 70 unmodified lines | |||
| 114 | context: ResolvedLocalWorkspaceContext, | 118 | context: ResolvedLocalWorkspaceContext, |
| 115 | fileSystem: FileSystem.FileSystem, | 119 | fileSystem: FileSystem.FileSystem, |
| 116 | ): { | 120 | ): { |
| 117 | load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>; | 121 | load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>; |
| 118 | writeProject: (config: LocalExecutorConfig) => Effect.Effect<void, Error, never>; | 122 | writeProject: (config: ExecutorScopeConfig) => Effect.Effect<void, Error, never>; |
| 119 | resolveRelativePath: typeof resolveConfigRelativePath; | 123 | resolveRelativePath: typeof resolveConfigRelativePath; |
| 120 | } => ({ | 124 | } => ({ |
| 121 | load: () => | 125 | load: () => |
| 122 | bindFileSystem(fileSystem, loadLocalExecutorConfig(context)).pipe( | 126 | bindFileSystem(fileSystem, loadExecutorScopeConfig(context)).pipe( |
| 123 | Effect.mapError(toError), | 127 | Effect.mapError(toError), |
| 124 | ), | 128 | ), |
| 125 | writeProject: (config) => | 129 | writeProject: (config) => |
| 126 | bindFileSystem( | 130 | bindFileSystem( |
| 127 | fileSystem, | 131 | fileSystem, |
| 128 | writeProjectLocalExecutorConfig({ context, config }), | 132 | writeProjectExecutorScopeConfig({ context, config }), |
| 129 | ).pipe(Effect.mapError(toError)), | 133 | ).pipe(Effect.mapError(toError)), |
| 130 | resolveRelativePath: resolveConfigRelativePath, | 134 | resolveRelativePath: resolveConfigRelativePath, |
| 131 | }); | 135 | }); |
| @@ -237,6 +241,14 @@ export const createLocalExecutorRepositoriesEffect = ( · 105 unmodified lines | |||
| 237 | homeStateDirectory: options.homeStateDirectory, | 241 | homeStateDirectory: options.homeStateDirectory, |
| 238 | }), | 242 | }), |
| 239 | ).pipe(Effect.mapError(toError)); | 243 | ).pipe(Effect.mapError(toError)); |
| 244 | yield* bindFileSystem( | ||
| 245 | fileSystem, | ||
| 246 | runStartupConfigMigrations(workspaceContext), | ||
| 247 | ).pipe(Effect.mapError(toError)); | ||
| 248 | yield* bindFileSystem( | ||
| 249 | fileSystem, | ||
| 250 | runStartupExecutorStateMigrations(workspaceContext), | ||
| 251 | ).pipe(Effect.mapError(toError)); | ||
| 240 | 252 | ||
| 241 | const executorStateStorage = createLocalExecutorStatePersistence( | 253 | const executorStateStorage = createLocalExecutorStatePersistence( |
| 242 | workspaceContext, | 254 | workspaceContext, |
| @@ -384,11 +396,19 @@ export { · 141 unmodified lines | |||
| 384 | writeLocalExecutorStateSnapshot, | 396 | writeLocalExecutorStateSnapshot, |
| 385 | } from "./executor-state-store"; | 397 | } from "./executor-state-store"; |
| 386 | export { | 398 | export { |
| 387 | loadLocalExecutorConfig, | 399 | migrateLegacyLocalExecutorState, |
| 400 | runStartupExecutorStateMigrations, | ||
| 401 | } from "./executor-state-migrations"; | ||
| 402 | export { | ||
| 403 | loadExecutorScopeConfig, | ||
| 388 | resolveConfigRelativePath, | 404 | resolveConfigRelativePath, |
| 389 | resolveLocalWorkspaceContext, | 405 | resolveLocalWorkspaceContext, |
| 390 | writeProjectLocalExecutorConfig, | 406 | writeProjectExecutorScopeConfig, |
| 391 | } from "./config"; | 407 | } from "./config"; |
| 408 | export { | ||
| 409 | migrateLegacyExecutorScopeConfigs, | ||
| 410 | runStartupConfigMigrations, | ||
| 411 | } from "./config-migrations"; | ||
| 392 | export { | 412 | export { |
| 393 | loadLocalWorkspaceState, | 413 | loadLocalWorkspaceState, |
| 394 | writeLocalWorkspaceState, | 414 | writeLocalWorkspaceState, |
| @@ -406,6 +426,6 @@ export { · 11 unmodified lines | |||
| 406 | syncWorkspaceSourceTypeDeclarationsNode, | 426 | syncWorkspaceSourceTypeDeclarationsNode, |
| 407 | } from "./source-type-declarations"; | 427 | } from "./source-type-declarations"; |
| 408 | export type { | 428 | export type { |
| 409 | LoadedLocalExecutorConfig, | 429 | LoadedExecutorScopeConfig, |
| 410 | ResolvedLocalWorkspaceContext, | 430 | ResolvedLocalWorkspaceContext, |
| 411 | } from "./config"; | 431 | } from "./config"; |
| @@ -8,8 +8,8 @@ import type { · 7 unmodified lines | |||
| 8 | import type { ExecutorEffect } from "./executor-effect"; | 8 | import type { ExecutorEffect } from "./executor-effect"; |
| 9 | import type { ExecutorScopeContext } from "./scope"; | 9 | import type { ExecutorScopeContext } from "./scope"; |
| 10 | import { | 10 | import { |
| 11 | type LocalConfigSource, | 11 | type ExecutorScopeConfigSource, |
| 12 | type LocalExecutorConfig, | 12 | type ExecutorScopeConfig, |
| 13 | type Source as ExecutorSource, | 13 | type Source as ExecutorSource, |
| 14 | type SecretMaterial, | 14 | type SecretMaterial, |
| 15 | type SecretMaterialPurpose, | 15 | type SecretMaterialPurpose, |
| @@ -300,15 +300,15 @@ export type ExecutorSourcePluginDefinition< · 284 unmodified lines | |||
| 300 | stored: TStored | null; | 300 | stored: TStored | null; |
| 301 | }) => Effect.Effect<void, Error, any>; | 301 | }) => Effect.Effect<void, Error, any>; |
| 302 | }; | 302 | }; |
| 303 | localConfig?: { | 303 | scopeConfig?: { |
| 304 | toConfigSource: (input: { | 304 | toConfigSource: (input: { |
| 305 | source: ExecutorSource; | 305 | source: ExecutorSource; |
| 306 | stored: TStored; | 306 | stored: TStored; |
| 307 | }) => LocalConfigSource; | 307 | }) => ExecutorScopeConfigSource; |
| 308 | recoverStored: (input: { | 308 | recoverStored: (input: { |
| 309 | source: ExecutorSource; | 309 | source: ExecutorSource; |
| 310 | config: LocalConfigSource; | 310 | config: ExecutorScopeConfigSource; |
| 311 | loadedConfig: LocalExecutorConfig | null; | 311 | loadedConfig: ExecutorScopeConfig | null; |
| 312 | }) => Effect.Effect<TStored, Error, any> | TStored; | 312 | }) => Effect.Effect<TStored, Error, any> | TStored; |
| 313 | }; | 313 | }; |
| 314 | catalog: { | 314 | catalog: { |
| @@ -588,7 +588,7 @@ const fromMaybeEffect = <A>( · 273 unmodified lines | |||
| 588 | ? value | 588 | ? value |
| 589 | : Effect.succeed(value); | 589 | : Effect.succeed(value); |
| 590 | 590 | ||
| 591 | const persistSourceLocalConfig = < | 591 | const persistSourceScopeConfig = < |
| 592 | TAddInput, | 592 | TAddInput, |
| 593 | TConnectInput, | 593 | TConnectInput, |
| 594 | TSourceConfig, | 594 | TSourceConfig, |
| @@ -610,15 +610,15 @@ const persistSourceLocalConfig = < · 15 unmodified lines | |||
| 610 | stored: TStored; | 610 | stored: TStored; |
| 611 | }, | 611 | }, |
| 612 | ): Effect.Effect<void, Error, ScopeConfigStore> => | 612 | ): Effect.Effect<void, Error, ScopeConfigStore> => |
| 613 | definition.localConfig | 613 | definition.scopeConfig |
| 614 | ? Effect.gen(function* () { | 614 | ? Effect.gen(function* () { |
| 615 | const localConfig = definition.localConfig!; | 615 | const scopeConfig = definition.scopeConfig!; |
| 616 | const scopeConfigStore = yield* ScopeConfigStore; | 616 | const scopeConfigStore = yield* ScopeConfigStore; |
| 617 | const loadedConfig = yield* scopeConfigStore.load(); | 617 | const loadedConfig = yield* scopeConfigStore.load(); |
| 618 | const projectConfig = cloneJson(loadedConfig.projectConfig ?? {}); | 618 | const projectConfig = cloneJson(loadedConfig.projectConfig ?? {}); |
| 619 | const sources = { | 619 | const sources = { |
| 620 | ...projectConfig.sources, | 620 | ...projectConfig.sources, |
| 621 | [input.source.id]: localConfig.toConfigSource({ | 621 | [input.source.id]: scopeConfig.toConfigSource({ |
| 622 | source: input.source, | 622 | source: input.source, |
| 623 | stored: input.stored, | 623 | stored: input.stored, |
| 624 | }), | 624 | }), |
| @@ -653,12 +653,12 @@ const loadStoredOrRecover = < · 28 unmodified lines | |||
| 653 | source: ExecutorSource, | 653 | source: ExecutorSource, |
| 654 | ): Effect.Effect<TStored | null, Error, ScopeConfigStore> => | 654 | ): Effect.Effect<TStored | null, Error, ScopeConfigStore> => |
| 655 | Effect.gen(function* () { | 655 | Effect.gen(function* () { |
| 656 | const localConfig = definition.localConfig; | 656 | const scopeConfig = definition.scopeConfig; |
| 657 | const stored = yield* definition.storage.get({ | 657 | const stored = yield* definition.storage.get({ |
| 658 | scopeId: source.scopeId, | 658 | scopeId: source.scopeId, |
| 659 | sourceId: source.id, | 659 | sourceId: source.id, |
| 660 | }); | 660 | }); |
| 661 | if (stored !== null || localConfig === undefined) { | 661 | if (stored !== null || scopeConfig === undefined) { |
| 662 | return stored; | 662 | return stored; |
| 663 | } | 663 | } |
| 664 | 664 | ||
| @@ -677,7 +677,7 @@ const loadStoredOrRecover = < · 12 unmodified lines | |||
| 677 | } | 677 | } |
| 678 | 678 | ||
| 679 | const recovered = yield* fromMaybeEffect( | 679 | const recovered = yield* fromMaybeEffect( |
| 680 | localConfig.recoverStored({ | 680 | scopeConfig.recoverStored({ |
| 681 | source, | 681 | source, |
| 682 | config, | 682 | config, |
| 683 | loadedConfig: loadedConfig.config, | 683 | loadedConfig: loadedConfig.config, |
| @@ -748,7 +748,7 @@ const createExecutorSourcePluginApi = < · 64 unmodified lines | |||
| 748 | sourceId: source.id, | 748 | sourceId: source.id, |
| 749 | value: created.stored, | 749 | value: created.stored, |
| 750 | }); | 750 | }); |
| 751 | yield* persistSourceLocalConfig(definition, { | 751 | yield* persistSourceScopeConfig(definition, { |
| 752 | source, | 752 | source, |
| 753 | stored: created.stored, | 753 | stored: created.stored, |
| 754 | }); | 754 | }); |
| @@ -772,7 +772,7 @@ const createExecutorSourcePluginApi = < · 17 unmodified lines | |||
| 772 | sourceId: saved.id, | 772 | sourceId: saved.id, |
| 773 | value: updated.stored, | 773 | value: updated.stored, |
| 774 | }); | 774 | }); |
| 775 | yield* persistSourceLocalConfig(definition, { | 775 | yield* persistSourceScopeConfig(definition, { |
| 776 | source: saved, | 776 | source: saved, |
| 777 | stored: updated.stored, | 777 | stored: updated.stored, |
| 778 | }); | 778 | }); |
| @@ -8,7 +8,7 @@ import type { · 7 unmodified lines | |||
| 8 | } from "./contracts"; | 8 | } from "./contracts"; |
| 9 | import { | 9 | import { |
| 10 | PolicyIdSchema, | 10 | PolicyIdSchema, |
| 11 | type LocalExecutorConfig, | 11 | type ExecutorScopeConfig, |
| 12 | type LocalScopePolicy, | 12 | type LocalScopePolicy, |
| 13 | type PolicyId, | 13 | type PolicyId, |
| 14 | type ScopeId, | 14 | type ScopeId, |
| @@ -64,7 +64,7 @@ const toLocalScopePolicy = (input: { · 49 unmodified lines | |||
| 64 | scopeId: ScopeId; | 64 | scopeId: ScopeId; |
| 65 | scopeStableKey: string; | 65 | scopeStableKey: string; |
| 66 | key: string; | 66 | key: string; |
| 67 | policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string]; | 67 | policyConfig: NonNullable<ExecutorScopeConfig["policies"]>[string]; |
| 68 | state: LocalScopeState["policies"][string] | undefined; | 68 | state: LocalScopeState["policies"][string] | undefined; |
| 69 | }): LocalScopePolicy => ({ | 69 | }): LocalScopePolicy => ({ |
| 70 | id: | 70 | id: |
| @@ -119,7 +119,7 @@ const writeLocalPolicyFiles = (input: { · 48 unmodified lines | |||
| 119 | operation: OperationErrors; | 119 | operation: OperationErrors; |
| 120 | scopeConfigStore: ScopeConfigStoreShape; | 120 | scopeConfigStore: ScopeConfigStoreShape; |
| 121 | scopeStateStore: ScopeStateStoreShape; | 121 | scopeStateStore: ScopeStateStoreShape; |
| 122 | projectConfig: LocalExecutorConfig; | 122 | projectConfig: ExecutorScopeConfig; |
| 123 | scopeState: LocalScopeState; | 123 | scopeState: LocalScopeState; |
| 124 | }) => | 124 | }) => |
| 125 | Effect.all( | 125 | Effect.all( |
| @@ -11,18 +11,18 @@ import { · 10 unmodified lines | |||
| 11 | makeSesExecutor, | 11 | makeSesExecutor, |
| 12 | } from "@executor/runtime-ses"; | 12 | } from "@executor/runtime-ses"; |
| 13 | import type { | 13 | import type { |
| 14 | LocalExecutorConfig, | 14 | ExecutorScopeConfig, |
| 15 | LocalExecutorRuntime, | 15 | ExecutorRuntimeConfig, |
| 16 | } from "#schema"; | 16 | } from "#schema"; |
| 17 | 17 | ||
| 18 | const DEFAULT_EXECUTION_RUNTIME: LocalExecutorRuntime = "quickjs"; | 18 | const DEFAULT_EXECUTION_RUNTIME: ExecutorRuntimeConfig = "quickjs"; |
| 19 | 19 | ||
| 20 | export const resolveConfiguredExecutionRuntime = ( | 20 | export const resolveConfiguredExecutionRuntime = ( |
| 21 | config: LocalExecutorConfig | null | undefined, | 21 | config: ExecutorScopeConfig | null | undefined, |
| 22 | ): LocalExecutorRuntime => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME; | 22 | ): ExecutorRuntimeConfig => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME; |
| 23 | 23 | ||
| 24 | export const createCodeExecutorForRuntime = ( | 24 | export const createCodeExecutorForRuntime = ( |
| 25 | runtime: LocalExecutorRuntime, | 25 | runtime: ExecutorRuntimeConfig, |
| 26 | ): CodeExecutor => { | 26 | ): CodeExecutor => { |
| 27 | switch (runtime) { | 27 | switch (runtime) { |
| 28 | case "deno": | 28 | case "deno": |
| @@ -32,10 +32,10 @@ import { · 31 unmodified lines | |||
| 32 | RuntimeExecutionResolverLive, | 32 | RuntimeExecutionResolverLive, |
| 33 | } from "./execution/scope/environment"; | 33 | } from "./execution/scope/environment"; |
| 34 | import type { | 34 | import type { |
| 35 | LoadedLocalExecutorConfig, | 35 | LoadedExecutorScopeConfig, |
| 36 | } from "./scope-config"; | 36 | } from "./scope-config"; |
| 37 | import type { | 37 | import type { |
| 38 | LocalExecutorConfig, | 38 | ExecutorScopeConfig, |
| 39 | } from "#schema"; | 39 | } from "#schema"; |
| 40 | import type { | 40 | import type { |
| 41 | InstanceConfig, | 41 | InstanceConfig, |
| @@ -113,7 +113,7 @@ export * from "./source-artifacts"; · 71 unmodified lines | |||
| 113 | export * from "./scope-config"; | 113 | export * from "./scope-config"; |
| 114 | export * from "./scope-state"; | 114 | export * from "./scope-state"; |
| 115 | export * from "./sources/source-store/config"; | 115 | export * from "./sources/source-store/config"; |
| 116 | export * from "./sources/source-store/plugin-local-config"; | 116 | export * from "./sources/source-store/plugin-scope-config"; |
| 117 | export { | 117 | export { |
| 118 | LocalInstanceConfigService, | 118 | LocalInstanceConfigService, |
| 119 | SecretMaterialDeleterService, | 119 | SecretMaterialDeleterService, |
| @@ -184,9 +184,9 @@ export type BoundInstallationStore = { · 64 unmodified lines | |||
| 184 | }; | 184 | }; |
| 185 | 185 | ||
| 186 | export type BoundScopeConfigStore = { | 186 | export type BoundScopeConfigStore = { |
| 187 | load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>; | 187 | load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>; |
| 188 | writeProject: ( | 188 | writeProject: ( |
| 189 | config: LocalExecutorConfig, | 189 | config: ExecutorScopeConfig, |
| 190 | ) => Effect.Effect<void, Error, never>; | 190 | ) => Effect.Effect<void, Error, never>; |
| 191 | resolveRelativePath: ScopeConfigStoreShape["resolveRelativePath"]; | 191 | resolveRelativePath: ScopeConfigStoreShape["resolveRelativePath"]; |
| 192 | }; | 192 | }; |
| @@ -1,11 +1,11 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | LocalExecutorConfig, | 2 | ExecutorScopeConfig, |
| 3 | } from "#schema"; | 3 | } from "#schema"; |
| 4 | 4 | ||
| 5 | export type LoadedExecutorConfig = { | 5 | export type LoadedExecutorConfig = { |
| 6 | config: LocalExecutorConfig | null; | 6 | config: ExecutorScopeConfig | null; |
| 7 | homeConfig: LocalExecutorConfig | null; | 7 | homeConfig: ExecutorScopeConfig | null; |
| 8 | projectConfig: LocalExecutorConfig | null; | 8 | projectConfig: ExecutorScopeConfig | null; |
| 9 | }; | 9 | }; |
| 10 | 10 | ||
| 11 | export type LoadedLocalExecutorConfig = LoadedExecutorConfig; | 11 | export type LoadedExecutorScopeConfig = LoadedExecutorConfig; |
| @@ -10,7 +10,7 @@ import type { · 9 unmodified lines | |||
| 10 | ExecutorScopeContext, | 10 | ExecutorScopeContext, |
| 11 | } from "../../scope"; | 11 | } from "../../scope"; |
| 12 | import type { | 12 | import type { |
| 13 | LoadedLocalExecutorConfig, | 13 | LoadedExecutorScopeConfig, |
| 14 | } from "../scope-config"; | 14 | } from "../scope-config"; |
| 15 | import { | 15 | import { |
| 16 | RuntimeLocalScopeMismatchError, | 16 | RuntimeLocalScopeMismatchError, |
| @@ -24,7 +24,7 @@ export type RuntimeLocalScopeState = { · 7 unmodified lines | |||
| 24 | actorScopeId: ScopeId; | 24 | actorScopeId: ScopeId; |
| 25 | resolutionScopeIds: ReadonlyArray<ScopeId>; | 25 | resolutionScopeIds: ReadonlyArray<ScopeId>; |
| 26 | }; | 26 | }; |
| 27 | loadedConfig: LoadedLocalExecutorConfig; | 27 | loadedConfig: LoadedExecutorScopeConfig; |
| 28 | }; | 28 | }; |
| 29 | 29 | ||
| 30 | export class RuntimeLocalScopeService extends Context.Tag( | 30 | export class RuntimeLocalScopeService extends Context.Tag( |
| @@ -1,7 +1,7 @@ | |||
| 1 | import * as Effect from "effect/Effect"; | 1 | import * as Effect from "effect/Effect"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | type LoadedLocalExecutorConfig, | 4 | type LoadedExecutorScopeConfig, |
| 5 | } from "../scope-config"; | 5 | } from "../scope-config"; |
| 6 | import { | 6 | import { |
| 7 | ScopeStateStore, | 7 | ScopeStateStore, |
| @@ -40,7 +40,7 @@ export const derivePolicyConfigKey = ( · 32 unmodified lines | |||
| 40 | }; | 40 | }; |
| 41 | 41 | ||
| 42 | const pruneLocalWorkspaceState = (input: { | 42 | const pruneLocalWorkspaceState = (input: { |
| 43 | loadedConfig: LoadedLocalExecutorConfig; | 43 | loadedConfig: LoadedExecutorScopeConfig; |
| 44 | }): Effect.Effect<LocalScopeState, Error, ScopeStateStore> => | 44 | }): Effect.Effect<LocalScopeState, Error, ScopeStateStore> => |
| 45 | Effect.gen(function* () { | 45 | Effect.gen(function* () { |
| 46 | const scopeStateStore = yield* ScopeStateStore; | 46 | const scopeStateStore = yield* ScopeStateStore; |
| @@ -79,8 +79,8 @@ const pruneLocalWorkspaceState = (input: { · 32 unmodified lines | |||
| 79 | }); | 79 | }); |
| 80 | 80 | ||
| 81 | export const synchronizeLocalScopeState = (input: { | 81 | export const synchronizeLocalScopeState = (input: { |
| 82 | loadedConfig: LoadedLocalExecutorConfig; | 82 | loadedConfig: LoadedExecutorScopeConfig; |
| 83 | }): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, ScopeStateStore> => | 83 | }): Effect.Effect<LoadedExecutorScopeConfig["config"], Error, ScopeStateStore> => |
| 84 | Effect.gen(function* () { | 84 | Effect.gen(function* () { |
| 85 | yield* pruneLocalWorkspaceState({ | 85 | yield* pruneLocalWorkspaceState({ |
| 86 | loadedConfig: input.loadedConfig, | 86 | loadedConfig: input.loadedConfig, |
| @@ -1,12 +1,12 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | LocalInstallation, | 2 | LocalInstallation, |
| 3 | LocalExecutorConfig, | 3 | ExecutorScopeConfig, |
| 4 | } from "#schema"; | 4 | } from "#schema"; |
| 5 | import * as Context from "effect/Context"; | 5 | import * as Context from "effect/Context"; |
| 6 | import * as Layer from "effect/Layer"; | 6 | import * as Layer from "effect/Layer"; |
| 7 | 7 | ||
| 8 | import type { | 8 | import type { |
| 9 | LoadedLocalExecutorConfig, | 9 | LoadedExecutorScopeConfig, |
| 10 | } from "../scope-config"; | 10 | } from "../scope-config"; |
| 11 | import type { | 11 | import type { |
| 12 | LocalSourceArtifact, | 12 | LocalSourceArtifact, |
| @@ -31,9 +31,9 @@ export class InstallationStore extends Context.Tag( · 18 unmodified lines | |||
| 31 | )<InstallationStore, InstallationStoreShape>() {} | 31 | )<InstallationStore, InstallationStoreShape>() {} |
| 32 | 32 | ||
| 33 | export type ScopeConfigStoreShape = { | 33 | export type ScopeConfigStoreShape = { |
| 34 | load: () => import("effect/Effect").Effect<LoadedLocalExecutorConfig, Error, never>; | 34 | load: () => import("effect/Effect").Effect<LoadedExecutorScopeConfig, Error, never>; |
| 35 | writeProject: (input: { | 35 | writeProject: (input: { |
| 36 | config: LocalExecutorConfig; | 36 | config: ExecutorScopeConfig; |
| 37 | }) => import("effect/Effect").Effect<void, Error, never>; | 37 | }) => import("effect/Effect").Effect<void, Error, never>; |
| 38 | resolveRelativePath: (input: { path: string; scopeRoot: string }) => string; | 38 | resolveRelativePath: (input: { path: string; scopeRoot: string }) => string; |
| 39 | }; | 39 | }; |
| @@ -1,5 +1,5 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | LocalConfigSource, | 2 | ExecutorScopeConfigSource, |
| 3 | Source, | 3 | Source, |
| 4 | SourceId, | 4 | SourceId, |
| 5 | } from "#schema"; | 5 | } from "#schema"; |
| @@ -23,7 +23,7 @@ export const trimOrNull = (value: string | null | undefined): string | null => { · 17 unmodified lines | |||
| 23 | export const cloneJson = <T>(value: T): T => | 23 | export const cloneJson = <T>(value: T): T => |
| 24 | JSON.parse(JSON.stringify(value)) as T; | 24 | JSON.parse(JSON.stringify(value)) as T; |
| 25 | 25 | ||
| 26 | export const deriveLocalSourceId = ( | 26 | export const deriveScopeConfigSourceId = ( |
| 27 | source: Pick<Source, "namespace" | "name">, | 27 | source: Pick<Source, "namespace" | "name">, |
| 28 | used: ReadonlySet<string>, | 28 | used: ReadonlySet<string>, |
| 29 | ): SourceId => { | 29 | ): SourceId => { |
| @@ -38,9 +38,9 @@ export const deriveLocalSourceId = ( · 8 unmodified lines | |||
| 38 | return SourceIdSchema.make(candidate); | 38 | return SourceIdSchema.make(candidate); |
| 39 | }; | 39 | }; |
| 40 | 40 | ||
| 41 | export const configSourceBaseFromLocalSource = (input: { | 41 | export const scopeConfigSourceBaseFromSource = (input: { |
| 42 | source: Source; | 42 | source: Source; |
| 43 | }): Omit<LocalConfigSource, "kind" | "config" | "connection" | "binding"> => ({ | 43 | }): Omit<ExecutorScopeConfigSource, "kind" | "config"> => ({ |
| 44 | ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id) | 44 | ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id) |
| 45 | ? { name: input.source.name } | 45 | ? { name: input.source.name } |
| 46 | : {}), | 46 | : {}), |
| @@ -50,29 +50,19 @@ export const configSourceBaseFromLocalSource = (input: { · 3 unmodified lines | |||
| 50 | ...(input.source.enabled === false ? { enabled: false } : {}), | 50 | ...(input.source.enabled === false ? { enabled: false } : {}), |
| 51 | }); | 51 | }); |
| 52 | 52 | ||
| 53 | export const configSourceFromLocalSource = (input: { | 53 | export const scopeConfigSourceFromSource = (input: { |
| 54 | source: Source; | 54 | source: Source; |
| 55 | existingConfig?: LocalConfigSource | null; | 55 | existingConfig?: ExecutorScopeConfigSource | null; |
| 56 | }): LocalConfigSource => { | 56 | }): ExecutorScopeConfigSource => { |
| 57 | return { | 57 | return { |
| 58 | ...configSourceBaseFromLocalSource({ | 58 | ...scopeConfigSourceBaseFromSource({ |
| 59 | source: input.source, | 59 | source: input.source, |
| 60 | }), | 60 | }), |
| 61 | kind: input.source.kind as LocalConfigSource["kind"], | 61 | kind: input.source.kind as ExecutorScopeConfigSource["kind"], |
| 62 | ...(input.existingConfig?.config !== undefined | 62 | ...(input.existingConfig?.config !== undefined |
| 63 | ? { | 63 | ? { |
| 64 | config: cloneJson(input.existingConfig.config), | 64 | config: cloneJson(input.existingConfig.config), |
| 65 | } | 65 | } |
| 66 | : {}), | 66 | : {}), |
| 67 | ...(input.existingConfig?.connection !== undefined | 67 | } as ExecutorScopeConfigSource; |
| 68 | ? { | ||
| 69 | connection: cloneJson(input.existingConfig.connection), | ||
| 70 | } | ||
| 71 | : {}), | ||
| 72 | ...(input.existingConfig?.binding !== undefined | ||
| 73 | ? { | ||
| 74 | binding: cloneJson(input.existingConfig.binding), | ||
| 75 | } | ||
| 76 | : {}), | ||
| 77 | } as LocalConfigSource; | ||
| 78 | }; | 68 | }; |
| @@ -4,7 +4,7 @@ import type { · 3 unmodified lines | |||
| 4 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | 5 | ||
| 6 | import type { | 6 | import type { |
| 7 | LoadedLocalExecutorConfig, | 7 | LoadedExecutorScopeConfig, |
| 8 | } from "../../scope-config"; | 8 | } from "../../scope-config"; |
| 9 | import { | 9 | import { |
| 10 | SourceTypeDeclarationsRefresherService, | 10 | SourceTypeDeclarationsRefresherService, |
| @@ -53,7 +53,7 @@ export type ResolvedSourceStoreScope = { · 42 unmodified lines | |||
| 53 | scopeConfigStore: ScopeConfigStoreShape; | 53 | scopeConfigStore: ScopeConfigStoreShape; |
| 54 | scopeStateStore: ScopeStateStoreShape; | 54 | scopeStateStore: ScopeStateStoreShape; |
| 55 | sourceArtifactStore: SourceArtifactStoreShape; | 55 | sourceArtifactStore: SourceArtifactStoreShape; |
| 56 | loadedConfig: LoadedLocalExecutorConfig; | 56 | loadedConfig: LoadedExecutorScopeConfig; |
| 57 | scopeState: LocalScopeState; | 57 | scopeState: LocalScopeState; |
| 58 | }; | 58 | }; |
| 59 | 59 | ||
| @@ -6,9 +6,9 @@ import * as Effect from "effect/Effect"; · 5 unmodified lines | |||
| 6 | 6 | ||
| 7 | import type { LocalScopeState } from "../../scope-state"; | 7 | import type { LocalScopeState } from "../../scope-state"; |
| 8 | import { | 8 | import { |
| 9 | configSourceFromLocalSource, | 9 | scopeConfigSourceFromSource, |
| 10 | cloneJson, | 10 | cloneJson, |
| 11 | deriveLocalSourceId, | 11 | deriveScopeConfigSourceId, |
| 12 | } from "./config"; | 12 | } from "./config"; |
| 13 | import { | 13 | import { |
| 14 | type RuntimeSourceStoreDeps, | 14 | type RuntimeSourceStoreDeps, |
| @@ -84,7 +84,7 @@ export const persistSourceWithDeps = ( · 69 unmodified lines | |||
| 84 | localScope.loadedConfig.config?.sources?.[source.id] || | 84 | localScope.loadedConfig.config?.sources?.[source.id] || |
| 85 | localScope.scopeState.sources[source.id] | 85 | localScope.scopeState.sources[source.id] |
| 86 | ? source.id | 86 | ? source.id |
| 87 | : deriveLocalSourceId( | 87 | : deriveScopeConfigSourceId( |
| 88 | source, | 88 | source, |
| 89 | new Set(Object.keys(localScope.loadedConfig.config?.sources ?? {})), | 89 | new Set(Object.keys(localScope.loadedConfig.config?.sources ?? {})), |
| 90 | ), | 90 | ), |
| @@ -94,7 +94,7 @@ export const persistSourceWithDeps = ( · 3 unmodified lines | |||
| 94 | const sources = { | 94 | const sources = { |
| 95 | ...projectConfig.sources, | 95 | ...projectConfig.sources, |
| 96 | }; | 96 | }; |
| 97 | sources[nextSource.id] = configSourceFromLocalSource({ | 97 | sources[nextSource.id] = scopeConfigSourceFromSource({ |
| 98 | source: nextSource, | 98 | source: nextSource, |
| 99 | existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null, | 99 | existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null, |
| 100 | }); | 100 | }); |
| @@ -1,14 +1,14 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | LocalConfigSource, | 2 | ExecutorScopeConfigSource, |
| 3 | Source, | 3 | Source, |
| 4 | } from "#schema"; | 4 | } from "#schema"; |
| 5 | import * as Schema from "effect/Schema"; | 5 | import * as Schema from "effect/Schema"; |
| 6 | import { | 6 | import { |
| 7 | cloneJson, | 7 | cloneJson, |
| 8 | configSourceBaseFromLocalSource, | 8 | scopeConfigSourceBaseFromSource, |
| 9 | } from "./config"; | 9 | } from "./config"; |
| 10 | 10 | ||
| 11 | export const createPluginLocalConfigEntrySchema = < | 11 | export const createPluginScopeConfigEntrySchema = < |
| 12 | TKind extends string, | 12 | TKind extends string, |
| 13 | TConfig, | 13 | TConfig, |
| 14 | >( | 14 | >( |
| @@ -23,17 +23,15 @@ export const createPluginLocalConfigEntrySchema = < · 8 unmodified lines | |||
| 23 | namespace: Schema.optional(Schema.String), | 23 | namespace: Schema.optional(Schema.String), |
| 24 | enabled: Schema.optional(Schema.Boolean), | 24 | enabled: Schema.optional(Schema.Boolean), |
| 25 | config: input.config, | 25 | config: input.config, |
| 26 | connection: Schema.optional(Schema.Unknown), | ||
| 27 | binding: Schema.optional(Schema.Unknown), | ||
| 28 | }); | 26 | }); |
| 29 | 27 | ||
| 30 | export const pluginLocalConfigSourceFromConfig = <TConfig>(input: { | 28 | export const pluginScopeConfigSourceFromConfig = <TConfig>(input: { |
| 31 | source: Source; | 29 | source: Source; |
| 32 | config: TConfig; | 30 | config: TConfig; |
| 33 | }): LocalConfigSource => ({ | 31 | }): ExecutorScopeConfigSource => ({ |
| 34 | ...configSourceBaseFromLocalSource({ | 32 | ...scopeConfigSourceBaseFromSource({ |
| 35 | source: input.source, | 33 | source: input.source, |
| 36 | }), | 34 | }), |
| 37 | kind: input.source.kind as LocalConfigSource["kind"], | 35 | kind: input.source.kind as ExecutorScopeConfigSource["kind"], |
| 38 | config: cloneJson(input.config), | 36 | config: cloneJson(input.config), |
| 39 | }); | 37 | }); |
| @@ -6,7 +6,7 @@ import type { · 5 unmodified lines | |||
| 6 | import { SourceIdSchema } from "#schema"; | 6 | import { SourceIdSchema } from "#schema"; |
| 7 | import * as Effect from "effect/Effect"; | 7 | import * as Effect from "effect/Effect"; |
| 8 | 8 | ||
| 9 | import type { LoadedLocalExecutorConfig } from "../../scope-config"; | 9 | import type { LoadedExecutorScopeConfig } from "../../scope-config"; |
| 10 | import { | 10 | import { |
| 11 | LocalConfiguredSourceNotFoundError, | 11 | LocalConfiguredSourceNotFoundError, |
| 12 | RuntimeLocalScopeMismatchError, | 12 | RuntimeLocalScopeMismatchError, |
| @@ -21,7 +21,7 @@ import { trimOrNull } from "./config"; · 8 unmodified lines | |||
| 21 | 21 | ||
| 22 | export const buildLocalSourceRecord = (input: { | 22 | export const buildLocalSourceRecord = (input: { |
| 23 | scopeId: ScopeId; | 23 | scopeId: ScopeId; |
| 24 | loadedConfig: LoadedLocalExecutorConfig; | 24 | loadedConfig: LoadedExecutorScopeConfig; |
| 25 | scopeState: LocalScopeState; | 25 | scopeState: LocalScopeState; |
| 26 | sourceId: SourceId; | 26 | sourceId: SourceId; |
| 27 | }): Effect.Effect< | 27 | }): Effect.Effect< |
| @@ -9,7 +9,7 @@ export * from "./models/secret-material"; · 8 unmodified lines | |||
| 9 | export * from "./models/secret-store"; | 9 | export * from "./models/secret-store"; |
| 10 | export * from "./models/policy"; | 10 | export * from "./models/policy"; |
| 11 | export * from "./models/local-installation"; | 11 | export * from "./models/local-installation"; |
| 12 | export * from "./models/local-config"; | 12 | export * from "./models/scope-config"; |
| 13 | export * from "./models/execution"; | 13 | export * from "./models/execution"; |
| 14 | export * from "./models/source-inspection"; | 14 | export * from "./models/source-inspection"; |
| 15 | export * from "./models/code-migration"; | 15 | export * from "./models/code-migration"; |
| @@ -1,68 +0,0 @@ | |||
| 1 | import { | ||
| 2 | Schema, | ||
| 3 | } from "effect"; | ||
| 4 | |||
| 5 | export const LocalExecutorRuntimeSchema = Schema.Literal( | ||
| 6 | "quickjs", | ||
| 7 | "ses", | ||
| 8 | "deno", | ||
| 9 | ); | ||
| 10 | |||
| 11 | export const LocalConfigSourceConnectionSchema = Schema.Struct({ | ||
| 12 | endpoint: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 13 | auth: Schema.optional(Schema.Unknown), | ||
| 14 | }); | ||
| 15 | |||
| 16 | const LocalConfigSourceEntryBaseSchema = Schema.Struct({ | ||
| 17 | name: Schema.optional(Schema.String), | ||
| 18 | namespace: Schema.optional(Schema.String), | ||
| 19 | enabled: Schema.optional(Schema.Boolean), | ||
| 20 | config: Schema.optional(Schema.Unknown), | ||
| 21 | connection: Schema.optional(LocalConfigSourceConnectionSchema), | ||
| 22 | binding: Schema.optional(Schema.Unknown), | ||
| 23 | }); | ||
| 24 | |||
| 25 | export const LocalConfigSourceSchema = Schema.extend( | ||
| 26 | LocalConfigSourceEntryBaseSchema, | ||
| 27 | Schema.Struct({ | ||
| 28 | kind: Schema.String, | ||
| 29 | }), | ||
| 30 | ); | ||
| 31 | |||
| 32 | export type LocalConfigSource = typeof LocalConfigSourceSchema.Type; | ||
| 33 | |||
| 34 | export const LocalConfigPolicyActionSchema = Schema.Literal("allow", "deny"); | ||
| 35 | export const LocalConfigPolicyApprovalSchema = Schema.Literal("auto", "manual"); | ||
| 36 | |||
| 37 | export const LocalConfigPolicySchema = Schema.Struct({ | ||
| 38 | match: Schema.String, | ||
| 39 | action: LocalConfigPolicyActionSchema, | ||
| 40 | approval: LocalConfigPolicyApprovalSchema, | ||
| 41 | enabled: Schema.optional(Schema.Boolean), | ||
| 42 | priority: Schema.optional(Schema.Number), | ||
| 43 | }); | ||
| 44 | |||
| 45 | export const LocalConfigWorkspaceSchema = Schema.Struct({ | ||
| 46 | name: Schema.optional(Schema.String), | ||
| 47 | }); | ||
| 48 | |||
| 49 | export const LocalExecutorConfigSchema = Schema.Struct({ | ||
| 50 | runtime: Schema.optional(LocalExecutorRuntimeSchema), | ||
| 51 | workspace: Schema.optional(LocalConfigWorkspaceSchema), | ||
| 52 | sources: Schema.optional( | ||
| 53 | Schema.Record({ | ||
| 54 | key: Schema.String, | ||
| 55 | value: LocalConfigSourceSchema, | ||
| 56 | }), | ||
| 57 | ), | ||
| 58 | policies: Schema.optional( | ||
| 59 | Schema.Record({ | ||
| 60 | key: Schema.String, | ||
| 61 | value: LocalConfigPolicySchema, | ||
| 62 | }), | ||
| 63 | ), | ||
| 64 | }); | ||
| 65 | |||
| 66 | export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type; | ||
| 67 | export type LocalExecutorRuntime = typeof LocalExecutorRuntimeSchema.Type; | ||
| 68 | export type LocalExecutorConfig = typeof LocalExecutorConfigSchema.Type; | ||
| @@ -0,0 +1,65 @@ | |||
| 1 | import { | ||
| 2 | Schema, | ||
| 3 | } from "effect"; | ||
| 4 | |||
| 5 | export const ExecutorRuntimeConfigSchema = Schema.Literal( | ||
| 6 | "quickjs", | ||
| 7 | "ses", | ||
| 8 | "deno", | ||
| 9 | ); | ||
| 10 | |||
| 11 | const ExecutorScopeConfigSourceBaseSchema = Schema.Struct({ | ||
| 12 | name: Schema.optional(Schema.String), | ||
| 13 | namespace: Schema.optional(Schema.String), | ||
| 14 | enabled: Schema.optional(Schema.Boolean), | ||
| 15 | config: Schema.optional(Schema.Unknown), | ||
| 16 | }); | ||
| 17 | |||
| 18 | export const ExecutorScopeConfigSourceSchema = Schema.extend( | ||
| 19 | ExecutorScopeConfigSourceBaseSchema, | ||
| 20 | Schema.Struct({ | ||
| 21 | kind: Schema.String, | ||
| 22 | }), | ||
| 23 | ); | ||
| 24 | |||
| 25 | export type ExecutorScopeConfigSource = typeof ExecutorScopeConfigSourceSchema.Type; | ||
| 26 | |||
| 27 | export const ExecutorScopeConfigPolicyActionSchema = Schema.Literal("allow", "deny"); | ||
| 28 | export const ExecutorScopeConfigPolicyApprovalSchema = Schema.Literal( | ||
| 29 | "auto", | ||
| 30 | "manual", | ||
| 31 | ); | ||
| 32 | |||
| 33 | export const ExecutorScopeConfigPolicySchema = Schema.Struct({ | ||
| 34 | match: Schema.String, | ||
| 35 | action: ExecutorScopeConfigPolicyActionSchema, | ||
| 36 | approval: ExecutorScopeConfigPolicyApprovalSchema, | ||
| 37 | enabled: Schema.optional(Schema.Boolean), | ||
| 38 | priority: Schema.optional(Schema.Number), | ||
| 39 | }); | ||
| 40 | |||
| 41 | export const ExecutorScopeWorkspaceConfigSchema = Schema.Struct({ | ||
| 42 | name: Schema.optional(Schema.String), | ||
| 43 | }); | ||
| 44 | |||
| 45 | export const ExecutorScopeConfigSchema = Schema.Struct({ | ||
| 46 | runtime: Schema.optional(ExecutorRuntimeConfigSchema), | ||
| 47 | workspace: Schema.optional(ExecutorScopeWorkspaceConfigSchema), | ||
| 48 | sources: Schema.optional( | ||
| 49 | Schema.Record({ | ||
| 50 | key: Schema.String, | ||
| 51 | value: ExecutorScopeConfigSourceSchema, | ||
| 52 | }), | ||
| 53 | ), | ||
| 54 | policies: Schema.optional( | ||
| 55 | Schema.Record({ | ||
| 56 | key: Schema.String, | ||
| 57 | value: ExecutorScopeConfigPolicySchema, | ||
| 58 | }), | ||
| 59 | ), | ||
| 60 | }); | ||
| 61 | |||
| 62 | export type ExecutorScopeConfigPolicy = | ||
| 63 | typeof ExecutorScopeConfigPolicySchema.Type; | ||
| 64 | export type ExecutorRuntimeConfig = typeof ExecutorRuntimeConfigSchema.Type; | ||
| 65 | export type ExecutorScopeConfig = typeof ExecutorScopeConfigSchema.Type; | ||
| @@ -34,10 +34,10 @@ import { · 33 unmodified lines | |||
| 34 | getOrProvisionLocalInstallation, | 34 | getOrProvisionLocalInstallation, |
| 35 | resolveLocalWorkspaceContext, | 35 | resolveLocalWorkspaceContext, |
| 36 | writeLocalSourceArtifact, | 36 | writeLocalSourceArtifact, |
| 37 | writeProjectLocalExecutorConfig, | 37 | writeProjectExecutorScopeConfig, |
| 38 | } from "@executor/platform-sdk-file/effect"; | 38 | } from "@executor/platform-sdk-file/effect"; |
| 39 | import { | 39 | import { |
| 40 | type LocalConfigSource, | 40 | type ExecutorScopeConfigSource, |
| 41 | SourceCatalogIdSchema, | 41 | SourceCatalogIdSchema, |
| 42 | SourceCatalogRevisionIdSchema, | 42 | SourceCatalogRevisionIdSchema, |
| 43 | SourceIdSchema, | 43 | SourceIdSchema, |
| @@ -136,7 +136,7 @@ const writeConfiguredLocalMcpSource = (input: { · 92 unmodified lines | |||
| 136 | }); | 136 | }); |
| 137 | const installation = yield* getOrProvisionLocalInstallation({ context }); | 137 | const installation = yield* getOrProvisionLocalInstallation({ context }); |
| 138 | 138 | ||
| 139 | yield* writeProjectLocalExecutorConfig({ | 139 | yield* writeProjectExecutorScopeConfig({ |
| 140 | context, | 140 | context, |
| 141 | config: { | 141 | config: { |
| 142 | sources: { | 142 | sources: { |
| @@ -274,7 +274,7 @@ const writeConfiguredLocalMcpSource = (input: { · 131 unmodified lines | |||
| 274 | 274 | ||
| 275 | const writeConfiguredWorkspaceSources = (input: { | 275 | const writeConfiguredWorkspaceSources = (input: { |
| 276 | workspaceRoot: string; | 276 | workspaceRoot: string; |
| 277 | sources: Record<string, LocalConfigSource>; | 277 | sources: Record<string, ExecutorScopeConfigSource>; |
| 278 | }) => | 278 | }) => |
| 279 | Effect.gen(function* () { | 279 | Effect.gen(function* () { |
| 280 | const context = yield* resolveLocalWorkspaceContext({ | 280 | const context = yield* resolveLocalWorkspaceContext({ |
| @@ -282,7 +282,7 @@ const writeConfiguredWorkspaceSources = (input: { · 1 unmodified line | |||
| 282 | }); | 282 | }); |
| 283 | const installation = yield* getOrProvisionLocalInstallation({ context }); | 283 | const installation = yield* getOrProvisionLocalInstallation({ context }); |
| 284 | 284 | ||
| 285 | yield* writeProjectLocalExecutorConfig({ | 285 | yield* writeProjectExecutorScopeConfig({ |
| 286 | context, | 286 | context, |
| 287 | config: { | 287 | config: { |
| 288 | sources: input.sources, | 288 | sources: input.sources, |
| @@ -7,7 +7,7 @@ import { · 6 unmodified lines | |||
| 7 | createSourceCatalogSyncResult, | 7 | createSourceCatalogSyncResult, |
| 8 | } from "@executor/source-core"; | 8 | } from "@executor/source-core"; |
| 9 | import type { | 9 | import type { |
| 10 | LocalExecutorConfig, | 10 | ExecutorScopeConfig, |
| 11 | Source, | 11 | Source, |
| 12 | } from "@executor/platform-sdk/schema"; | 12 | } from "@executor/platform-sdk/schema"; |
| 13 | import { | 13 | import { |
| @@ -17,8 +17,8 @@ import { · 3 unmodified lines | |||
| 17 | defineExecutorSourcePlugin, | 17 | defineExecutorSourcePlugin, |
| 18 | } from "@executor/platform-sdk/plugins"; | 18 | } from "@executor/platform-sdk/plugins"; |
| 19 | import { | 19 | import { |
| 20 | createPluginLocalConfigEntrySchema, | 20 | createPluginScopeConfigEntrySchema, |
| 21 | pluginLocalConfigSourceFromConfig, | 21 | pluginScopeConfigSourceFromConfig, |
| 22 | SecretMaterialDeleterService, | 22 | SecretMaterialDeleterService, |
| 23 | SecretMaterialResolverService, | 23 | SecretMaterialResolverService, |
| 24 | SecretMaterialStorerService, | 24 | SecretMaterialStorerService, |
| @@ -97,7 +97,7 @@ const decodeBatchSourceInput = Schema.decodeUnknownSync( · 72 unmodified lines | |||
| 97 | const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema); | 97 | const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema); |
| 98 | const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema); | 98 | const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema); |
| 99 | const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption( | 99 | const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 100 | createPluginLocalConfigEntrySchema({ | 100 | createPluginScopeConfigEntrySchema({ |
| 101 | kind: GOOGLE_DISCOVERY_SOURCE_KIND, | 101 | kind: GOOGLE_DISCOVERY_SOURCE_KIND, |
| 102 | config: GoogleDiscoveryStoredSourceDataSchema, | 102 | config: GoogleDiscoveryStoredSourceDataSchema, |
| 103 | }), | 103 | }), |
| @@ -563,7 +563,7 @@ const googleDiscoveryConnectInputFromAddInput = ( · 459 unmodified lines | |||
| 563 | 563 | ||
| 564 | const googleDiscoveryStoredSourceDataFromLocalConfig = (input: { | 564 | const googleDiscoveryStoredSourceDataFromLocalConfig = (input: { |
| 565 | config: unknown; | 565 | config: unknown; |
| 566 | loadedConfig: LocalExecutorConfig | null; | 566 | loadedConfig: ExecutorScopeConfig | null; |
| 567 | }): GoogleDiscoveryStoredSourceData => { | 567 | }): GoogleDiscoveryStoredSourceData => { |
| 568 | const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config); | 568 | const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config); |
| 569 | if (Option.isSome(current)) { | 569 | if (Option.isSome(current)) { |
| @@ -629,9 +629,9 @@ export const googleDiscoverySdkPlugin = (options: { · 59 unmodified lines | |||
| 629 | } | 629 | } |
| 630 | }), | 630 | }), |
| 631 | }, | 631 | }, |
| 632 | localConfig: { | 632 | scopeConfig: { |
| 633 | toConfigSource: ({ source, stored }) => | 633 | toConfigSource: ({ source, stored }) => |
| 634 | pluginLocalConfigSourceFromConfig({ | 634 | pluginScopeConfigSourceFromConfig({ |
| 635 | source, | 635 | source, |
| 636 | config: stored, | 636 | config: stored, |
| 637 | }), | 637 | }), |
| @@ -12,7 +12,7 @@ import { · 11 unmodified lines | |||
| 12 | createSourceCatalogSyncResult, | 12 | createSourceCatalogSyncResult, |
| 13 | } from "@executor/source-core"; | 13 | } from "@executor/source-core"; |
| 14 | import type { | 14 | import type { |
| 15 | LocalExecutorConfig, | 15 | ExecutorScopeConfig, |
| 16 | Source, | 16 | Source, |
| 17 | } from "@executor/platform-sdk/schema"; | 17 | } from "@executor/platform-sdk/schema"; |
| 18 | import { | 18 | import { |
| @@ -23,8 +23,8 @@ import { · 4 unmodified lines | |||
| 23 | } from "@executor/platform-sdk/plugins"; | 23 | } from "@executor/platform-sdk/plugins"; |
| 24 | import { | 24 | import { |
| 25 | SecretMaterialResolverService, | 25 | SecretMaterialResolverService, |
| 26 | createPluginLocalConfigEntrySchema, | 26 | createPluginScopeConfigEntrySchema, |
| 27 | pluginLocalConfigSourceFromConfig, | 27 | pluginScopeConfigSourceFromConfig, |
| 28 | provideExecutorRuntime, | 28 | provideExecutorRuntime, |
| 29 | runtimeEffectError, | 29 | runtimeEffectError, |
| 30 | } from "@executor/platform-sdk/runtime"; | 30 | } from "@executor/platform-sdk/runtime"; |
| @@ -151,7 +151,7 @@ const asStringRecord = (value: unknown): Record<string, string> => · 120 unmodified lines | |||
| 151 | ); | 151 | ); |
| 152 | 152 | ||
| 153 | const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption( | 153 | const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 154 | createPluginLocalConfigEntrySchema({ | 154 | createPluginScopeConfigEntrySchema({ |
| 155 | kind: "graphql", | 155 | kind: "graphql", |
| 156 | config: GraphqlStoredSourceDataSchema, | 156 | config: GraphqlStoredSourceDataSchema, |
| 157 | }), | 157 | }), |
| @@ -336,7 +336,7 @@ const graphqlConnectInputFromAddInput = ( · 178 unmodified lines | |||
| 336 | 336 | ||
| 337 | const graphqlStoredSourceDataFromLocalConfig = (input: { | 337 | const graphqlStoredSourceDataFromLocalConfig = (input: { |
| 338 | config: unknown; | 338 | config: unknown; |
| 339 | loadedConfig: LocalExecutorConfig | null; | 339 | loadedConfig: ExecutorScopeConfig | null; |
| 340 | }): GraphqlStoredSourceData => { | 340 | }): GraphqlStoredSourceData => { |
| 341 | const current = decodeGraphqlCurrentLocalConfigOption(input.config); | 341 | const current = decodeGraphqlCurrentLocalConfigOption(input.config); |
| 342 | if (Option.isSome(current)) { | 342 | if (Option.isSome(current)) { |
| @@ -397,9 +397,9 @@ export const graphqlSdkPlugin = (options: { · 54 unmodified lines | |||
| 397 | toConfig: ({ source, stored }) => | 397 | toConfig: ({ source, stored }) => |
| 398 | sourceConfigFromStored(source, normalizeStoredSourceData(stored)), | 398 | sourceConfigFromStored(source, normalizeStoredSourceData(stored)), |
| 399 | }, | 399 | }, |
| 400 | localConfig: { | 400 | scopeConfig: { |
| 401 | toConfigSource: ({ source, stored }) => | 401 | toConfigSource: ({ source, stored }) => |
| 402 | pluginLocalConfigSourceFromConfig({ | 402 | pluginScopeConfigSourceFromConfig({ |
| 403 | source, | 403 | source, |
| 404 | config: normalizeStoredSourceData(stored), | 404 | config: normalizeStoredSourceData(stored), |
| 405 | }), | 405 | }), |
| @@ -31,8 +31,8 @@ import { · 30 unmodified lines | |||
| 31 | defineExecutorSourcePlugin, | 31 | defineExecutorSourcePlugin, |
| 32 | } from "@executor/platform-sdk/plugins"; | 32 | } from "@executor/platform-sdk/plugins"; |
| 33 | import { | 33 | import { |
| 34 | createPluginLocalConfigEntrySchema, | 34 | createPluginScopeConfigEntrySchema, |
| 35 | pluginLocalConfigSourceFromConfig, | 35 | pluginScopeConfigSourceFromConfig, |
| 36 | SecretMaterialDeleterService, | 36 | SecretMaterialDeleterService, |
| 37 | SecretMaterialResolverService, | 37 | SecretMaterialResolverService, |
| 38 | SecretMaterialStorerService, | 38 | SecretMaterialStorerService, |
| @@ -250,7 +250,7 @@ const normalizeStringArray = ( · 211 unmodified lines | |||
| 250 | }; | 250 | }; |
| 251 | 251 | ||
| 252 | const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption( | 252 | const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 253 | createPluginLocalConfigEntrySchema({ | 253 | createPluginScopeConfigEntrySchema({ |
| 254 | kind: "mcp", | 254 | kind: "mcp", |
| 255 | config: McpStoredSourceDataSchema, | 255 | config: McpStoredSourceDataSchema, |
| 256 | }), | 256 | }), |
| @@ -720,9 +720,9 @@ export const mcpSdkPlugin = ( · 463 unmodified lines | |||
| 720 | } | 720 | } |
| 721 | }), | 721 | }), |
| 722 | }, | 722 | }, |
| 723 | localConfig: { | 723 | scopeConfig: { |
| 724 | toConfigSource: ({ source, stored }) => | 724 | toConfigSource: ({ source, stored }) => |
| 725 | pluginLocalConfigSourceFromConfig({ | 725 | pluginScopeConfigSourceFromConfig({ |
| 726 | source, | 726 | source, |
| 727 | config: stored, | 727 | config: stored, |
| 728 | }), | 728 | }), |
| @@ -8,7 +8,7 @@ import { · 7 unmodified lines | |||
| 8 | createSourceCatalogSyncResult, | 8 | createSourceCatalogSyncResult, |
| 9 | } from "@executor/source-core"; | 9 | } from "@executor/source-core"; |
| 10 | import type { | 10 | import type { |
| 11 | LocalExecutorConfig, | 11 | ExecutorScopeConfig, |
| 12 | Source, | 12 | Source, |
| 13 | } from "@executor/platform-sdk/schema"; | 13 | } from "@executor/platform-sdk/schema"; |
| 14 | import { | 14 | import { |
| @@ -18,9 +18,9 @@ import { · 3 unmodified lines | |||
| 18 | defineExecutorSourcePlugin, | 18 | defineExecutorSourcePlugin, |
| 19 | } from "@executor/platform-sdk/plugins"; | 19 | } from "@executor/platform-sdk/plugins"; |
| 20 | import { | 20 | import { |
| 21 | createPluginLocalConfigEntrySchema, | 21 | createPluginScopeConfigEntrySchema, |
| 22 | provideExecutorRuntime, | 22 | provideExecutorRuntime, |
| 23 | pluginLocalConfigSourceFromConfig, | 23 | pluginScopeConfigSourceFromConfig, |
| 24 | runtimeEffectError, | 24 | runtimeEffectError, |
| 25 | SecretMaterialResolverService, | 25 | SecretMaterialResolverService, |
| 26 | } from "@executor/platform-sdk/runtime"; | 26 | } from "@executor/platform-sdk/runtime"; |
| @@ -157,7 +157,7 @@ const OpenApiLocalSourceConfigSchema = Schema.Struct({ · 130 unmodified lines | |||
| 157 | }); | 157 | }); |
| 158 | 158 | ||
| 159 | const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption( | 159 | const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 160 | createPluginLocalConfigEntrySchema({ | 160 | createPluginScopeConfigEntrySchema({ |
| 161 | kind: "openapi", | 161 | kind: "openapi", |
| 162 | config: OpenApiLocalSourceConfigSchema, | 162 | config: OpenApiLocalSourceConfigSchema, |
| 163 | }), | 163 | }), |
| @@ -193,7 +193,7 @@ const configFromStoredSourceData = ( · 29 unmodified lines | |||
| 193 | 193 | ||
| 194 | const openApiStoredSourceDataFromLocalConfig = (input: { | 194 | const openApiStoredSourceDataFromLocalConfig = (input: { |
| 195 | config: unknown; | 195 | config: unknown; |
| 196 | loadedConfig: LocalExecutorConfig | null; | 196 | loadedConfig: ExecutorScopeConfig | null; |
| 197 | }): OpenApiStoredSourceData => { | 197 | }): OpenApiStoredSourceData => { |
| 198 | const current = decodeOpenApiCurrentLocalConfigOption(input.config); | 198 | const current = decodeOpenApiCurrentLocalConfigOption(input.config); |
| 199 | if (Option.isSome(current)) { | 199 | if (Option.isSome(current)) { |
| @@ -539,9 +539,9 @@ export const openApiSdkPlugin = ( · 339 unmodified lines | |||
| 539 | toConfig: ({ source, stored }) => | 539 | toConfig: ({ source, stored }) => |
| 540 | configFromStoredSourceData(source, normalizeStoredSourceData(stored)), | 540 | configFromStoredSourceData(source, normalizeStoredSourceData(stored)), |
| 541 | }, | 541 | }, |
| 542 | localConfig: { | 542 | scopeConfig: { |
| 543 | toConfigSource: ({ source, stored }) => | 543 | toConfigSource: ({ source, stored }) => |
| 544 | pluginLocalConfigSourceFromConfig({ | 544 | pluginScopeConfigSourceFromConfig({ |
| 545 | source, | 545 | source, |
| 546 | config: { | 546 | config: { |
| 547 | specUrl: stored.specUrl, | 547 | specUrl: stored.specUrl, |