Unified · Split
| @@ -6,10 +6,10 @@ import * as Data from "effect/Data"; · 5 unmodified lines | ||
| 6 | 6 | import * as Effect from "effect/Effect"; |
| 7 | 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 | 11 | import { |
| 12 | − readOptionalLocalExecutorConfig, | |
| 12 | + readOptionalExecutorScopeConfig, | |
| 13 | 13 | resolveLocalWorkspaceContext, |
| 14 | 14 | } from "./config"; |
| 15 | 15 | import { |
| @@ -186,8 +186,8 @@ const copyRecursive = ( · 170 unmodified lines | ||
| 186 | 186 | }); |
| 187 | 187 | |
| 188 | 188 | const sanitizeSourceConfig = ( |
| 189 | − sourceConfig: NonNullable<LocalExecutorConfig["sources"]>[string], | |
| 190 | −): NonNullable<LocalExecutorConfig["sources"]>[string] => sourceConfig; | |
| 189 | + sourceConfig: NonNullable<ExecutorScopeConfig["sources"]>[string], | |
| 190 | +): NonNullable<ExecutorScopeConfig["sources"]>[string] => sourceConfig; | |
| 191 | 191 | |
| 192 | 192 | const main = Effect.gen(function* () { |
| 193 | 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 | 222 | context.projectConfigPath, |
| 223 | 223 | ).pipe( |
| 224 | 224 | Effect.mapError((cause) => |
| @@ -293,7 +293,7 @@ const main = Effect.gen(function* () { · 68 unmodified lines | ||
| 293 | 293 | sources: { |
| 294 | 294 | [args.sourceId]: sanitizeSourceConfig(sourceConfig), |
| 295 | 295 | }, |
| 296 | − } satisfies LocalExecutorConfig; | |
| 296 | + } satisfies ExecutorScopeConfig; | |
| 297 | 297 | |
| 298 | 298 | const fixtureState = { |
| 299 | 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 | 6 | import * as Effect from "effect/Effect"; |
| 7 | 7 | |
| 8 | 8 | import { |
| 9 | − loadLocalExecutorConfig, | |
| 10 | − mergeLocalExecutorConfigs, | |
| 9 | + loadExecutorScopeConfig, | |
| 10 | + mergeExecutorScopeConfigs, | |
| 11 | 11 | resolveDefaultHomeConfigCandidates, |
| 12 | 12 | resolveDefaultHomeStateDirectory, |
| 13 | 13 | resolveLocalWorkspaceContext, |
| @@ -53,7 +53,7 @@ describe("local-config", () => { · 39 unmodified lines | ||
| 53 | 53 | ); |
| 54 | 54 | |
| 55 | 55 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); |
| 56 | − const loaded = yield* loadLocalExecutorConfig(context); | |
| 56 | + const loaded = yield* loadExecutorScopeConfig(context); | |
| 57 | 57 | |
| 58 | 58 | expect(loaded.config?.sources?.github?.kind).toBe("openapi"); |
| 59 | 59 | expect(loaded.config?.runtime).toBe("ses"); |
| @@ -83,7 +83,7 @@ describe("local-config", () => { · 23 unmodified lines | ||
| 83 | 83 | ); |
| 84 | 84 | |
| 85 | 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 | 88 | expect(failure.message).toContain("Invalid executor config"); |
| 89 | 89 | expect(failure.message).toContain("line 5, column 7"); |
| @@ -129,7 +129,7 @@ describe("local-config", () => { · 39 unmodified lines | ||
| 129 | 129 | }); |
| 130 | 130 | |
| 131 | 131 | it("lets project config override the merged runtime", () => { |
| 132 | − const merged = mergeLocalExecutorConfigs( | |
| 132 | + const merged = mergeExecutorScopeConfigs( | |
| 133 | 133 | { |
| 134 | 134 | runtime: "quickjs", |
| 135 | 135 | sources: {}, |
| @@ -1,37 +1,30 @@ | ||
| 1 | 1 | import { homedir } from "node:os"; |
| 2 | 2 | import { basename, dirname, isAbsolute, join, resolve } from "node:path"; |
| 3 | 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 | 5 | import { |
| 11 | − LocalExecutorConfigSchema, | |
| 12 | − type LocalExecutorConfig, | |
| 13 | − type LocalConfigPolicy, | |
| 14 | − type LocalConfigSource, | |
| 6 | + type ExecutorScopeConfig, | |
| 7 | + type ExecutorScopeConfigPolicy, | |
| 8 | + type ExecutorScopeConfigSource, | |
| 15 | 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 | 11 | import * as Effect from "effect/Effect"; |
| 18 | −import * as Schema from "effect/Schema"; | |
| 19 | 12 | import { |
| 20 | − LocalExecutorConfigDecodeError, | |
| 13 | + decodeExecutorScopeConfig, | |
| 14 | + encodeExecutorScopeConfig, | |
| 15 | + parseExecutorScopeConfig, | |
| 16 | +} from "./config-codec"; | |
| 17 | +import { | |
| 18 | + ExecutorScopeConfigDecodeError, | |
| 21 | 19 | LocalFileSystemError, |
| 22 | 20 | unknownLocalErrorDetails, |
| 23 | 21 | } from "./errors"; |
| 24 | 22 | |
| 25 | −const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema); | |
| 26 | − | |
| 27 | −export type FileLoadedExecutorConfig = LoadedLocalExecutorConfig & { | |
| 23 | +export type FileLoadedExecutorConfig = LoadedExecutorScopeConfig & { | |
| 28 | 24 | homeConfigPath: string; |
| 29 | 25 | projectConfigPath: string; |
| 30 | 26 | }; |
| 31 | 27 | |
| 32 | −export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string => | |
| 33 | − `${JSON.stringify(config, null, 2)}\n`; | |
| 34 | − | |
| 35 | 28 | export const resolveConfigRelativePath = (input: { |
| 36 | 29 | path: string; |
| 37 | 30 | scopeRoot: string; |
| @@ -49,7 +42,8 @@ export const resolveConfigRelativePath = (input: { · 11 unmodified lines | ||
| 49 | 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 | 48 | const PROJECT_CONFIG_BASENAME = "executor.jsonc"; |
| 55 | 49 | const PROJECT_CONFIG_DIRECTORY = ".executor"; |
| @@ -173,59 +167,10 @@ export const resolveDefaultHomeStateDirectory = (input: { · 117 unmodified lines | ||
| 173 | 167 | } = {}): string => |
| 174 | 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 | 170 | const mergeSourceMaps = ( |
| 226 | − base: Record<string, LocalConfigSource> | undefined, | |
| 227 | − extra: Record<string, LocalConfigSource> | undefined, | |
| 228 | −): Record<string, LocalConfigSource> | undefined => { | |
| 171 | + base: Record<string, ExecutorScopeConfigSource> | undefined, | |
| 172 | + extra: Record<string, ExecutorScopeConfigSource> | undefined, | |
| 173 | +): Record<string, ExecutorScopeConfigSource> | undefined => { | |
| 229 | 174 | if (!base && !extra) { |
| 230 | 175 | return undefined; |
| 231 | 176 | } |
| @@ -236,9 +181,9 @@ const mergeSourceMaps = ( · 4 unmodified lines | ||
| 236 | 181 | }; |
| 237 | 182 | |
| 238 | 183 | const mergePolicyMaps = ( |
| 239 | − base: Record<string, LocalConfigPolicy> | undefined, | |
| 240 | − extra: Record<string, LocalConfigPolicy> | undefined, | |
| 241 | −): Record<string, LocalConfigPolicy> | undefined => { | |
| 184 | + base: Record<string, ExecutorScopeConfigPolicy> | undefined, | |
| 185 | + extra: Record<string, ExecutorScopeConfigPolicy> | undefined, | |
| 186 | +): Record<string, ExecutorScopeConfigPolicy> | undefined => { | |
| 242 | 187 | if (!base && !extra) { |
| 243 | 188 | return undefined; |
| 244 | 189 | } |
| @@ -248,15 +193,15 @@ const mergePolicyMaps = ( · 3 unmodified lines | ||
| 248 | 193 | }; |
| 249 | 194 | }; |
| 250 | 195 | |
| 251 | −export const mergeLocalExecutorConfigs = ( | |
| 252 | − base: LocalExecutorConfig | null, | |
| 253 | − extra: LocalExecutorConfig | null, | |
| 254 | −): LocalExecutorConfig | null => { | |
| 196 | +export const mergeExecutorScopeConfigs = ( | |
| 197 | + base: ExecutorScopeConfig | null, | |
| 198 | + extra: ExecutorScopeConfig | null, | |
| 199 | +): ExecutorScopeConfig | null => { | |
| 255 | 200 | if (!base && !extra) { |
| 256 | 201 | return null; |
| 257 | 202 | } |
| 258 | 203 | |
| 259 | − return decodeLocalExecutorConfig({ | |
| 204 | + return decodeExecutorScopeConfig({ | |
| 260 | 205 | runtime: extra?.runtime ?? base?.runtime, |
| 261 | 206 | workspace: { |
| 262 | 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 | 319 | path: string, |
| 375 | 320 | ): Effect.Effect< |
| 376 | − LocalExecutorConfig | null, | |
| 377 | − LocalFileSystemError | LocalExecutorConfigDecodeError, | |
| 321 | + ExecutorScopeConfig | null, | |
| 322 | + LocalFileSystemError | ExecutorScopeConfigDecodeError, | |
| 378 | 323 | FileSystem.FileSystem |
| 379 | 324 | > => |
| 380 | 325 | Effect.gen(function* () { |
| @@ -390,11 +335,11 @@ export const readOptionalLocalExecutorConfig = ( · 9 unmodified lines | ||
| 390 | 335 | Effect.mapError(mapFileSystemError(path, "read config")), |
| 391 | 336 | ); |
| 392 | 337 | return yield* Effect.try({ |
| 393 | − try: () => parseJsonc({ path, content }), | |
| 338 | + try: () => parseExecutorScopeConfig({ path, content }), | |
| 394 | 339 | catch: (cause) => |
| 395 | − cause instanceof LocalExecutorConfigDecodeError | |
| 340 | + cause instanceof ExecutorScopeConfigDecodeError | |
| 396 | 341 | ? cause |
| 397 | − : new LocalExecutorConfigDecodeError({ | |
| 342 | + : new ExecutorScopeConfigDecodeError({ | |
| 398 | 343 | message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 399 | 344 | path, |
| 400 | 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 | 351 | context: ResolvedLocalWorkspaceContext, |
| 407 | 352 | ): Effect.Effect< |
| 408 | 353 | FileLoadedExecutorConfig, |
| 409 | − LocalFileSystemError | LocalExecutorConfigDecodeError, | |
| 354 | + LocalFileSystemError | ExecutorScopeConfigDecodeError, | |
| 410 | 355 | FileSystem.FileSystem |
| 411 | 356 | > => |
| 412 | 357 | Effect.gen(function* () { |
| 413 | 358 | const [homeConfig, projectConfig] = yield* Effect.all([ |
| 414 | − readOptionalLocalExecutorConfig(context.homeConfigPath), | |
| 415 | − readOptionalLocalExecutorConfig(context.projectConfigPath), | |
| 359 | + readOptionalExecutorScopeConfig(context.homeConfigPath), | |
| 360 | + readOptionalExecutorScopeConfig(context.projectConfigPath), | |
| 416 | 361 | ]); |
| 417 | 362 | |
| 418 | 363 | return { |
| 419 | − config: mergeLocalExecutorConfigs(homeConfig, projectConfig), | |
| 364 | + config: mergeExecutorScopeConfigs(homeConfig, projectConfig), | |
| 420 | 365 | homeConfig, |
| 421 | 366 | projectConfig, |
| 422 | 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 | 373 | context: ResolvedLocalWorkspaceContext; |
| 429 | − config: LocalExecutorConfig; | |
| 374 | + config: ExecutorScopeConfig; | |
| 430 | 375 | }): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> => |
| 431 | 376 | Effect.gen(function* () { |
| 432 | 377 | const fs = yield* FileSystem.FileSystem; |
| @@ -435,7 +380,7 @@ export const writeProjectLocalExecutorConfig = (input: { · 2 unmodified lines | ||
| 435 | 380 | ); |
| 436 | 381 | yield* fs.writeFileString( |
| 437 | 382 | input.context.projectConfigPath, |
| 438 | − encodeLocalExecutorConfig(input.config), | |
| 383 | + encodeExecutorScopeConfig(input.config), | |
| 439 | 384 | ).pipe( |
| 440 | 385 | Effect.mapError(mapFileSystemError(input.context.projectConfigPath, "write config")), |
| 441 | 386 | ); |
| @@ -5,7 +5,7 @@ export { · 4 unmodified lines | ||
| 5 | 5 | createLocalExecutorRepositoriesEffect, |
| 6 | 6 | deriveLocalInstallation, |
| 7 | 7 | getOrProvisionLocalInstallation, |
| 8 | − loadLocalExecutorConfig, | |
| 8 | + loadExecutorScopeConfig, | |
| 9 | 9 | loadLocalExecutorStateSnapshot, |
| 10 | 10 | loadLocalInstallation, |
| 11 | 11 | loadLocalWorkspaceState, |
| @@ -20,9 +20,9 @@ export { · 8 unmodified lines | ||
| 20 | 20 | writeLocalExecutorStateSnapshot, |
| 21 | 21 | writeLocalSourceArtifact, |
| 22 | 22 | writeLocalWorkspaceState, |
| 23 | − writeProjectLocalExecutorConfig, | |
| 23 | + writeProjectExecutorScopeConfig, | |
| 24 | 24 | } from "./index"; |
| 25 | 25 | export type { |
| 26 | − LoadedLocalExecutorConfig, | |
| 26 | + LoadedExecutorScopeConfig, | |
| 27 | 27 | ResolvedLocalWorkspaceContext, |
| 28 | 28 | } from "./index"; |
| @@ -12,8 +12,8 @@ export class LocalFileSystemError extends Data.TaggedError( · 11 unmodified lines | ||
| 12 | 12 | readonly details: string; |
| 13 | 13 | }> {} |
| 14 | 14 | |
| 15 | −export class LocalExecutorConfigDecodeError extends Data.TaggedError( | |
| 16 | − "LocalExecutorConfigDecodeError", | |
| 15 | +export class ExecutorScopeConfigDecodeError extends Data.TaggedError( | |
| 16 | + "ExecutorScopeConfigDecodeError", | |
| 17 | 17 | )<{ |
| 18 | 18 | readonly message: string; |
| 19 | 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 | 1 | import { randomUUID } from "node:crypto"; |
| 2 | −import { dirname, join } from "node:path"; | |
| 2 | +import { dirname } from "node:path"; | |
| 3 | 3 | import { FileSystem } from "@effect/platform"; |
| 4 | 4 | import { NodeFileSystem } from "@effect/platform-node"; |
| 5 | 5 | |
| @@ -7,66 +7,32 @@ import { · 1 unmodified line | ||
| 7 | 7 | type Execution, |
| 8 | 8 | type ExecutionInteraction, |
| 9 | 9 | type ExecutionStep, |
| 10 | − ExecutionInteractionSchema, | |
| 11 | − ExecutionSchema, | |
| 12 | − ExecutionStepSchema, | |
| 13 | − SecretMaterialSchema, | |
| 14 | 10 | type SecretMaterial, |
| 15 | − SecretStoreSchema, | |
| 16 | 11 | type SecretStore, |
| 17 | 12 | } from "@executor/platform-sdk/schema"; |
| 18 | 13 | import * as Effect from "effect/Effect"; |
| 19 | 14 | import * as Option from "effect/Option"; |
| 20 | −import * as Schema from "effect/Schema"; | |
| 21 | 15 | |
| 22 | 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 | 26 | import { |
| 25 | 27 | LocalFileSystemError, |
| 26 | 28 | unknownLocalErrorDetails, |
| 27 | 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 | 31 | export type LocalExecutorStatePersistence = { |
| 52 | 32 | executorState: LocalExecutorStateStore; |
| 53 | 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 | 36 | const cloneValue = <T>(value: T): T => |
| 71 | 37 | JSON.parse(JSON.stringify(value)) as T; |
| 72 | 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 | 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 | 53 | const bindFileSystem = <A, E>( |
| 98 | 54 | fileSystem: FileSystem.FileSystem, |
| 99 | 55 | effect: Effect.Effect<A, E, FileSystem.FileSystem>, |
| @@ -125,10 +81,10 @@ const readStateFromDisk = ( · 25 unmodified lines | ||
| 125 | 81 | try: () => JSON.parse(content) as unknown, |
| 126 | 82 | catch: mapFileSystemError(path, "parse executor state"), |
| 127 | 83 | }); |
| 128 | − const decoded = yield* decodeLocalExecutorStateSnapshot(parsed).pipe( | |
| 129 | − Effect.mapError(mapFileSystemError(path, "decode executor state")), | |
| 130 | − ); | |
| 131 | − return decoded; | |
| 84 | + return yield* Effect.try({ | |
| 85 | + try: () => decodeLocalExecutorStateSnapshot(parsed), | |
| 86 | + catch: mapFileSystemError(path, "decode executor state"), | |
| 87 | + }); | |
| 132 | 88 | }); |
| 133 | 89 | |
| 134 | 90 | const writeStateToDisk = ( |
| @@ -143,7 +99,7 @@ const writeStateToDisk = ( · 8 unmodified lines | ||
| 143 | 99 | yield* fs.makeDirectory(dirname(path), { recursive: true }).pipe( |
| 144 | 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 | 103 | mode: 0o600, |
| 148 | 104 | }).pipe( |
| 149 | 105 | Effect.mapError(mapFileSystemError(tempPath, "write executor state")), |
| @@ -19,7 +19,7 @@ import type { · 18 unmodified lines | ||
| 19 | 19 | ExecutorSdkPluginExtensions, |
| 20 | 20 | } from "@executor/platform-sdk/plugins"; |
| 21 | 21 | import type { |
| 22 | − LocalExecutorConfig, | |
| 22 | + ExecutorScopeConfig, | |
| 23 | 23 | LocalInstallation, |
| 24 | 24 | } from "@executor/platform-sdk/schema"; |
| 25 | 25 | import { |
| @@ -30,14 +30,18 @@ import { · 4 unmodified lines | ||
| 30 | 30 | } from "@executor/platform-sdk/runtime"; |
| 31 | 31 | import * as Effect from "effect/Effect"; |
| 32 | 32 | import { |
| 33 | − type LoadedLocalExecutorConfig, | |
| 33 | + type LoadedExecutorScopeConfig, | |
| 34 | 34 | type ResolvedLocalWorkspaceContext, |
| 35 | − loadLocalExecutorConfig, | |
| 35 | + loadExecutorScopeConfig, | |
| 36 | 36 | resolveConfigRelativePath, |
| 37 | 37 | resolveLocalWorkspaceContext, |
| 38 | − writeProjectLocalExecutorConfig, | |
| 38 | + writeProjectExecutorScopeConfig, | |
| 39 | 39 | } from "./config"; |
| 40 | +import { | |
| 41 | + runStartupConfigMigrations, | |
| 42 | +} from "./config-migrations"; | |
| 40 | 43 | import { createLocalExecutorStatePersistence } from "./executor-state-store"; |
| 44 | +import { runStartupExecutorStateMigrations } from "./executor-state-migrations"; | |
| 41 | 45 | import { |
| 42 | 46 | createDefaultSecretMaterialDeleter, |
| 43 | 47 | createLocalInstanceConfigResolver, |
| @@ -114,18 +118,18 @@ const createBoundWorkspaceConfigStore = ( · 70 unmodified lines | ||
| 114 | 118 | context: ResolvedLocalWorkspaceContext, |
| 115 | 119 | fileSystem: FileSystem.FileSystem, |
| 116 | 120 | ): { |
| 117 | − load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>; | |
| 118 | − writeProject: (config: LocalExecutorConfig) => Effect.Effect<void, Error, never>; | |
| 121 | + load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>; | |
| 122 | + writeProject: (config: ExecutorScopeConfig) => Effect.Effect<void, Error, never>; | |
| 119 | 123 | resolveRelativePath: typeof resolveConfigRelativePath; |
| 120 | 124 | } => ({ |
| 121 | 125 | load: () => |
| 122 | − bindFileSystem(fileSystem, loadLocalExecutorConfig(context)).pipe( | |
| 126 | + bindFileSystem(fileSystem, loadExecutorScopeConfig(context)).pipe( | |
| 123 | 127 | Effect.mapError(toError), |
| 124 | 128 | ), |
| 125 | 129 | writeProject: (config) => |
| 126 | 130 | bindFileSystem( |
| 127 | 131 | fileSystem, |
| 128 | − writeProjectLocalExecutorConfig({ context, config }), | |
| 132 | + writeProjectExecutorScopeConfig({ context, config }), | |
| 129 | 133 | ).pipe(Effect.mapError(toError)), |
| 130 | 134 | resolveRelativePath: resolveConfigRelativePath, |
| 131 | 135 | }); |
| @@ -237,6 +241,14 @@ export const createLocalExecutorRepositoriesEffect = ( · 105 unmodified lines | ||
| 237 | 241 | homeStateDirectory: options.homeStateDirectory, |
| 238 | 242 | }), |
| 239 | 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 | 253 | const executorStateStorage = createLocalExecutorStatePersistence( |
| 242 | 254 | workspaceContext, |
| @@ -384,11 +396,19 @@ export { · 141 unmodified lines | ||
| 384 | 396 | writeLocalExecutorStateSnapshot, |
| 385 | 397 | } from "./executor-state-store"; |
| 386 | 398 | export { |
| 387 | − loadLocalExecutorConfig, | |
| 399 | + migrateLegacyLocalExecutorState, | |
| 400 | + runStartupExecutorStateMigrations, | |
| 401 | +} from "./executor-state-migrations"; | |
| 402 | +export { | |
| 403 | + loadExecutorScopeConfig, | |
| 388 | 404 | resolveConfigRelativePath, |
| 389 | 405 | resolveLocalWorkspaceContext, |
| 390 | − writeProjectLocalExecutorConfig, | |
| 406 | + writeProjectExecutorScopeConfig, | |
| 391 | 407 | } from "./config"; |
| 408 | +export { | |
| 409 | + migrateLegacyExecutorScopeConfigs, | |
| 410 | + runStartupConfigMigrations, | |
| 411 | +} from "./config-migrations"; | |
| 392 | 412 | export { |
| 393 | 413 | loadLocalWorkspaceState, |
| 394 | 414 | writeLocalWorkspaceState, |
| @@ -406,6 +426,6 @@ export { · 11 unmodified lines | ||
| 406 | 426 | syncWorkspaceSourceTypeDeclarationsNode, |
| 407 | 427 | } from "./source-type-declarations"; |
| 408 | 428 | export type { |
| 409 | − LoadedLocalExecutorConfig, | |
| 429 | + LoadedExecutorScopeConfig, | |
| 410 | 430 | ResolvedLocalWorkspaceContext, |
| 411 | 431 | } from "./config"; |
| @@ -8,8 +8,8 @@ import type { · 7 unmodified lines | ||
| 8 | 8 | import type { ExecutorEffect } from "./executor-effect"; |
| 9 | 9 | import type { ExecutorScopeContext } from "./scope"; |
| 10 | 10 | import { |
| 11 | − type LocalConfigSource, | |
| 12 | − type LocalExecutorConfig, | |
| 11 | + type ExecutorScopeConfigSource, | |
| 12 | + type ExecutorScopeConfig, | |
| 13 | 13 | type Source as ExecutorSource, |
| 14 | 14 | type SecretMaterial, |
| 15 | 15 | type SecretMaterialPurpose, |
| @@ -300,15 +300,15 @@ export type ExecutorSourcePluginDefinition< · 284 unmodified lines | ||
| 300 | 300 | stored: TStored | null; |
| 301 | 301 | }) => Effect.Effect<void, Error, any>; |
| 302 | 302 | }; |
| 303 | − localConfig?: { | |
| 303 | + scopeConfig?: { | |
| 304 | 304 | toConfigSource: (input: { |
| 305 | 305 | source: ExecutorSource; |
| 306 | 306 | stored: TStored; |
| 307 | − }) => LocalConfigSource; | |
| 307 | + }) => ExecutorScopeConfigSource; | |
| 308 | 308 | recoverStored: (input: { |
| 309 | 309 | source: ExecutorSource; |
| 310 | − config: LocalConfigSource; | |
| 311 | − loadedConfig: LocalExecutorConfig | null; | |
| 310 | + config: ExecutorScopeConfigSource; | |
| 311 | + loadedConfig: ExecutorScopeConfig | null; | |
| 312 | 312 | }) => Effect.Effect<TStored, Error, any> | TStored; |
| 313 | 313 | }; |
| 314 | 314 | catalog: { |
| @@ -588,7 +588,7 @@ const fromMaybeEffect = <A>( · 273 unmodified lines | ||
| 588 | 588 | ? value |
| 589 | 589 | : Effect.succeed(value); |
| 590 | 590 | |
| 591 | −const persistSourceLocalConfig = < | |
| 591 | +const persistSourceScopeConfig = < | |
| 592 | 592 | TAddInput, |
| 593 | 593 | TConnectInput, |
| 594 | 594 | TSourceConfig, |
| @@ -610,15 +610,15 @@ const persistSourceLocalConfig = < · 15 unmodified lines | ||
| 610 | 610 | stored: TStored; |
| 611 | 611 | }, |
| 612 | 612 | ): Effect.Effect<void, Error, ScopeConfigStore> => |
| 613 | − definition.localConfig | |
| 613 | + definition.scopeConfig | |
| 614 | 614 | ? Effect.gen(function* () { |
| 615 | − const localConfig = definition.localConfig!; | |
| 615 | + const scopeConfig = definition.scopeConfig!; | |
| 616 | 616 | const scopeConfigStore = yield* ScopeConfigStore; |
| 617 | 617 | const loadedConfig = yield* scopeConfigStore.load(); |
| 618 | 618 | const projectConfig = cloneJson(loadedConfig.projectConfig ?? {}); |
| 619 | 619 | const sources = { |
| 620 | 620 | ...projectConfig.sources, |
| 621 | − [input.source.id]: localConfig.toConfigSource({ | |
| 621 | + [input.source.id]: scopeConfig.toConfigSource({ | |
| 622 | 622 | source: input.source, |
| 623 | 623 | stored: input.stored, |
| 624 | 624 | }), |
| @@ -653,12 +653,12 @@ const loadStoredOrRecover = < · 28 unmodified lines | ||
| 653 | 653 | source: ExecutorSource, |
| 654 | 654 | ): Effect.Effect<TStored | null, Error, ScopeConfigStore> => |
| 655 | 655 | Effect.gen(function* () { |
| 656 | − const localConfig = definition.localConfig; | |
| 656 | + const scopeConfig = definition.scopeConfig; | |
| 657 | 657 | const stored = yield* definition.storage.get({ |
| 658 | 658 | scopeId: source.scopeId, |
| 659 | 659 | sourceId: source.id, |
| 660 | 660 | }); |
| 661 | − if (stored !== null || localConfig === undefined) { | |
| 661 | + if (stored !== null || scopeConfig === undefined) { | |
| 662 | 662 | return stored; |
| 663 | 663 | } |
| 664 | 664 | |
| @@ -677,7 +677,7 @@ const loadStoredOrRecover = < · 12 unmodified lines | ||
| 677 | 677 | } |
| 678 | 678 | |
| 679 | 679 | const recovered = yield* fromMaybeEffect( |
| 680 | − localConfig.recoverStored({ | |
| 680 | + scopeConfig.recoverStored({ | |
| 681 | 681 | source, |
| 682 | 682 | config, |
| 683 | 683 | loadedConfig: loadedConfig.config, |
| @@ -748,7 +748,7 @@ const createExecutorSourcePluginApi = < · 64 unmodified lines | ||
| 748 | 748 | sourceId: source.id, |
| 749 | 749 | value: created.stored, |
| 750 | 750 | }); |
| 751 | − yield* persistSourceLocalConfig(definition, { | |
| 751 | + yield* persistSourceScopeConfig(definition, { | |
| 752 | 752 | source, |
| 753 | 753 | stored: created.stored, |
| 754 | 754 | }); |
| @@ -772,7 +772,7 @@ const createExecutorSourcePluginApi = < · 17 unmodified lines | ||
| 772 | 772 | sourceId: saved.id, |
| 773 | 773 | value: updated.stored, |
| 774 | 774 | }); |
| 775 | − yield* persistSourceLocalConfig(definition, { | |
| 775 | + yield* persistSourceScopeConfig(definition, { | |
| 776 | 776 | source: saved, |
| 777 | 777 | stored: updated.stored, |
| 778 | 778 | }); |
| @@ -8,7 +8,7 @@ import type { · 7 unmodified lines | ||
| 8 | 8 | } from "./contracts"; |
| 9 | 9 | import { |
| 10 | 10 | PolicyIdSchema, |
| 11 | − type LocalExecutorConfig, | |
| 11 | + type ExecutorScopeConfig, | |
| 12 | 12 | type LocalScopePolicy, |
| 13 | 13 | type PolicyId, |
| 14 | 14 | type ScopeId, |
| @@ -64,7 +64,7 @@ const toLocalScopePolicy = (input: { · 49 unmodified lines | ||
| 64 | 64 | scopeId: ScopeId; |
| 65 | 65 | scopeStableKey: string; |
| 66 | 66 | key: string; |
| 67 | − policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string]; | |
| 67 | + policyConfig: NonNullable<ExecutorScopeConfig["policies"]>[string]; | |
| 68 | 68 | state: LocalScopeState["policies"][string] | undefined; |
| 69 | 69 | }): LocalScopePolicy => ({ |
| 70 | 70 | id: |
| @@ -119,7 +119,7 @@ const writeLocalPolicyFiles = (input: { · 48 unmodified lines | ||
| 119 | 119 | operation: OperationErrors; |
| 120 | 120 | scopeConfigStore: ScopeConfigStoreShape; |
| 121 | 121 | scopeStateStore: ScopeStateStoreShape; |
| 122 | − projectConfig: LocalExecutorConfig; | |
| 122 | + projectConfig: ExecutorScopeConfig; | |
| 123 | 123 | scopeState: LocalScopeState; |
| 124 | 124 | }) => |
| 125 | 125 | Effect.all( |
| @@ -11,18 +11,18 @@ import { · 10 unmodified lines | ||
| 11 | 11 | makeSesExecutor, |
| 12 | 12 | } from "@executor/runtime-ses"; |
| 13 | 13 | import type { |
| 14 | − LocalExecutorConfig, | |
| 15 | − LocalExecutorRuntime, | |
| 14 | + ExecutorScopeConfig, | |
| 15 | + ExecutorRuntimeConfig, | |
| 16 | 16 | } from "#schema"; |
| 17 | 17 | |
| 18 | −const DEFAULT_EXECUTION_RUNTIME: LocalExecutorRuntime = "quickjs"; | |
| 18 | +const DEFAULT_EXECUTION_RUNTIME: ExecutorRuntimeConfig = "quickjs"; | |
| 19 | 19 | |
| 20 | 20 | export const resolveConfiguredExecutionRuntime = ( |
| 21 | − config: LocalExecutorConfig | null | undefined, | |
| 22 | −): LocalExecutorRuntime => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME; | |
| 21 | + config: ExecutorScopeConfig | null | undefined, | |
| 22 | +): ExecutorRuntimeConfig => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME; | |
| 23 | 23 | |
| 24 | 24 | export const createCodeExecutorForRuntime = ( |
| 25 | − runtime: LocalExecutorRuntime, | |
| 25 | + runtime: ExecutorRuntimeConfig, | |
| 26 | 26 | ): CodeExecutor => { |
| 27 | 27 | switch (runtime) { |
| 28 | 28 | case "deno": |
| @@ -32,10 +32,10 @@ import { · 31 unmodified lines | ||
| 32 | 32 | RuntimeExecutionResolverLive, |
| 33 | 33 | } from "./execution/scope/environment"; |
| 34 | 34 | import type { |
| 35 | − LoadedLocalExecutorConfig, | |
| 35 | + LoadedExecutorScopeConfig, | |
| 36 | 36 | } from "./scope-config"; |
| 37 | 37 | import type { |
| 38 | − LocalExecutorConfig, | |
| 38 | + ExecutorScopeConfig, | |
| 39 | 39 | } from "#schema"; |
| 40 | 40 | import type { |
| 41 | 41 | InstanceConfig, |
| @@ -113,7 +113,7 @@ export * from "./source-artifacts"; · 71 unmodified lines | ||
| 113 | 113 | export * from "./scope-config"; |
| 114 | 114 | export * from "./scope-state"; |
| 115 | 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 | 117 | export { |
| 118 | 118 | LocalInstanceConfigService, |
| 119 | 119 | SecretMaterialDeleterService, |
| @@ -184,9 +184,9 @@ export type BoundInstallationStore = { · 64 unmodified lines | ||
| 184 | 184 | }; |
| 185 | 185 | |
| 186 | 186 | export type BoundScopeConfigStore = { |
| 187 | − load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>; | |
| 187 | + load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>; | |
| 188 | 188 | writeProject: ( |
| 189 | − config: LocalExecutorConfig, | |
| 189 | + config: ExecutorScopeConfig, | |
| 190 | 190 | ) => Effect.Effect<void, Error, never>; |
| 191 | 191 | resolveRelativePath: ScopeConfigStoreShape["resolveRelativePath"]; |
| 192 | 192 | }; |
| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | import type { |
| 2 | − LocalExecutorConfig, | |
| 2 | + ExecutorScopeConfig, | |
| 3 | 3 | } from "#schema"; |
| 4 | 4 | |
| 5 | 5 | export type LoadedExecutorConfig = { |
| 6 | − config: LocalExecutorConfig | null; | |
| 7 | − homeConfig: LocalExecutorConfig | null; | |
| 8 | − projectConfig: LocalExecutorConfig | null; | |
| 6 | + config: ExecutorScopeConfig | null; | |
| 7 | + homeConfig: ExecutorScopeConfig | 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 | 10 | ExecutorScopeContext, |
| 11 | 11 | } from "../../scope"; |
| 12 | 12 | import type { |
| 13 | − LoadedLocalExecutorConfig, | |
| 13 | + LoadedExecutorScopeConfig, | |
| 14 | 14 | } from "../scope-config"; |
| 15 | 15 | import { |
| 16 | 16 | RuntimeLocalScopeMismatchError, |
| @@ -24,7 +24,7 @@ export type RuntimeLocalScopeState = { · 7 unmodified lines | ||
| 24 | 24 | actorScopeId: ScopeId; |
| 25 | 25 | resolutionScopeIds: ReadonlyArray<ScopeId>; |
| 26 | 26 | }; |
| 27 | − loadedConfig: LoadedLocalExecutorConfig; | |
| 27 | + loadedConfig: LoadedExecutorScopeConfig; | |
| 28 | 28 | }; |
| 29 | 29 | |
| 30 | 30 | export class RuntimeLocalScopeService extends Context.Tag( |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import * as Effect from "effect/Effect"; |
| 2 | 2 | |
| 3 | 3 | import { |
| 4 | − type LoadedLocalExecutorConfig, | |
| 4 | + type LoadedExecutorScopeConfig, | |
| 5 | 5 | } from "../scope-config"; |
| 6 | 6 | import { |
| 7 | 7 | ScopeStateStore, |
| @@ -40,7 +40,7 @@ export const derivePolicyConfigKey = ( · 32 unmodified lines | ||
| 40 | 40 | }; |
| 41 | 41 | |
| 42 | 42 | const pruneLocalWorkspaceState = (input: { |
| 43 | − loadedConfig: LoadedLocalExecutorConfig; | |
| 43 | + loadedConfig: LoadedExecutorScopeConfig; | |
| 44 | 44 | }): Effect.Effect<LocalScopeState, Error, ScopeStateStore> => |
| 45 | 45 | Effect.gen(function* () { |
| 46 | 46 | const scopeStateStore = yield* ScopeStateStore; |
| @@ -79,8 +79,8 @@ const pruneLocalWorkspaceState = (input: { · 32 unmodified lines | ||
| 79 | 79 | }); |
| 80 | 80 | |
| 81 | 81 | export const synchronizeLocalScopeState = (input: { |
| 82 | − loadedConfig: LoadedLocalExecutorConfig; | |
| 83 | −}): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, ScopeStateStore> => | |
| 82 | + loadedConfig: LoadedExecutorScopeConfig; | |
| 83 | +}): Effect.Effect<LoadedExecutorScopeConfig["config"], Error, ScopeStateStore> => | |
| 84 | 84 | Effect.gen(function* () { |
| 85 | 85 | yield* pruneLocalWorkspaceState({ |
| 86 | 86 | loadedConfig: input.loadedConfig, |
| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | import type { |
| 2 | 2 | LocalInstallation, |
| 3 | − LocalExecutorConfig, | |
| 3 | + ExecutorScopeConfig, | |
| 4 | 4 | } from "#schema"; |
| 5 | 5 | import * as Context from "effect/Context"; |
| 6 | 6 | import * as Layer from "effect/Layer"; |
| 7 | 7 | |
| 8 | 8 | import type { |
| 9 | − LoadedLocalExecutorConfig, | |
| 9 | + LoadedExecutorScopeConfig, | |
| 10 | 10 | } from "../scope-config"; |
| 11 | 11 | import type { |
| 12 | 12 | LocalSourceArtifact, |
| @@ -31,9 +31,9 @@ export class InstallationStore extends Context.Tag( · 18 unmodified lines | ||
| 31 | 31 | )<InstallationStore, InstallationStoreShape>() {} |
| 32 | 32 | |
| 33 | 33 | export type ScopeConfigStoreShape = { |
| 34 | − load: () => import("effect/Effect").Effect<LoadedLocalExecutorConfig, Error, never>; | |
| 34 | + load: () => import("effect/Effect").Effect<LoadedExecutorScopeConfig, Error, never>; | |
| 35 | 35 | writeProject: (input: { |
| 36 | − config: LocalExecutorConfig; | |
| 36 | + config: ExecutorScopeConfig; | |
| 37 | 37 | }) => import("effect/Effect").Effect<void, Error, never>; |
| 38 | 38 | resolveRelativePath: (input: { path: string; scopeRoot: string }) => string; |
| 39 | 39 | }; |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import type { |
| 2 | − LocalConfigSource, | |
| 2 | + ExecutorScopeConfigSource, | |
| 3 | 3 | Source, |
| 4 | 4 | SourceId, |
| 5 | 5 | } from "#schema"; |
| @@ -23,7 +23,7 @@ export const trimOrNull = (value: string | null | undefined): string | null => { · 17 unmodified lines | ||
| 23 | 23 | export const cloneJson = <T>(value: T): T => |
| 24 | 24 | JSON.parse(JSON.stringify(value)) as T; |
| 25 | 25 | |
| 26 | −export const deriveLocalSourceId = ( | |
| 26 | +export const deriveScopeConfigSourceId = ( | |
| 27 | 27 | source: Pick<Source, "namespace" | "name">, |
| 28 | 28 | used: ReadonlySet<string>, |
| 29 | 29 | ): SourceId => { |
| @@ -38,9 +38,9 @@ export const deriveLocalSourceId = ( · 8 unmodified lines | ||
| 38 | 38 | return SourceIdSchema.make(candidate); |
| 39 | 39 | }; |
| 40 | 40 | |
| 41 | −export const configSourceBaseFromLocalSource = (input: { | |
| 41 | +export const scopeConfigSourceBaseFromSource = (input: { | |
| 42 | 42 | source: Source; |
| 43 | −}): Omit<LocalConfigSource, "kind" | "config" | "connection" | "binding"> => ({ | |
| 43 | +}): Omit<ExecutorScopeConfigSource, "kind" | "config"> => ({ | |
| 44 | 44 | ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id) |
| 45 | 45 | ? { name: input.source.name } |
| 46 | 46 | : {}), |
| @@ -50,29 +50,19 @@ export const configSourceBaseFromLocalSource = (input: { · 3 unmodified lines | ||
| 50 | 50 | ...(input.source.enabled === false ? { enabled: false } : {}), |
| 51 | 51 | }); |
| 52 | 52 | |
| 53 | −export const configSourceFromLocalSource = (input: { | |
| 53 | +export const scopeConfigSourceFromSource = (input: { | |
| 54 | 54 | source: Source; |
| 55 | − existingConfig?: LocalConfigSource | null; | |
| 56 | −}): LocalConfigSource => { | |
| 55 | + existingConfig?: ExecutorScopeConfigSource | null; | |
| 56 | +}): ExecutorScopeConfigSource => { | |
| 57 | 57 | return { |
| 58 | − ...configSourceBaseFromLocalSource({ | |
| 58 | + ...scopeConfigSourceBaseFromSource({ | |
| 59 | 59 | source: input.source, |
| 60 | 60 | }), |
| 61 | − kind: input.source.kind as LocalConfigSource["kind"], | |
| 61 | + kind: input.source.kind as ExecutorScopeConfigSource["kind"], | |
| 62 | 62 | ...(input.existingConfig?.config !== undefined |
| 63 | 63 | ? { |
| 64 | 64 | config: cloneJson(input.existingConfig.config), |
| 65 | 65 | } |
| 66 | 66 | : {}), |
| 67 | − ...(input.existingConfig?.connection !== undefined | |
| 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; | |
| 67 | + } as ExecutorScopeConfigSource; | |
| 78 | 68 | }; |
| @@ -4,7 +4,7 @@ import type { · 3 unmodified lines | ||
| 4 | 4 | import * as Effect from "effect/Effect"; |
| 5 | 5 | |
| 6 | 6 | import type { |
| 7 | − LoadedLocalExecutorConfig, | |
| 7 | + LoadedExecutorScopeConfig, | |
| 8 | 8 | } from "../../scope-config"; |
| 9 | 9 | import { |
| 10 | 10 | SourceTypeDeclarationsRefresherService, |
| @@ -53,7 +53,7 @@ export type ResolvedSourceStoreScope = { · 42 unmodified lines | ||
| 53 | 53 | scopeConfigStore: ScopeConfigStoreShape; |
| 54 | 54 | scopeStateStore: ScopeStateStoreShape; |
| 55 | 55 | sourceArtifactStore: SourceArtifactStoreShape; |
| 56 | − loadedConfig: LoadedLocalExecutorConfig; | |
| 56 | + loadedConfig: LoadedExecutorScopeConfig; | |
| 57 | 57 | scopeState: LocalScopeState; |
| 58 | 58 | }; |
| 59 | 59 | |
| @@ -6,9 +6,9 @@ import * as Effect from "effect/Effect"; · 5 unmodified lines | ||
| 6 | 6 | |
| 7 | 7 | import type { LocalScopeState } from "../../scope-state"; |
| 8 | 8 | import { |
| 9 | − configSourceFromLocalSource, | |
| 9 | + scopeConfigSourceFromSource, | |
| 10 | 10 | cloneJson, |
| 11 | − deriveLocalSourceId, | |
| 11 | + deriveScopeConfigSourceId, | |
| 12 | 12 | } from "./config"; |
| 13 | 13 | import { |
| 14 | 14 | type RuntimeSourceStoreDeps, |
| @@ -84,7 +84,7 @@ export const persistSourceWithDeps = ( · 69 unmodified lines | ||
| 84 | 84 | localScope.loadedConfig.config?.sources?.[source.id] || |
| 85 | 85 | localScope.scopeState.sources[source.id] |
| 86 | 86 | ? source.id |
| 87 | − : deriveLocalSourceId( | |
| 87 | + : deriveScopeConfigSourceId( | |
| 88 | 88 | source, |
| 89 | 89 | new Set(Object.keys(localScope.loadedConfig.config?.sources ?? {})), |
| 90 | 90 | ), |
| @@ -94,7 +94,7 @@ export const persistSourceWithDeps = ( · 3 unmodified lines | ||
| 94 | 94 | const sources = { |
| 95 | 95 | ...projectConfig.sources, |
| 96 | 96 | }; |
| 97 | − sources[nextSource.id] = configSourceFromLocalSource({ | |
| 97 | + sources[nextSource.id] = scopeConfigSourceFromSource({ | |
| 98 | 98 | source: nextSource, |
| 99 | 99 | existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null, |
| 100 | 100 | }); |
| @@ -1,14 +1,14 @@ | ||
| 1 | 1 | import type { |
| 2 | − LocalConfigSource, | |
| 2 | + ExecutorScopeConfigSource, | |
| 3 | 3 | Source, |
| 4 | 4 | } from "#schema"; |
| 5 | 5 | import * as Schema from "effect/Schema"; |
| 6 | 6 | import { |
| 7 | 7 | cloneJson, |
| 8 | − configSourceBaseFromLocalSource, | |
| 8 | + scopeConfigSourceBaseFromSource, | |
| 9 | 9 | } from "./config"; |
| 10 | 10 | |
| 11 | −export const createPluginLocalConfigEntrySchema = < | |
| 11 | +export const createPluginScopeConfigEntrySchema = < | |
| 12 | 12 | TKind extends string, |
| 13 | 13 | TConfig, |
| 14 | 14 | >( |
| @@ -23,17 +23,15 @@ export const createPluginLocalConfigEntrySchema = < · 8 unmodified lines | ||
| 23 | 23 | namespace: Schema.optional(Schema.String), |
| 24 | 24 | enabled: Schema.optional(Schema.Boolean), |
| 25 | 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 | 29 | source: Source; |
| 32 | 30 | config: TConfig; |
| 33 | −}): LocalConfigSource => ({ | |
| 34 | − ...configSourceBaseFromLocalSource({ | |
| 31 | +}): ExecutorScopeConfigSource => ({ | |
| 32 | + ...scopeConfigSourceBaseFromSource({ | |
| 35 | 33 | source: input.source, |
| 36 | 34 | }), |
| 37 | − kind: input.source.kind as LocalConfigSource["kind"], | |
| 35 | + kind: input.source.kind as ExecutorScopeConfigSource["kind"], | |
| 38 | 36 | config: cloneJson(input.config), |
| 39 | 37 | }); |
| @@ -6,7 +6,7 @@ import type { · 5 unmodified lines | ||
| 6 | 6 | import { SourceIdSchema } from "#schema"; |
| 7 | 7 | import * as Effect from "effect/Effect"; |
| 8 | 8 | |
| 9 | −import type { LoadedLocalExecutorConfig } from "../../scope-config"; | |
| 9 | +import type { LoadedExecutorScopeConfig } from "../../scope-config"; | |
| 10 | 10 | import { |
| 11 | 11 | LocalConfiguredSourceNotFoundError, |
| 12 | 12 | RuntimeLocalScopeMismatchError, |
| @@ -21,7 +21,7 @@ import { trimOrNull } from "./config"; · 8 unmodified lines | ||
| 21 | 21 | |
| 22 | 22 | export const buildLocalSourceRecord = (input: { |
| 23 | 23 | scopeId: ScopeId; |
| 24 | − loadedConfig: LoadedLocalExecutorConfig; | |
| 24 | + loadedConfig: LoadedExecutorScopeConfig; | |
| 25 | 25 | scopeState: LocalScopeState; |
| 26 | 26 | sourceId: SourceId; |
| 27 | 27 | }): Effect.Effect< |
| @@ -9,7 +9,7 @@ export * from "./models/secret-material"; · 8 unmodified lines | ||
| 9 | 9 | export * from "./models/secret-store"; |
| 10 | 10 | export * from "./models/policy"; |
| 11 | 11 | export * from "./models/local-installation"; |
| 12 | −export * from "./models/local-config"; | |
| 12 | +export * from "./models/scope-config"; | |
| 13 | 13 | export * from "./models/execution"; |
| 14 | 14 | export * from "./models/source-inspection"; |
| 15 | 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 | 34 | getOrProvisionLocalInstallation, |
| 35 | 35 | resolveLocalWorkspaceContext, |
| 36 | 36 | writeLocalSourceArtifact, |
| 37 | − writeProjectLocalExecutorConfig, | |
| 37 | + writeProjectExecutorScopeConfig, | |
| 38 | 38 | } from "@executor/platform-sdk-file/effect"; |
| 39 | 39 | import { |
| 40 | − type LocalConfigSource, | |
| 40 | + type ExecutorScopeConfigSource, | |
| 41 | 41 | SourceCatalogIdSchema, |
| 42 | 42 | SourceCatalogRevisionIdSchema, |
| 43 | 43 | SourceIdSchema, |
| @@ -136,7 +136,7 @@ const writeConfiguredLocalMcpSource = (input: { · 92 unmodified lines | ||
| 136 | 136 | }); |
| 137 | 137 | const installation = yield* getOrProvisionLocalInstallation({ context }); |
| 138 | 138 | |
| 139 | − yield* writeProjectLocalExecutorConfig({ | |
| 139 | + yield* writeProjectExecutorScopeConfig({ | |
| 140 | 140 | context, |
| 141 | 141 | config: { |
| 142 | 142 | sources: { |
| @@ -274,7 +274,7 @@ const writeConfiguredLocalMcpSource = (input: { · 131 unmodified lines | ||
| 274 | 274 | |
| 275 | 275 | const writeConfiguredWorkspaceSources = (input: { |
| 276 | 276 | workspaceRoot: string; |
| 277 | − sources: Record<string, LocalConfigSource>; | |
| 277 | + sources: Record<string, ExecutorScopeConfigSource>; | |
| 278 | 278 | }) => |
| 279 | 279 | Effect.gen(function* () { |
| 280 | 280 | const context = yield* resolveLocalWorkspaceContext({ |
| @@ -282,7 +282,7 @@ const writeConfiguredWorkspaceSources = (input: { · 1 unmodified line | ||
| 282 | 282 | }); |
| 283 | 283 | const installation = yield* getOrProvisionLocalInstallation({ context }); |
| 284 | 284 | |
| 285 | − yield* writeProjectLocalExecutorConfig({ | |
| 285 | + yield* writeProjectExecutorScopeConfig({ | |
| 286 | 286 | context, |
| 287 | 287 | config: { |
| 288 | 288 | sources: input.sources, |
| @@ -7,7 +7,7 @@ import { · 6 unmodified lines | ||
| 7 | 7 | createSourceCatalogSyncResult, |
| 8 | 8 | } from "@executor/source-core"; |
| 9 | 9 | import type { |
| 10 | − LocalExecutorConfig, | |
| 10 | + ExecutorScopeConfig, | |
| 11 | 11 | Source, |
| 12 | 12 | } from "@executor/platform-sdk/schema"; |
| 13 | 13 | import { |
| @@ -17,8 +17,8 @@ import { · 3 unmodified lines | ||
| 17 | 17 | defineExecutorSourcePlugin, |
| 18 | 18 | } from "@executor/platform-sdk/plugins"; |
| 19 | 19 | import { |
| 20 | − createPluginLocalConfigEntrySchema, | |
| 21 | − pluginLocalConfigSourceFromConfig, | |
| 20 | + createPluginScopeConfigEntrySchema, | |
| 21 | + pluginScopeConfigSourceFromConfig, | |
| 22 | 22 | SecretMaterialDeleterService, |
| 23 | 23 | SecretMaterialResolverService, |
| 24 | 24 | SecretMaterialStorerService, |
| @@ -97,7 +97,7 @@ const decodeBatchSourceInput = Schema.decodeUnknownSync( · 72 unmodified lines | ||
| 97 | 97 | const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema); |
| 98 | 98 | const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema); |
| 99 | 99 | const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 100 | − createPluginLocalConfigEntrySchema({ | |
| 100 | + createPluginScopeConfigEntrySchema({ | |
| 101 | 101 | kind: GOOGLE_DISCOVERY_SOURCE_KIND, |
| 102 | 102 | config: GoogleDiscoveryStoredSourceDataSchema, |
| 103 | 103 | }), |
| @@ -563,7 +563,7 @@ const googleDiscoveryConnectInputFromAddInput = ( · 459 unmodified lines | ||
| 563 | 563 | |
| 564 | 564 | const googleDiscoveryStoredSourceDataFromLocalConfig = (input: { |
| 565 | 565 | config: unknown; |
| 566 | − loadedConfig: LocalExecutorConfig | null; | |
| 566 | + loadedConfig: ExecutorScopeConfig | null; | |
| 567 | 567 | }): GoogleDiscoveryStoredSourceData => { |
| 568 | 568 | const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config); |
| 569 | 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 | 633 | toConfigSource: ({ source, stored }) => |
| 634 | − pluginLocalConfigSourceFromConfig({ | |
| 634 | + pluginScopeConfigSourceFromConfig({ | |
| 635 | 635 | source, |
| 636 | 636 | config: stored, |
| 637 | 637 | }), |
| @@ -12,7 +12,7 @@ import { · 11 unmodified lines | ||
| 12 | 12 | createSourceCatalogSyncResult, |
| 13 | 13 | } from "@executor/source-core"; |
| 14 | 14 | import type { |
| 15 | − LocalExecutorConfig, | |
| 15 | + ExecutorScopeConfig, | |
| 16 | 16 | Source, |
| 17 | 17 | } from "@executor/platform-sdk/schema"; |
| 18 | 18 | import { |
| @@ -23,8 +23,8 @@ import { · 4 unmodified lines | ||
| 23 | 23 | } from "@executor/platform-sdk/plugins"; |
| 24 | 24 | import { |
| 25 | 25 | SecretMaterialResolverService, |
| 26 | − createPluginLocalConfigEntrySchema, | |
| 27 | − pluginLocalConfigSourceFromConfig, | |
| 26 | + createPluginScopeConfigEntrySchema, | |
| 27 | + pluginScopeConfigSourceFromConfig, | |
| 28 | 28 | provideExecutorRuntime, |
| 29 | 29 | runtimeEffectError, |
| 30 | 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 | 153 | const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 154 | − createPluginLocalConfigEntrySchema({ | |
| 154 | + createPluginScopeConfigEntrySchema({ | |
| 155 | 155 | kind: "graphql", |
| 156 | 156 | config: GraphqlStoredSourceDataSchema, |
| 157 | 157 | }), |
| @@ -336,7 +336,7 @@ const graphqlConnectInputFromAddInput = ( · 178 unmodified lines | ||
| 336 | 336 | |
| 337 | 337 | const graphqlStoredSourceDataFromLocalConfig = (input: { |
| 338 | 338 | config: unknown; |
| 339 | − loadedConfig: LocalExecutorConfig | null; | |
| 339 | + loadedConfig: ExecutorScopeConfig | null; | |
| 340 | 340 | }): GraphqlStoredSourceData => { |
| 341 | 341 | const current = decodeGraphqlCurrentLocalConfigOption(input.config); |
| 342 | 342 | if (Option.isSome(current)) { |
| @@ -397,9 +397,9 @@ export const graphqlSdkPlugin = (options: { · 54 unmodified lines | ||
| 397 | 397 | toConfig: ({ source, stored }) => |
| 398 | 398 | sourceConfigFromStored(source, normalizeStoredSourceData(stored)), |
| 399 | 399 | }, |
| 400 | − localConfig: { | |
| 400 | + scopeConfig: { | |
| 401 | 401 | toConfigSource: ({ source, stored }) => |
| 402 | − pluginLocalConfigSourceFromConfig({ | |
| 402 | + pluginScopeConfigSourceFromConfig({ | |
| 403 | 403 | source, |
| 404 | 404 | config: normalizeStoredSourceData(stored), |
| 405 | 405 | }), |
| @@ -31,8 +31,8 @@ import { · 30 unmodified lines | ||
| 31 | 31 | defineExecutorSourcePlugin, |
| 32 | 32 | } from "@executor/platform-sdk/plugins"; |
| 33 | 33 | import { |
| 34 | − createPluginLocalConfigEntrySchema, | |
| 35 | − pluginLocalConfigSourceFromConfig, | |
| 34 | + createPluginScopeConfigEntrySchema, | |
| 35 | + pluginScopeConfigSourceFromConfig, | |
| 36 | 36 | SecretMaterialDeleterService, |
| 37 | 37 | SecretMaterialResolverService, |
| 38 | 38 | SecretMaterialStorerService, |
| @@ -250,7 +250,7 @@ const normalizeStringArray = ( · 211 unmodified lines | ||
| 250 | 250 | }; |
| 251 | 251 | |
| 252 | 252 | const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 253 | − createPluginLocalConfigEntrySchema({ | |
| 253 | + createPluginScopeConfigEntrySchema({ | |
| 254 | 254 | kind: "mcp", |
| 255 | 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 | 724 | toConfigSource: ({ source, stored }) => |
| 725 | − pluginLocalConfigSourceFromConfig({ | |
| 725 | + pluginScopeConfigSourceFromConfig({ | |
| 726 | 726 | source, |
| 727 | 727 | config: stored, |
| 728 | 728 | }), |
| @@ -8,7 +8,7 @@ import { · 7 unmodified lines | ||
| 8 | 8 | createSourceCatalogSyncResult, |
| 9 | 9 | } from "@executor/source-core"; |
| 10 | 10 | import type { |
| 11 | − LocalExecutorConfig, | |
| 11 | + ExecutorScopeConfig, | |
| 12 | 12 | Source, |
| 13 | 13 | } from "@executor/platform-sdk/schema"; |
| 14 | 14 | import { |
| @@ -18,9 +18,9 @@ import { · 3 unmodified lines | ||
| 18 | 18 | defineExecutorSourcePlugin, |
| 19 | 19 | } from "@executor/platform-sdk/plugins"; |
| 20 | 20 | import { |
| 21 | − createPluginLocalConfigEntrySchema, | |
| 21 | + createPluginScopeConfigEntrySchema, | |
| 22 | 22 | provideExecutorRuntime, |
| 23 | − pluginLocalConfigSourceFromConfig, | |
| 23 | + pluginScopeConfigSourceFromConfig, | |
| 24 | 24 | runtimeEffectError, |
| 25 | 25 | SecretMaterialResolverService, |
| 26 | 26 | } from "@executor/platform-sdk/runtime"; |
| @@ -157,7 +157,7 @@ const OpenApiLocalSourceConfigSchema = Schema.Struct({ · 130 unmodified lines | ||
| 157 | 157 | }); |
| 158 | 158 | |
| 159 | 159 | const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption( |
| 160 | − createPluginLocalConfigEntrySchema({ | |
| 160 | + createPluginScopeConfigEntrySchema({ | |
| 161 | 161 | kind: "openapi", |
| 162 | 162 | config: OpenApiLocalSourceConfigSchema, |
| 163 | 163 | }), |
| @@ -193,7 +193,7 @@ const configFromStoredSourceData = ( · 29 unmodified lines | ||
| 193 | 193 | |
| 194 | 194 | const openApiStoredSourceDataFromLocalConfig = (input: { |
| 195 | 195 | config: unknown; |
| 196 | − loadedConfig: LocalExecutorConfig | null; | |
| 196 | + loadedConfig: ExecutorScopeConfig | null; | |
| 197 | 197 | }): OpenApiStoredSourceData => { |
| 198 | 198 | const current = decodeOpenApiCurrentLocalConfigOption(input.config); |
| 199 | 199 | if (Option.isSome(current)) { |
| @@ -539,9 +539,9 @@ export const openApiSdkPlugin = ( · 339 unmodified lines | ||
| 539 | 539 | toConfig: ({ source, stored }) => |
| 540 | 540 | configFromStoredSourceData(source, normalizeStoredSourceData(stored)), |
| 541 | 541 | }, |
| 542 | − localConfig: { | |
| 542 | + scopeConfig: { | |
| 543 | 543 | toConfigSource: ({ source, stored }) => |
| 544 | − pluginLocalConfigSourceFromConfig({ | |
| 544 | + pluginScopeConfigSourceFromConfig({ | |
| 545 | 545 | source, |
| 546 | 546 | config: { |
| 547 | 547 | specUrl: stored.specUrl, |